Compare commits
2 commits
master
...
lockscreen
Author | SHA1 | Date | |
---|---|---|---|
|
22433427bf | ||
|
b75fd52909 |
434 changed files with 5967 additions and 18742 deletions
|
@ -1,22 +0,0 @@
|
|||
|
||||
{
|
||||
"type": "android",
|
||||
"stages": [
|
||||
{
|
||||
"name": "testWithMapsWithAnalyticsForPlayDebugComposer",
|
||||
"needsEmulator": true
|
||||
},
|
||||
{
|
||||
"name": "lint",
|
||||
"needsEmulator": false
|
||||
},
|
||||
{
|
||||
"name": "test",
|
||||
"needsEmulator": false
|
||||
},
|
||||
{
|
||||
"name": "assembleRelease",
|
||||
"needsEmulator": false
|
||||
}
|
||||
]
|
||||
}
|
6
.github/CONTRIBUTING.md
vendored
6
.github/CONTRIBUTING.md
vendored
|
@ -1,6 +0,0 @@
|
|||
# Contributing Guidelines
|
||||
|
||||
You are very welcome to submit PullRequests and Issues. Please keep the PullRequests small if not otherwise possible. Contact me *before* you are creating a big PR to avoid unnecessary work and rebasing of big PRs.
|
||||
|
||||
If you have a question please ask the [community](https://plus.google.com/communities/116353894782342292067).
|
||||
|
12
.github/FUNDING.yml
vendored
12
.github/FUNDING.yml
vendored
|
@ -1,12 +0,0 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: ligi
|
||||
patreon: ligi
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
5
.github/ISSUE_TEMPLATE.md
vendored
5
.github/ISSUE_TEMPLATE.md
vendored
|
@ -1,5 +0,0 @@
|
|||
# Creating a issue
|
||||
|
||||
Thanks for your feedback - this is really important to improve the project!
|
||||
Please make issues atomar and closeable. This means if you have one idea for an enhancement and experience a bug - please submit 2 issues and don't pack it all in one.
|
||||
Please try to give a lot of detail - so ideally the issue can be reproduced. At the very minimum it is important to know what version of the app and what version of android you are using and where you got the app from.
|
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,7 +0,0 @@
|
|||
# PullRequest
|
||||
|
||||
Great that you are thinking about submitting a PullRequest!
|
||||
|
||||
Please keep them small if not otherwise possible. Contact me *before* you are creating a big PR to avoid unnecessary work and rebasing of big PRs. Also try to add tests - I am not dogmatic about that but prefer PRs backed by tests. This project has everything setup for Espresso UI and Unit-tests. Also the existing unit and UI-tests muss pass before submitting a PullRequest.
|
||||
|
||||
Please base the PullRequests on the branch named dev if one currently exists - otherwise use master
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -4,7 +4,7 @@ build
|
|||
#assets
|
||||
bin
|
||||
gen
|
||||
proguard-project.txt
|
||||
project.properties
|
||||
gradle.properties
|
||||
local.properties
|
||||
*iml
|
||||
|
||||
|
|
18
.tx/config
18
.tx/config
|
@ -1,18 +0,0 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[passandroid.strings]
|
||||
file_filter = android/src/main/res/values-<lang>/strings.xml
|
||||
source_file = android/src/main/res/values/strings.xml
|
||||
source_lang = en
|
||||
|
||||
[passandroid.play_short_description]
|
||||
file_filter = meta/txt/<lang>/play_short_description.txt
|
||||
source_file = meta/txt/en/play_short_description.txt
|
||||
source_lang = en
|
||||
|
||||
[passandroid.play_description]
|
||||
file_filter = meta/txt/<lang>/play_description.txt
|
||||
source_file = meta/txt/en/play_description.txt
|
||||
source_lang = en
|
||||
|
44
Jenkinsfile
vendored
44
Jenkinsfile
vendored
|
@ -1,44 +0,0 @@
|
|||
|
||||
node {
|
||||
def flavorCombination='WithMapsWithAnalyticsForPlay'
|
||||
|
||||
stage 'checkout'
|
||||
checkout scm
|
||||
|
||||
stage 'UITest'
|
||||
lock('adb') {
|
||||
try {
|
||||
sh "./gradlew clean spoon${flavorCombination}"
|
||||
} catch(err) {
|
||||
currentBuild.result = FAILURE
|
||||
} finally {
|
||||
publishHTML(target:[allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: "android/build/spoon", reportFiles: '*/debug/index.html', reportName: 'Spoon'])
|
||||
step([$class: 'JUnitResultArchiver', testResults: 'android/build/spoon/*/debug/junit-reports/*.xml'])
|
||||
}
|
||||
}
|
||||
|
||||
stage 'lint'
|
||||
try {
|
||||
sh "./gradlew lint${flavorCombination}Release"
|
||||
} catch(err) {
|
||||
currentBuild.result = FAILURE
|
||||
} finally {
|
||||
androidLint canComputeNew: false, defaultEncoding: '', healthy: '', pattern: '', unHealthy: ''
|
||||
}
|
||||
|
||||
stage 'test'
|
||||
try {
|
||||
sh "./gradlew test${flavorCombination}DebugUnitTest"
|
||||
} catch(err) {
|
||||
currentBuild.result = FAILURE
|
||||
} finally {
|
||||
step([$class: 'JUnitResultArchiver', testResults: 'android/build/test-results/*/*.xml'])
|
||||
publishHTML(target:[allowMissing: true, alwaysLinkToLastBuild: true, keepAll: true, reportDir: 'android/build/reports/tests/', reportFiles: "*/index.html", reportName: 'UnitTest'])
|
||||
}
|
||||
|
||||
stage 'assemble'
|
||||
sh "./gradlew assemble${flavorCombination}Release"
|
||||
archive 'android/build/outputs/apk/*'
|
||||
archive 'android/build/outputs/mapping/*/release/mapping.txt'
|
||||
|
||||
}
|
26
README.md
26
README.md
|
@ -1,28 +1,20 @@
|
|||
[![on Google Play](https://ligi.de/img/play_badge.png)](https://play.google.com/store/apps/details?id=org.ligi.passandroid)
|
||||
[![on FDroid](https://ligi.de/img/fdroid_badge.png)](https://f-droid.org/repository/browse/?fdid=org.ligi.passandroid)
|
||||
[![on Amazon](https://ligi.de/img/amazon_badge.png)](https://www.amazon.com/ligi-Passandroid/dp/B01LX9DMSQ)
|
||||
[![Build Status](https://ligi.ci.cloudbees.com/job/PassAndroid/badge/icon)](https://ligi.ci.cloudbees.com/job/PassAndroid/)
|
||||
|
||||
# PassAndroid
|
||||
[![Android app on Google Play](https://developer.android.com/images/brand/en_app_rgb_wo_60.png)](https://play.google.com/store/apps/details?id=org.ligi.passandroid)
|
||||
|
||||
Android App to view Passes (e.g. event tickets, coupons, loyalty cards, boarding passes, ...)
|
||||
PassAndroid
|
||||
===========
|
||||
|
||||
![Screenshots](https://ligi.de/img/passandroid_screenshots.png)
|
||||
Android App to view Passbook files
|
||||
|
||||
Displays [esPass](https://espass.it) (`*.esPass`) & Passbook (`*.pkpass`) files, shows the Barcode (QR, PDF417, AZTEC, Code 39 and Code 128 format) and is also usable offline.
|
||||
<img src="https://raw.github.com/ligi/PassAndroid/master/gfx/promo/1024x500.png"/>
|
||||
|
||||
When preparing for the Chaos Communication Congress 2012 #29c3 I stumbled upon a passbook file for the first time.
|
||||
I really like the idea of paperless tickets as it saves time and trees which are both very valuable to me.
|
||||
The problem was that I was unable to find an app to open and use the downloaded passbook files with, that's why I wrote my own one.
|
||||
|
||||
## Legal
|
||||
|
||||
This project is licensed under the [GNU General Public License v3.0](COPYING).
|
||||
|
||||
We are not affiliated with Apple - Passbook might be trademarked by Apple, but it's introduced like a standard so this should be okay.
|
||||
Displays Passbook ( *.pkpass ) files & shows the Barcode ( QR, PDF417 and AZTEC format ). It useable offline.
|
||||
When preparing for the Chaos Communication Congress 2012 ( #29c3 ) I stumbled upon a passbook file for the first time. As I really like the idea of paperless tickets as it saves time and trees which both are very valuable to me. The problem was that I found no app with which I could use the downloaded passbook file. I found 2 apps which promised to do it, but both failed and judging by the comments: not only for me. Badly written intent-filters where one of the problems, but as both apps where closed source there was no option to submit a fix. After reading a bit I realized that this app can be written within one hour. the passbook format is just a zip container with some Json encoded data and some images. the essential thing is the Barcode message which is included in the json.
|
||||
It is not pretty at the moment, but functional ..
|
||||
|
||||
THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
|
|
@ -1,195 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.trevjonez.composer'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'de.mobilej.unmock'
|
||||
apply plugin: 'com.github.triplet.play'
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenLocal()
|
||||
google()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
|
||||
android {
|
||||
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
versionCode 356
|
||||
versionName "3.5.6"
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 29
|
||||
applicationId "org.ligi.passandroid"
|
||||
testInstrumentationRunner "org.ligi.passandroid.AppReplacingRunner"
|
||||
archivesBaseName = "PassAndroid-$versionName"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
|
||||
flavorDimensions "maps", "analytics", "distribution"
|
||||
|
||||
productFlavors {
|
||||
|
||||
withMaps {
|
||||
dimension "maps"
|
||||
}
|
||||
|
||||
noMaps {
|
||||
dimension "maps"
|
||||
}
|
||||
|
||||
withAnalytics {
|
||||
dimension "analytics"
|
||||
}
|
||||
|
||||
noAnalytics {
|
||||
dimension "analytics"
|
||||
}
|
||||
|
||||
forFDroid {
|
||||
dimension "distribution"
|
||||
}
|
||||
|
||||
forPlay {
|
||||
dimension "distribution"
|
||||
}
|
||||
forAmazon {
|
||||
dimension "distribution"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
android.variantFilter { variant ->
|
||||
def maps = variant.getFlavors().get(0).name
|
||||
def analytics = variant.getFlavors().get(1).name
|
||||
def distribution = variant.getFlavors().get(2).name
|
||||
|
||||
variant.setIgnore((project.hasProperty("singleFlavor") && (distribution != 'forPlay'))
|
||||
|| ((distribution == 'forAmazon' || distribution == 'forPlay') && analytics == 'noAnalytics')
|
||||
|| ((distribution == 'forAmazon' || distribution == 'forPlay') && maps == 'noMaps')
|
||||
|| (distribution == 'forFDroid' && analytics == 'withAnalytics')
|
||||
|| (distribution == 'forFDroid' && maps == 'withMaps'))
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
// needed for assertJ
|
||||
exclude 'asm-license.txt'
|
||||
exclude 'LICENSE'
|
||||
exclude 'NOTICE'
|
||||
|
||||
// hack for instrumentation testing :-(
|
||||
exclude 'LICENSE.txt'
|
||||
|
||||
exclude 'META-INF/maven/com.google.guava/guava/pom.properties'
|
||||
exclude 'META-INF/maven/com.google.guava/guava/pom.xml'
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
warning 'MissingTranslation'
|
||||
warning 'InvalidPackage'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
|
||||
}
|
||||
|
||||
debug {
|
||||
multiDexEnabled true
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.permissionsdispatcher:permissionsdispatcher:4.6.0'
|
||||
kapt 'org.permissionsdispatcher:permissionsdispatcher-processor:4.6.0'
|
||||
|
||||
implementation "org.koin:koin-android:2.1.2"
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
|
||||
|
||||
androidTestImplementation 'com.github.ligi:trulesk:0.31'
|
||||
androidTestUtil 'com.linkedin.testbutler:test-butler-app:2.1.0'
|
||||
|
||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
|
||||
androidTestImplementation 'com.squareup.assertj:assertj-android:1.2.0'
|
||||
androidTestImplementation "org.mockito:mockito-core:$mockito_version"
|
||||
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:2.25.1'
|
||||
androidTestImplementation 'com.google.code.findbugs:jsr305:3.0.2'
|
||||
androidTestImplementation 'org.threeten:threetenbp:1.4.1'
|
||||
androidTestImplementation 'com.android.support:multidex:1.0.3'
|
||||
|
||||
|
||||
implementation 'com.github.ligi:TouchImageView:2.1'
|
||||
implementation 'com.github.ligi:ExtraCompats:1.0'
|
||||
implementation 'net.lingala.zip4j:zip4j:2.3.2'
|
||||
implementation 'com.jakewharton.threetenabp:threetenabp:1.2.2'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.annotation:annotation:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'net.i2p.android.ext:floatingactionbutton:1.10.1'
|
||||
|
||||
implementation 'com.github.ligi:KAXT:1.0'
|
||||
implementation 'com.github.ligi:KAXTUI:1.0'
|
||||
implementation 'com.github.ligi:loadtoast:1.10.11'
|
||||
implementation 'com.github.ligi:tracedroid:3.0'
|
||||
|
||||
forPlayImplementation 'com.github.ligi.snackengage:snackengage-playrate:0.24'
|
||||
forFDroidImplementation 'com.github.ligi.snackengage:snackengage-playrate:0.24'
|
||||
forAmazonImplementation 'com.github.ligi.snackengage:snackengage-amazonrate:0.24'
|
||||
|
||||
// https://medium.com/square-corner-blog/okhttp-3-13-requires-android-5-818bb78d07ce
|
||||
// Don't update to >=3.13 before minSDK 21 + Java 8
|
||||
//noinspection GradleDependency
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.1'
|
||||
|
||||
implementation 'com.larswerkman:HoloColorPicker:1.5'
|
||||
implementation 'com.google.code.findbugs:jsr305:3.0.2'
|
||||
|
||||
implementation 'com.squareup.okio:okio:2.2.2'
|
||||
|
||||
implementation 'com.squareup.moshi:moshi:1.9.2'
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.9.2")
|
||||
|
||||
implementation 'com.chibatching.kotpref:kotpref:2.10.0'
|
||||
implementation 'com.chibatching.kotpref:initializer:2.10.0'
|
||||
testImplementation 'androidx.annotation:annotation:1.1.0'
|
||||
testImplementation 'com.squareup.assertj:assertj-android:1.2.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation "org.mockito:mockito-core:$mockito_version"
|
||||
testImplementation 'org.threeten:threetenbp:1.4.1'
|
||||
|
||||
// https://github.com/ligi/PassAndroid/issues/181
|
||||
// Don't upgrade before minSDK 19 - or replace zxing
|
||||
//noinspection GradleDependency
|
||||
implementation 'com.google.zxing:core:3.3.0'
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
|
||||
|
||||
// requires minSDK 16 according to docs, not sure if it causes an issue
|
||||
withAnalyticsImplementation 'com.google.android.gms:play-services-analytics:17.0.0'
|
||||
withMapsImplementation 'com.google.android.gms:play-services-maps:17.0.0'
|
||||
}
|
||||
|
||||
play {
|
||||
jsonFile = file('/media/ligi/USBCRED/play.json')
|
||||
uploadImages = true
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
# To enable ProGuard in your project, edit project.properties
|
||||
# to define the proguard.config property as described in that file.
|
||||
#
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in ${sdk.dir}/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the ProGuard
|
||||
# include property in project.properties.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
#http://stackoverflow.com/questions/19274974/android-badparcelableexception-only-with-signed-apk
|
||||
-keep class * implements android.os.Parcelable {
|
||||
public static final android.os.Parcelable$Creator *;
|
||||
}
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# optimize
|
||||
-optimizationpasses 2
|
||||
-optimizations !code/simplification/arithmetic
|
||||
-dontusemixedcaseclassnames
|
||||
-dontskipnonpubliclibraryclasses
|
||||
|
||||
# Keep line numbers to alleviate debugging stack traces
|
||||
|
||||
-renamesourcefileattribute SourceFile
|
||||
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
### for api client
|
||||
|
||||
-keepattributes Signature,RuntimeVisibleAnnotations,AnnotationDefault
|
||||
|
||||
-keepclassmembers class * {
|
||||
@com.google.api.client.util.Key <fields>;
|
||||
}
|
||||
|
||||
# Needed by Guava
|
||||
# See https://groups.google.com/forum/#!topic/guava-discuss/YCZzeCiIVoI
|
||||
|
||||
-dontwarn sun.misc.Unsafe
|
||||
-dontwarn com.google.common.collect.MinMaxPriorityQueue
|
||||
|
||||
# Needed by google-http-client-android when linking against an older platform version
|
||||
|
||||
-dontwarn com.google.api.client.extensions.android.**
|
||||
|
||||
# Needed by google-api-client-android when linking against an older platform version
|
||||
|
||||
-dontwarn com.google.api.client.googleapis.extensions.android.**
|
||||
|
||||
|
||||
#### for butterknife
|
||||
-dontwarn butterknife.internal.**
|
||||
-keep class **$$ViewBinder { *; }
|
||||
-keepnames class * { @butterknife.Bind *;}
|
||||
|
||||
#### for guava
|
||||
-dontwarn javax.annotation.**
|
||||
-dontwarn javax.inject.**
|
||||
-dontwarn sun.misc.Unsafe
|
||||
-dontwarn com.google.common.collect.MinMaxPriorityQueue
|
||||
|
||||
-keep,allowoptimization class com.google.inject.** { *; }
|
||||
-keep,allowoptimization class javax.inject.** { *; }
|
||||
-keep,allowoptimization class javax.annotation.** { *; }
|
||||
-keep,allowoptimization class com.google.inject.Binder
|
||||
|
||||
-keepclasseswithmembers public class * {
|
||||
public static void main(java.lang.String[]);
|
||||
}
|
||||
|
||||
-keepclassmembers,allowoptimization class com.google.common.* {
|
||||
void finalizeReferent();
|
||||
void startFinalizer(java.lang.Class,java.lang.Object);
|
||||
}
|
||||
|
||||
-keepclassmembers class * {
|
||||
@com.google.common.eventbus.Subscribe *;
|
||||
}
|
||||
|
||||
|
||||
-dontwarn java.nio.file.Files
|
||||
-dontwarn java.nio.file.Path
|
||||
-dontwarn java.nio.file.OpenOption
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
|
||||
|
||||
## New rules for EventBus 3.0.x ##
|
||||
# http://greenrobot.org/eventbus/documentation/proguard/
|
||||
|
||||
-keepattributes *Annotation*
|
||||
-keepclassmembers class * {
|
||||
@org.greenrobot.eventbus.Subscribe <methods>;
|
||||
}
|
||||
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
|
||||
|
||||
# Only required if you use AsyncExecutor
|
||||
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
|
||||
<init>(java.lang.Throwable);
|
||||
}
|
||||
|
||||
### for moshi
|
||||
|
||||
-keepclassmembers class ** {
|
||||
@com.squareup.moshi.FromJson *;
|
||||
@com.squareup.moshi.ToJson *;
|
||||
}
|
||||
|
||||
|
||||
# Application classes that will be serialized/deserialized via Moshi, keepclassmembers
|
||||
|
||||
-keep class org.ligi.passandroid.model.** { *; }
|
||||
-keepclassmembers class org.ligi.passandroid.model.** { *; }
|
||||
|
||||
# http://stackoverflow.com/questions/37431372/cant-find-referenced-class-with-proguard-and-kotlin
|
||||
-keep class org.ligi.passandroid.model.InputStreamWithSource
|
||||
-keep class org.ligi.passandroid.ui.PassExporter
|
||||
-keep class org.ligi.passandroid.helper.CategoryHelper
|
||||
-keep enum org.ligi.passandroid.model.pass.PassBarCodeFormat
|
||||
-keep class org.ligi.passandroid.helper.CategoryHelper
|
||||
-keep class org.ligi.passandroid.ui.MoveToNewTopicUI
|
||||
|
||||
# the below line needs some investigation - was only needed after switching event classes to kotlin
|
||||
-keep class org.ligi.passandroid.events.** { *; }
|
||||
|
||||
# Kotlin specific
|
||||
-dontwarn kotlin.**
|
||||
|
||||
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
|
||||
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
|
||||
}
|
||||
|
||||
## okhttp
|
||||
|
||||
# JSR 305 annotations are for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
# A resource is loaded with a relative path so the package of this class must be preserved.
|
||||
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||
|
||||
# Animal Sniffer compileOnly dependency to ensure APIs are compatible with older versions of Java.
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
||||
|
||||
# OkHttp platform used only on JVM and when Conscrypt dependency is available.
|
||||
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"format_version":2,
|
||||
|
||||
"what": {
|
||||
"description":"Concert Ticket"
|
||||
},
|
||||
|
||||
"where": {
|
||||
},
|
||||
|
||||
"when": {
|
||||
},
|
||||
|
||||
"secret": {
|
||||
"message":"",
|
||||
"transfer":"barcode_QR",
|
||||
"alternative": {
|
||||
"order":1
|
||||
}
|
||||
},
|
||||
|
||||
"meta" : {
|
||||
"app":"org.ligi.passandroid",
|
||||
"id":"28293fd5-d129-4bd3-b935-22746b4b1248",
|
||||
"type":"event"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import org.ligi.trulesk.AppReplacingRunnerBase
|
||||
|
||||
class AppReplacingRunner : AppReplacingRunnerBase() {
|
||||
|
||||
override fun testAppClass() = TestApp::class.java
|
||||
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
import org.ligi.passandroid.injections.FixedPassListPassStore
|
||||
import org.ligi.passandroid.model.PassStore
|
||||
import org.ligi.passandroid.model.Settings
|
||||
import org.ligi.passandroid.model.comparator.PassSortOrder
|
||||
import org.ligi.passandroid.model.pass.BarCode
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import org.ligi.passandroid.model.pass.PassBarCodeFormat
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.mock
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class TestApp : App() {
|
||||
|
||||
override fun createKoin(): Module {
|
||||
|
||||
return module {
|
||||
single { passStore as PassStore }
|
||||
single { settings }
|
||||
single { tracker }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val tracker = mock(Tracker::class.java)
|
||||
val passStore = FixedPassListPassStore(emptyList())
|
||||
val settings = mock(Settings::class.java).apply {
|
||||
`when`(getSortOrder()).thenReturn(PassSortOrder.DATE_ASC)
|
||||
`when`(getPassesDir()).thenReturn(File(""))
|
||||
`when`(doTraceDroidEmailSend()).thenReturn(false)
|
||||
}
|
||||
|
||||
fun populatePassStoreWithSinglePass() {
|
||||
|
||||
val passList = ArrayList<Pass>()
|
||||
val pass = PassImpl(UUID.randomUUID().toString())
|
||||
pass.description = "description"
|
||||
pass.barCode = BarCode(PassBarCodeFormat.AZTEC, "messageprobe")
|
||||
passList.add(pass)
|
||||
|
||||
fixedPassListPassStore().setList(passList)
|
||||
|
||||
passStore.classifier.moveToTopic(pass, "test")
|
||||
}
|
||||
|
||||
fun emptyPassStore() {
|
||||
fixedPassListPassStore().setList(emptyList())
|
||||
}
|
||||
|
||||
private fun fixedPassListPassStore() = passStore as FixedPassListPassStore
|
||||
}
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import android.app.Activity.RESULT_CANCELED
|
||||
import android.app.Instrumentation
|
||||
import android.provider.CalendarContract
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.intent.Intents.intended
|
||||
import androidx.test.espresso.intent.Intents.intending
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasType
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.hamcrest.Matchers.allOf
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.functions.DEFAULT_EVENT_LENGTH_IN_HOURS
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
import org.ligi.passandroid.ui.PassListActivity
|
||||
import org.ligi.trulesk.TruleskIntentRule
|
||||
import org.threeten.bp.ZonedDateTime
|
||||
|
||||
class TheAddToCalendar {
|
||||
|
||||
private val time = ZonedDateTime.now()
|
||||
private val time2 = ZonedDateTime.now().plusHours(3)
|
||||
|
||||
@get:Rule
|
||||
var rule = TruleskIntentRule(PassListActivity::class.java, false)
|
||||
|
||||
@Test
|
||||
fun testIfWeOnlyHaveCalendarStartDate() {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
|
||||
TestApp.passStore.currentPass!!.calendarTimespan = PassImpl.TimeSpan(time)
|
||||
rule.launchActivity()
|
||||
|
||||
intending(hasType("vnd.android.cursor.item/event")).respondWith(Instrumentation.ActivityResult(RESULT_CANCELED, null))
|
||||
|
||||
onView(withId(R.id.timeButton)).perform(click())
|
||||
|
||||
intended(allOf(
|
||||
hasType("vnd.android.cursor.item/event"),
|
||||
hasExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, time.toEpochSecond() * 1000),
|
||||
hasExtra(CalendarContract.EXTRA_EVENT_END_TIME, time.plusHours(DEFAULT_EVENT_LENGTH_IN_HOURS).toEpochSecond() * 1000),
|
||||
hasExtra("title", TestApp.passStore.currentPass!!.description)
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfWeOnlyHaveCalendarEndDate() {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
|
||||
TestApp.passStore.currentPass!!.calendarTimespan = PassImpl.TimeSpan(to = time)
|
||||
rule.launchActivity()
|
||||
|
||||
intending(hasType("vnd.android.cursor.item/event")).respondWith(Instrumentation.ActivityResult(RESULT_CANCELED, null))
|
||||
|
||||
onView(withId(R.id.timeButton)).perform(click())
|
||||
|
||||
intended(allOf(
|
||||
hasType("vnd.android.cursor.item/event"),
|
||||
hasExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, time.minusHours(DEFAULT_EVENT_LENGTH_IN_HOURS).toEpochSecond() * 1000),
|
||||
hasExtra(CalendarContract.EXTRA_EVENT_END_TIME, time.toEpochSecond() * 1000),
|
||||
hasExtra("title", TestApp.passStore.currentPass!!.description)
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfWeOnlyHaveCalendarStartAndEndDate() {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
|
||||
TestApp.passStore.currentPass!!.calendarTimespan = PassImpl.TimeSpan(time, time2)
|
||||
rule.launchActivity()
|
||||
|
||||
intending(hasType("vnd.android.cursor.item/event")).respondWith(Instrumentation.ActivityResult(RESULT_CANCELED, null))
|
||||
|
||||
onView(withId(R.id.timeButton)).perform(click())
|
||||
|
||||
intended(allOf(
|
||||
hasType("vnd.android.cursor.item/event"),
|
||||
hasExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, time.toEpochSecond() * 1000),
|
||||
hasExtra(CalendarContract.EXTRA_EVENT_END_TIME, time2.toEpochSecond() * 1000),
|
||||
hasExtra("title", TestApp.passStore.currentPass!!.description)
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testIfWeOnlyHaveExpirationDate() {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
|
||||
(TestApp.passStore.currentPass as PassImpl).validTimespans = listOf(PassImpl.TimeSpan(time))
|
||||
rule.launchActivity()
|
||||
|
||||
intending(hasType("vnd.android.cursor.item/event")).respondWith(Instrumentation.ActivityResult(RESULT_CANCELED, null))
|
||||
|
||||
onView(withId(R.id.timeButton)).perform(click())
|
||||
|
||||
onView(withText(R.string.expiration_date_to_calendar_warning_title)).check(matches(isDisplayed()))
|
||||
onView(withText(android.R.string.ok)).perform(click())
|
||||
|
||||
intended(allOf(
|
||||
hasType("vnd.android.cursor.item/event"),
|
||||
hasExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, time.toEpochSecond() * 1000),
|
||||
hasExtra(CalendarContract.EXTRA_EVENT_END_TIME, time.plusHours(DEFAULT_EVENT_LENGTH_IN_HOURS).toEpochSecond() * 1000),
|
||||
hasExtra("title", TestApp.passStore.currentPass!!.description)
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfWeOnlyHaveExpirationEndDate() {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
|
||||
(TestApp.passStore.currentPass as PassImpl).validTimespans = listOf(PassImpl.TimeSpan(to = time))
|
||||
rule.launchActivity()
|
||||
|
||||
intending(hasType("vnd.android.cursor.item/event")).respondWith(Instrumentation.ActivityResult(RESULT_CANCELED, null))
|
||||
|
||||
onView(withId(R.id.timeButton)).perform(click())
|
||||
|
||||
onView(withText(R.string.expiration_date_to_calendar_warning_title)).check(matches(isDisplayed()))
|
||||
onView(withText(android.R.string.ok)).perform(click())
|
||||
|
||||
intended(allOf(
|
||||
hasType("vnd.android.cursor.item/event"),
|
||||
hasExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, time.minusHours(DEFAULT_EVENT_LENGTH_IN_HOURS).toEpochSecond() * 1000),
|
||||
hasExtra(CalendarContract.EXTRA_EVENT_END_TIME, time.toEpochSecond() * 1000),
|
||||
hasExtra("title", TestApp.passStore.currentPass!!.description)
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIfWeOnlyHaveExpirationStartAndEndDate() {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
|
||||
(TestApp.passStore.currentPass as PassImpl).validTimespans = listOf(PassImpl.TimeSpan(time, time2))
|
||||
rule.launchActivity()
|
||||
|
||||
intending(hasType("vnd.android.cursor.item/event")).respondWith(Instrumentation.ActivityResult(RESULT_CANCELED, null))
|
||||
|
||||
onView(withId(R.id.timeButton)).perform(click())
|
||||
|
||||
onView(withText(R.string.expiration_date_to_calendar_warning_title)).check(matches(isDisplayed()))
|
||||
onView(withText(android.R.string.ok)).perform(click())
|
||||
|
||||
intended(allOf(
|
||||
hasType("vnd.android.cursor.item/event"),
|
||||
hasExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, time.toEpochSecond() * 1000),
|
||||
hasExtra(CalendarContract.EXTRA_EVENT_END_TIME, time2.toEpochSecond() * 1000),
|
||||
hasExtra("title", TestApp.passStore.currentPass!!.description)
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThereIsNoButtonWithNoDate() {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
rule.launchActivity()
|
||||
onView(withId(R.id.timeButton)).check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,153 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import android.Manifest
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.espresso.Espresso.closeSoftKeyboard
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.*
|
||||
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SdkSuppress
|
||||
import com.linkedin.android.testbutler.TestButler
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.ligi.passandroid.model.PassStore
|
||||
import org.ligi.passandroid.model.pass.PassBarCodeFormat
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
import org.ligi.passandroid.ui.PassEditActivity
|
||||
import org.ligi.trulesk.TruleskActivityRule
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TheBarCodeEditing {
|
||||
|
||||
@get:Rule
|
||||
val rule = TruleskActivityRule(PassEditActivity::class.java, false)
|
||||
|
||||
val passStore: PassStore = TestApp.passStore
|
||||
|
||||
private lateinit var currentPass: PassImpl
|
||||
|
||||
private fun start(setupPass: (pass: PassImpl) -> Unit = {}) {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
|
||||
currentPass = passStore.currentPass as PassImpl
|
||||
|
||||
setupPass(currentPass)
|
||||
|
||||
TestButler.grantPermission(ApplicationProvider.getApplicationContext(), Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
TestButler.grantPermission(ApplicationProvider.getApplicationContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
|
||||
rule.launchActivity(null)
|
||||
closeSoftKeyboard()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNullBarcodeShowButtonAppears() {
|
||||
|
||||
start {
|
||||
it.barCode = null
|
||||
}
|
||||
|
||||
rule.screenShot("no_barcode")
|
||||
|
||||
onView(withId(R.id.add_barcode_button)).perform(scrollTo())
|
||||
onView(withId(R.id.add_barcode_button)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testCreateBarcodeDefaultsToQR() {
|
||||
|
||||
start {
|
||||
it.barCode = null
|
||||
}
|
||||
|
||||
onView(withId(R.id.add_barcode_button)).perform(scrollTo(), click())
|
||||
|
||||
closeSoftKeyboard()
|
||||
|
||||
onView(withText(android.R.string.ok)).perform(click())
|
||||
|
||||
assertThat(currentPass.barCode!!.format).isEqualTo(PassBarCodeFormat.QR_CODE)
|
||||
}
|
||||
|
||||
@SdkSuppress(minSdkVersion = 14)
|
||||
@Test
|
||||
fun testCanSetToAllBarcodeTypes() {
|
||||
start()
|
||||
for (passBarCodeFormat in PassBarCodeFormat.values()) {
|
||||
onView(withId(R.id.barcode_img)).perform(scrollTo(), click())
|
||||
|
||||
onView(withText(passBarCodeFormat.name)).perform(scrollTo(), click())
|
||||
|
||||
onView(withId(R.id.randomButton)).perform(click())
|
||||
closeSoftKeyboard()
|
||||
|
||||
onView(withText(android.R.string.ok)).perform(click())
|
||||
|
||||
assertThat(currentPass.barCode!!.format).isEqualTo(passBarCodeFormat)
|
||||
rule.screenShot("edit_set_" + passBarCodeFormat.name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testCanSetMessage() {
|
||||
start()
|
||||
|
||||
onView(withId(R.id.barcode_img)).perform(click())
|
||||
|
||||
onView(withId(R.id.messageInput)).perform(clearText())
|
||||
onView(withId(R.id.messageInput)).perform(replaceText("msg foo txt ;-)"))
|
||||
|
||||
closeSoftKeyboard()
|
||||
|
||||
onView(withText(android.R.string.ok)).perform(click())
|
||||
|
||||
onView(withText(R.string.edit_barcode_dialog_title)).check(doesNotExist())
|
||||
|
||||
assertThat(passStore.currentPass!!.barCode!!.message).isEqualTo("msg foo txt ;-)")
|
||||
rule.screenShot("edit_set_msg")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testCanSetAltMessage() {
|
||||
start()
|
||||
|
||||
onView(withId(R.id.barcode_img)).perform(click())
|
||||
|
||||
onView(withId(R.id.alternativeMessageInput)).perform(clearText())
|
||||
onView(withId(R.id.alternativeMessageInput)).perform(replaceText("alt bar txt ;-)"))
|
||||
|
||||
closeSoftKeyboard()
|
||||
|
||||
onView(withText(android.R.string.ok)).perform(click())
|
||||
|
||||
onView(withText(R.string.edit_barcode_dialog_title)).check(doesNotExist())
|
||||
|
||||
assertThat(passStore.currentPass!!.barCode!!.alternativeText).isEqualTo("alt bar txt ;-)")
|
||||
rule.screenShot("edit_set_altmsg")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatRandomChangesMessage() {
|
||||
start()
|
||||
|
||||
onView(withId(R.id.barcode_img)).perform(click())
|
||||
|
||||
val oldMessage = passStore.currentPass!!.barCode!!.message
|
||||
onView(withId(R.id.randomButton)).perform(click())
|
||||
|
||||
closeSoftKeyboard()
|
||||
|
||||
onView(withText(android.R.string.ok)).perform(click())
|
||||
|
||||
assertThat(oldMessage).isNotEqualTo(passStore.currentPass!!.barCode!!.message)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Fail.fail
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.functions.generateBarCodeBitmap
|
||||
import org.ligi.passandroid.functions.getBitMatrix
|
||||
import org.ligi.passandroid.model.pass.PassBarCodeFormat
|
||||
|
||||
class TheBarcodeHelper {
|
||||
|
||||
@Test
|
||||
fun testQRBitMatrixHasCorrectSize() {
|
||||
testBitMatrixSizeIsSane(PassBarCodeFormat.QR_CODE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testQRBitmapHasCorrectSize() {
|
||||
testBitmapSizeIsSane(PassBarCodeFormat.QR_CODE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPDF417BitmapHasCorrectSize() {
|
||||
testBitmapSizeIsSane(PassBarCodeFormat.PDF_417)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPDF417BitMatrixHasCorrectSize() {
|
||||
testBitMatrixSizeIsSane(PassBarCodeFormat.PDF_417)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAZTECBitmapHasCorrectSize() {
|
||||
testBitmapSizeIsSane(PassBarCodeFormat.AZTEC)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAZTECBitMatrixHasCorrectSize() {
|
||||
testBitMatrixSizeIsSane(PassBarCodeFormat.AZTEC)
|
||||
}
|
||||
|
||||
fun testBitMatrixSizeIsSane(format: PassBarCodeFormat) {
|
||||
try {
|
||||
val tested = getBitMatrix("foo-data", format)
|
||||
|
||||
assertThat(tested.width).isGreaterThan(3)
|
||||
} catch (e: Exception) {
|
||||
fail("could not create barcode", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun testBitmapSizeIsSane(format: PassBarCodeFormat) {
|
||||
try {
|
||||
val tested2 = generateBarCodeBitmap("foo-data", format)!!
|
||||
|
||||
assertThat(tested2.width).isGreaterThan(3)
|
||||
} catch (e: Exception) {
|
||||
fail("could not create barcode", e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.functions.getHumanCategoryString
|
||||
import org.ligi.passandroid.model.PassDefinitions
|
||||
|
||||
class TheCategoryHelper {
|
||||
|
||||
@Test
|
||||
fun testAllCategoriesAreTranslated() {
|
||||
|
||||
val allTranslationSet = PassDefinitions.TYPE_TO_NAME.keys
|
||||
.map(::getHumanCategoryString)
|
||||
.toSet()
|
||||
|
||||
assertThat(allTranslationSet.size).isEqualTo(PassDefinitions.TYPE_TO_NAME.keys.size)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import org.hamcrest.CoreMatchers.containsString
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.model.pass.PassField
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
import org.ligi.passandroid.ui.PassListActivity
|
||||
import org.ligi.trulesk.TruleskActivityRule
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.threeten.bp.ZoneId
|
||||
import org.threeten.bp.ZonedDateTime
|
||||
|
||||
class TheCondensedPassViewMode {
|
||||
|
||||
@get:Rule
|
||||
var rule = TruleskActivityRule(PassListActivity::class.java, false) {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
val currentPass = TestApp.passStore.currentPass as PassImpl
|
||||
currentPass.calendarTimespan = PassImpl.TimeSpan(ZonedDateTime.of(2016, 11, 23, 20, 42, 42, 5, ZoneId.systemDefault()))
|
||||
currentPass.fields = mutableListOf(PassField("textprobe", "bar", "yo", false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDateShowsForCondensedOff() {
|
||||
`when`(TestApp.settings.isCondensedModeEnabled()).thenReturn(false)
|
||||
|
||||
rule.launchActivity()
|
||||
|
||||
onView(withId(R.id.date)).check(matches(withText(containsString("23"))))
|
||||
onView(withId(R.id.timeButton)).check(matches(withText(R.string.pass_to_calendar)))
|
||||
|
||||
rule.screenShot("condensed_off")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testFieldShowsForCondensedOn() {
|
||||
|
||||
`when`(TestApp.settings.isCondensedModeEnabled()).thenReturn(true)
|
||||
|
||||
rule.launchActivity()
|
||||
|
||||
onView(withId(R.id.date)).check(matches(withText(containsString("bar"))))
|
||||
onView(withId(R.id.timeButton)).check(matches(withText(containsString("23"))))
|
||||
|
||||
rule.screenShot("condensed_on")
|
||||
}
|
||||
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.R.id.emptyView
|
||||
import org.ligi.passandroid.functions.checkThatHelpIsThere
|
||||
import org.ligi.passandroid.ui.PassListActivity
|
||||
import org.ligi.trulesk.TruleskIntentRule
|
||||
|
||||
|
||||
class TheEmptyPassList {
|
||||
|
||||
@get:Rule
|
||||
var rule = TruleskIntentRule(PassListActivity::class.java) {
|
||||
TestApp.emptyPassStore()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmptyViewIsThereWhenThereAreNoPasses() {
|
||||
rule.screenShot("empty_view")
|
||||
onView(withId(emptyView)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHelpGoesToHelp() {
|
||||
onView(withId(R.id.menu_help)).perform(click())
|
||||
|
||||
checkThatHelpIsThere()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.replaceText
|
||||
import androidx.test.espresso.action.ViewActions.scrollTo
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.model.pass.PassField
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
import org.ligi.passandroid.ui.PassEditActivity
|
||||
import org.ligi.trulesk.TruleskIntentRule
|
||||
import java.util.*
|
||||
|
||||
class TheFieldListEditFragment {
|
||||
|
||||
@get:Rule
|
||||
val rule = TruleskIntentRule(PassEditActivity::class.java) {
|
||||
TestApp.passStore.currentPass = PassImpl(UUID.randomUUID().toString()).apply {
|
||||
fields = arrayListOf(field)
|
||||
}
|
||||
}
|
||||
|
||||
private val field: PassField = PassField(null, "labelfieldcontent", "valuefieldcontent", false)
|
||||
|
||||
@Test
|
||||
fun testFieldDetailsArePreFilled() {
|
||||
|
||||
rule.screenShot("one_field")
|
||||
|
||||
onView(withId(R.id.label_field_edit)).perform(scrollTo())
|
||||
onView(withId(R.id.label_field_edit)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.label_field_edit)).check(matches(withText("labelfieldcontent")))
|
||||
|
||||
onView(withId(R.id.value_field_edit)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.value_field_edit)).check(matches(withText("valuefieldcontent")))
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testThatChangingLabelWorks() {
|
||||
|
||||
onView(withId(R.id.label_field_edit)).perform(scrollTo())
|
||||
onView(withId(R.id.label_field_edit)).perform(replaceText("newlabel"))
|
||||
assertThat(field.label).isEqualTo("newlabel")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatChangingValueWorks() {
|
||||
|
||||
onView(withId(R.id.value_field_edit)).perform(scrollTo())
|
||||
onView(withId(R.id.value_field_edit)).perform(replaceText("newvalue"))
|
||||
assertThat(field.value).isEqualTo("newvalue")
|
||||
}
|
||||
|
||||
/* TODO add tests for delete and add */
|
||||
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.widget.ImageView
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.functions.decodeBarCode
|
||||
import org.ligi.passandroid.model.pass.BarCode
|
||||
import org.ligi.passandroid.model.pass.PassBarCodeFormat
|
||||
import org.ligi.passandroid.model.pass.PassBarCodeFormat.*
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
import org.ligi.passandroid.ui.FullscreenBarcodeActivity
|
||||
import org.ligi.trulesk.TruleskIntentRule
|
||||
import java.util.*
|
||||
|
||||
private const val BARCODE_MESSAGE = "2323"
|
||||
|
||||
class TheFullscreenBarcodeActivity {
|
||||
|
||||
@get:Rule
|
||||
var rule = TruleskIntentRule(FullscreenBarcodeActivity::class.java, false)
|
||||
|
||||
@Test
|
||||
fun testPDF417BarcodeIsShown() {
|
||||
testWithBarcodeFormat(PDF_417)
|
||||
|
||||
rule.screenShot("pdf417_barcode")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAztecBarcodeIsShown() {
|
||||
testWithBarcodeFormat(AZTEC)
|
||||
|
||||
rule.screenShot("aztec_barcode")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testQRCodeIsShown() {
|
||||
testWithBarcodeFormat(QR_CODE)
|
||||
|
||||
rule.screenShot("qr_barcode")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCode128CodeIsShown() {
|
||||
testWithBarcodeFormat(CODE_128)
|
||||
|
||||
rule.screenShot("code128_barcode")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testCode39CodeIsShown() {
|
||||
testWithBarcodeFormat(CODE_39)
|
||||
|
||||
rule.screenShot("code39_barcode")
|
||||
}
|
||||
|
||||
|
||||
private fun testWithBarcodeFormat(format: PassBarCodeFormat) {
|
||||
val pass = PassImpl(UUID.randomUUID().toString())
|
||||
pass.barCode = BarCode(format, BARCODE_MESSAGE)
|
||||
|
||||
TestApp.passStore.currentPass = pass
|
||||
|
||||
rule.launchActivity(null)
|
||||
onView(withId(R.id.fullscreen_barcode)).check(matches(isDisplayed()))
|
||||
|
||||
val viewById = rule.activity.findViewById(R.id.fullscreen_barcode) as ImageView
|
||||
val bitmapDrawable = viewById.drawable as BitmapDrawable
|
||||
val bitmap = bitmapDrawable.bitmap
|
||||
|
||||
val bitmapToTest: Bitmap
|
||||
bitmapToTest = if (format === PassBarCodeFormat.AZTEC) {
|
||||
// not sure why - but for the decoder to pick up AZTEC it must have moar pixelz - smells like a zxing bug
|
||||
Bitmap.createScaledBitmap(bitmap, bitmap.width * 2, bitmap.height * 2, false)
|
||||
} else {
|
||||
bitmap
|
||||
}
|
||||
|
||||
assertThat(bitmapToTest.decodeBarCode()).isEqualTo(BARCODE_MESSAGE)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.functions.checkThatHelpIsThere
|
||||
import org.ligi.passandroid.ui.HelpActivity
|
||||
import org.ligi.trulesk.TruleskActivityRule
|
||||
|
||||
class TheHelpActivity {
|
||||
|
||||
@get:Rule
|
||||
val rule = TruleskActivityRule(HelpActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun testHelpIsThere() {
|
||||
checkThatHelpIsThere()
|
||||
rule.screenShot("help")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_that_help_finishes_on_home() {
|
||||
onView(withContentDescription(R.string.abc_action_bar_up_description)).perform(click())
|
||||
|
||||
assertThat(rule.activity.isFinishing).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_that_version_is_shown() {
|
||||
onView(withText("v" + BuildConfig.VERSION_NAME)).check(matches(isDisplayed()))
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity.RESULT_CANCELED
|
||||
import android.app.Instrumentation.ActivityResult
|
||||
import android.content.Intent.ACTION_SEND
|
||||
import android.content.Intent.ACTION_VIEW
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.Espresso.pressBack
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.contrib.DrawerActions.open
|
||||
import androidx.test.espresso.intent.Intents.intended
|
||||
import androidx.test.espresso.intent.Intents.intending
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.*
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import org.hamcrest.Matchers.allOf
|
||||
import org.hamcrest.Matchers.not
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.R.string.*
|
||||
import org.ligi.passandroid.ui.PassListActivity
|
||||
import org.ligi.passandroid.ui.PreferenceActivity
|
||||
import org.ligi.trulesk.TruleskIntentRule
|
||||
|
||||
@TargetApi(14)
|
||||
class TheNavigationDrawer {
|
||||
|
||||
@get:Rule
|
||||
var rule = TruleskIntentRule(PassListActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun testNavigationDrawerIsUsuallyNotShown() {
|
||||
onView(withId(R.id.navigationView)).check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatNavigationDrawerOpens() {
|
||||
onView(withId(R.id.drawer_layout)).perform(open())
|
||||
onView(withId(R.id.navigationView)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatNavigationDrawerClosesOnBackPress() {
|
||||
testThatNavigationDrawerOpens()
|
||||
|
||||
pressBack()
|
||||
|
||||
onView(withId(R.id.navigationView)).check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBetatestClick() {
|
||||
testThatNavigationDrawerOpens()
|
||||
rule.screenShot("open_drawer")
|
||||
|
||||
intending(hasAction(ACTION_VIEW)).respondWith(ActivityResult(RESULT_CANCELED, null))
|
||||
|
||||
onView(withText(nav_betatest_opt_in_out)).perform(click())
|
||||
|
||||
intended(allOf(hasAction(ACTION_VIEW), hasData("https://play.google.com/apps/testing/org.ligi.passandroid")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGitHubClick() {
|
||||
testThatNavigationDrawerOpens()
|
||||
rule.screenShot("open_drawer")
|
||||
|
||||
intending(hasAction(ACTION_VIEW)).respondWith(ActivityResult(RESULT_CANCELED, null))
|
||||
|
||||
onView(withText(nav_github)).perform(click())
|
||||
|
||||
intended(allOf(hasAction(ACTION_VIEW), hasData("https://github.com/ligi/PassAndroid")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testImproveTranslationsClick() {
|
||||
testThatNavigationDrawerOpens()
|
||||
rule.screenShot("open_drawer")
|
||||
|
||||
intending(hasAction(ACTION_VIEW)).respondWith(ActivityResult(RESULT_CANCELED, null))
|
||||
|
||||
onView(withText(nav_improve_translation)).perform(click())
|
||||
|
||||
intended(allOf(hasAction(ACTION_VIEW), hasData("https://transifex.com/projects/p/passandroid")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShareClick() {
|
||||
testThatNavigationDrawerOpens()
|
||||
rule.screenShot("open_drawer")
|
||||
|
||||
intending(hasAction(ACTION_SEND)).respondWith(ActivityResult(RESULT_CANCELED, null))
|
||||
|
||||
onView(withText(nav_share)).perform(click())
|
||||
|
||||
intended(allOf(hasAction(ACTION_SEND), hasType("text/plain")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSettings() {
|
||||
testThatNavigationDrawerOpens()
|
||||
rule.screenShot("open_drawer")
|
||||
|
||||
onView(withText(nav_settings)).perform(click())
|
||||
|
||||
intended(hasComponent(PreferenceActivity::class.java.name))
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import android.content.Intent
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.*
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.intent.Intents.intended
|
||||
import androidx.test.espresso.intent.Intents.intending
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.linkedin.android.testbutler.TestButler
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.model.pass.PassType.COUPON
|
||||
import org.ligi.passandroid.model.pass.PassType.EVENT
|
||||
import org.ligi.passandroid.ui.PassEditActivity
|
||||
import org.ligi.trulesk.TruleskIntentRule
|
||||
|
||||
@TargetApi(14)
|
||||
class ThePassEditActivity {
|
||||
|
||||
val passStore = TestApp.passStore
|
||||
|
||||
@get:Rule
|
||||
var rule = TruleskIntentRule(PassEditActivity::class.java) {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
TestButler.grantPermission(InstrumentationRegistry.getInstrumentation().targetContext, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
TestButler.grantPermission(InstrumentationRegistry.getInstrumentation().targetContext, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetToEventWorks() {
|
||||
onView(withId(R.id.categoryView)).perform(click())
|
||||
|
||||
onView(withText(R.string.select_category_dialog_title)).perform(click())
|
||||
onView(withText(R.string.category_event)).perform(click())
|
||||
assertThat(passStore.currentPass!!.type).isEqualTo(EVENT)
|
||||
|
||||
rule.screenShot("edit_set_event")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetToCouponWorks() {
|
||||
onView(withId(R.id.categoryView)).perform(click())
|
||||
|
||||
onView(withText(R.string.select_category_dialog_title)).perform(click())
|
||||
onView(withText(R.string.category_coupon)).perform(click())
|
||||
assertThat(passStore.currentPass!!.type).isEqualTo(COUPON)
|
||||
|
||||
rule.screenShot("edit_set_coupon")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetDescriptionWorks() {
|
||||
|
||||
onView(withId(R.id.passTitle)).perform(clearText(), replaceText("test description"))
|
||||
assertThat(passStore.currentPass!!.description).isEqualTo("test description")
|
||||
|
||||
rule.screenShot("edit_set_description")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testColorWheelIsThere() {
|
||||
|
||||
onView(withId(R.id.categoryView)).perform(click())
|
||||
onView(withText(R.string.change_color_dialog_title)).perform(click())
|
||||
|
||||
onView(withId(R.id.colorPicker)).check(matches(isDisplayed()))
|
||||
|
||||
rule.screenShot("edit_set_color")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testAddAbortFooterImagePick() {
|
||||
intending(hasAction(Intent.ACTION_CHOOSER)).respondWith(Instrumentation.ActivityResult(Activity.RESULT_CANCELED, null))
|
||||
|
||||
onView(withId(R.id.add_footer)).perform(scrollTo(), click())
|
||||
|
||||
intended(hasAction(Intent.ACTION_CHOOSER))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddAbortStripImagePick() {
|
||||
intending(hasAction(Intent.ACTION_CHOOSER)).respondWith(Instrumentation.ActivityResult(Activity.RESULT_CANCELED, null))
|
||||
|
||||
onView(withId(R.id.add_strip)).perform(scrollTo(), click())
|
||||
|
||||
intended(hasAction(Intent.ACTION_CHOOSER))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddAbortLogoImagePick() {
|
||||
|
||||
intending(hasAction(Intent.ACTION_CHOOSER)).respondWith(Instrumentation.ActivityResult(Activity.RESULT_CANCELED, null))
|
||||
|
||||
onView(withId(R.id.add_logo)).perform(scrollTo(), click())
|
||||
|
||||
intended(hasAction(Intent.ACTION_CHOOSER))
|
||||
}
|
||||
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.Espresso.pressBack
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.R.id.pass_recyclerview
|
||||
import org.ligi.passandroid.functions.checkThatHelpIsThere
|
||||
import org.ligi.passandroid.functions.expand
|
||||
import org.ligi.passandroid.functions.isCollapsed
|
||||
import org.ligi.passandroid.ui.PassListActivity
|
||||
import org.ligi.trulesk.TruleskActivityRule
|
||||
|
||||
@TargetApi(14)
|
||||
class ThePassListActivity {
|
||||
|
||||
@get:Rule
|
||||
var rule = TruleskActivityRule(PassListActivity::class.java) {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testListIsThere() {
|
||||
|
||||
onView(withId(pass_recyclerview)).check(matches(isDisplayed()))
|
||||
rule.screenShot("list")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHelpMenuBringsUsToHelp() {
|
||||
onView(withId(R.id.menu_help)).perform(click())
|
||||
|
||||
checkThatHelpIsThere()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCloseFabOnBackPressed() {
|
||||
onView(withId(R.id.fam)).perform(expand())
|
||||
|
||||
pressBack()
|
||||
|
||||
onView(withId(R.id.fam))
|
||||
.check(matches(isDisplayed()))
|
||||
.check(matches(isCollapsed()))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testOpenVisibleOn19plus() {
|
||||
onView(withId(R.id.fam)).perform(expand())
|
||||
|
||||
pressBack()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 19) {
|
||||
onView(withId(R.id.fab_action_open_file)).check(matches(isDisplayed()))
|
||||
} else {
|
||||
onView(withId(R.id.fab_action_open_file)).check(matches(withEffectiveVisibility(Visibility.GONE)))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.action.ViewActions.replaceText
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.ui.PassListActivity
|
||||
import org.ligi.passandroid.ui.PassListFragment
|
||||
import org.ligi.trulesk.TruleskIntentRule
|
||||
|
||||
const val CUSTOM_PROBE = "FOO_PROBE"
|
||||
|
||||
class ThePassListSwiping {
|
||||
|
||||
@get:Rule
|
||||
val rule = TruleskIntentRule(PassListActivity::class.java) {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWeCanMoveToTrash() {
|
||||
fakeSwipeLeft()
|
||||
|
||||
onView(withText(R.string.topic_trash)).perform(click())
|
||||
|
||||
assertThat(TestApp.passStore.classifier.getTopics()).containsExactly(rule.activity.getString(R.string.topic_trash))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testWeCanMoveToArchive() {
|
||||
fakeSwipeLeft()
|
||||
|
||||
onView(withText(R.string.topic_archive)).perform(click())
|
||||
|
||||
assertThat(TestApp.passStore.classifier.getTopics()).containsExactly(rule.activity.getString(R.string.topic_archive))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWeCanMoveToCustom() {
|
||||
|
||||
fakeSwipeLeft()
|
||||
|
||||
onView(withId(R.id.new_topic_edit)).perform(replaceText(CUSTOM_PROBE))
|
||||
|
||||
onView(withText(android.R.string.ok)).perform(click())
|
||||
|
||||
assertThat(TestApp.passStore.classifier.getTopics()).containsExactly(CUSTOM_PROBE)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testDialogOpensWhenSwipeLeft() {
|
||||
fakeSwipeLeft()
|
||||
onView(withText(R.string.move_to_new_topic)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDialogOpensWhenSwipeRight() {
|
||||
|
||||
fakeSwipeRight()
|
||||
|
||||
rule.screenShot("move_to_new_topic_dialog")
|
||||
|
||||
onView(withText(R.string.move_to_new_topic)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
/*
|
||||
we have to fake swiping as the espresso methods swipeLeft and swipeRight made
|
||||
these tests flaky - more info here:
|
||||
http://stackoverflow.com/questions/35397439/swipe-tests-flaky-on-recyclerview
|
||||
*/
|
||||
private fun fakeSwipe(dir: Int) {
|
||||
rule.activity.runOnUiThread {
|
||||
val fragment = rule.activity.supportFragmentManager.fragments.firstOrNull { it is PassListFragment } as PassListFragment
|
||||
fragment.onSwiped(0, dir)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fakeSwipeRight() = fakeSwipe(ItemTouchHelper.RIGHT)
|
||||
private fun fakeSwipeLeft() = fakeSwipe(ItemTouchHelper.LEFT)
|
||||
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.*
|
||||
import org.hamcrest.core.IsNot.not
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.model.pass.BarCode
|
||||
import org.ligi.passandroid.model.pass.PassBarCodeFormat
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
import org.ligi.passandroid.model.pass.PassLocation
|
||||
import org.ligi.passandroid.ui.PassViewActivity
|
||||
import org.ligi.trulesk.TruleskActivityRule
|
||||
import org.threeten.bp.ZonedDateTime
|
||||
import java.util.*
|
||||
|
||||
@TargetApi(14)
|
||||
class ThePassViewActivity {
|
||||
|
||||
private fun getActPass() = TestApp.passStore.currentPass as PassImpl
|
||||
|
||||
@get:Rule
|
||||
var rule = TruleskActivityRule(PassViewActivity::class.java, false)
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatDescriptionIsThere() {
|
||||
rule.launchActivity(null)
|
||||
|
||||
onView(withText(getActPass().description)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDateIsGoneWhenPassbookHasNoDate() {
|
||||
getActPass().validTimespans = ArrayList()
|
||||
rule.launchActivity(null)
|
||||
|
||||
onView(withId(R.id.date)).check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEverythingWorksWhenWeHaveSomeLocation() {
|
||||
val timeSpen = ArrayList<PassLocation>()
|
||||
timeSpen.add(PassLocation())
|
||||
getActPass().locations = timeSpen
|
||||
rule.launchActivity(null)
|
||||
|
||||
onView(withId(R.id.date)).check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testDateIsThereWhenPassbookHasDate() {
|
||||
getActPass().calendarTimespan = PassImpl.TimeSpan(ZonedDateTime.now(), null, null)
|
||||
rule.launchActivity(null)
|
||||
|
||||
onView(withId(R.id.date)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLinkToCalendarIsThereWhenPassbookHasDate() {
|
||||
getActPass().calendarTimespan = PassImpl.TimeSpan(ZonedDateTime.now(), null, null)
|
||||
rule.launchActivity(null)
|
||||
|
||||
onView(withText(R.string.pass_to_calendar)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClickOnCalendarWithExpirationDateGivesWarning() {
|
||||
val validTimespans = ArrayList<PassImpl.TimeSpan>()
|
||||
validTimespans.add(PassImpl.TimeSpan(null, ZonedDateTime.now().minusHours(12), null))
|
||||
getActPass().validTimespans = validTimespans
|
||||
getActPass().calendarTimespan = null
|
||||
rule.launchActivity(null)
|
||||
|
||||
onView(withText(R.string.pass_to_calendar)).perform(click())
|
||||
|
||||
onView(withText(R.string.expiration_date_to_calendar_warning_message)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testThatTheDialogCanBeDismissed() {
|
||||
testClickOnCalendarWithExpirationDateGivesWarning()
|
||||
|
||||
onView(withText(android.R.string.cancel)).perform(click())
|
||||
|
||||
onView(withText(R.string.expiration_date_to_calendar_warning_message)).check(doesNotExist())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLinkToCalendarIsNotThereWhenPassbookHasNoDate() {
|
||||
getActPass().validTimespans = ArrayList()
|
||||
rule.launchActivity(null)
|
||||
|
||||
onView(withText(R.string.pass_to_calendar)).check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClickOnBarcodeOpensFullscreenImage() {
|
||||
getActPass().barCode = BarCode(PassBarCodeFormat.QR_CODE, "foo")
|
||||
rule.launchActivity(null)
|
||||
onView(withId(R.id.barcode_img)).perform(click())
|
||||
|
||||
onView(withId(R.id.fullscreen_barcode)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testZoomControlsAreThereWithBarcode() {
|
||||
getActPass().barCode = BarCode(PassBarCodeFormat.AZTEC, "foo")
|
||||
rule.launchActivity(null)
|
||||
|
||||
onView(withId(R.id.zoomIn)).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.zoomIn)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testZoomControlsAreGoneWithoutBarcode() {
|
||||
getActPass().barCode = null
|
||||
rule.launchActivity(null)
|
||||
|
||||
onView(withId(R.id.zoomIn)).check(matches(not(isDisplayed())))
|
||||
onView(withId(R.id.zoomIn)).check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import org.hamcrest.CoreMatchers.not
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
import org.ligi.passandroid.model.pass.PassLocation
|
||||
import org.ligi.passandroid.ui.PassListActivity
|
||||
import org.ligi.trulesk.TruleskActivityRule
|
||||
import org.threeten.bp.ZoneId
|
||||
import org.threeten.bp.ZonedDateTime
|
||||
|
||||
class ThePassViewHolder {
|
||||
|
||||
private val currentPass by lazy {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
TestApp.passStore.currentPass as PassImpl
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
var rule = TruleskActivityRule(PassListActivity::class.java, false)
|
||||
|
||||
@Test
|
||||
fun locationButtonShouldBeVisibleIfWeHaveALocation() {
|
||||
|
||||
currentPass.locations = listOf(PassLocation())
|
||||
|
||||
rule.launchActivity()
|
||||
|
||||
onView(withId(R.id.locationButton)).check(ViewAssertions.matches(isDisplayed()))
|
||||
|
||||
rule.screenShot("with_location")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun locationButtonShouldNotShowIfWeHaveNoLocation() {
|
||||
|
||||
currentPass.locations = listOf()
|
||||
|
||||
rule.launchActivity()
|
||||
|
||||
onView(withId(R.id.locationButton)).check(ViewAssertions.matches(not(isDisplayed())))
|
||||
|
||||
rule.screenShot("no_location")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun dateButtonShouldBeVisibleIfWeHaveADate() {
|
||||
currentPass.calendarTimespan = PassImpl.TimeSpan(ZonedDateTime.of(2016, 11, 23, 20, 42, 42, 5, ZoneId.systemDefault()))
|
||||
|
||||
rule.launchActivity()
|
||||
|
||||
onView(withId(R.id.timeButton)).check(ViewAssertions.matches(isDisplayed()))
|
||||
|
||||
rule.screenShot("with_date")
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun dateButtonShouldNotBeVisibleIfWeHaveNoDate() {
|
||||
currentPass.calendarTimespan = null
|
||||
|
||||
rule.launchActivity()
|
||||
|
||||
onView(withId(R.id.timeButton)).check(ViewAssertions.matches(not(isDisplayed())))
|
||||
|
||||
rule.screenShot("no_date")
|
||||
}
|
||||
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.model.PastLocationsStore
|
||||
import org.ligi.passandroid.ui.PassViewActivity
|
||||
import org.ligi.trulesk.TruleskActivityRule
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
class ThePastLocationsStore {
|
||||
|
||||
@get:Rule
|
||||
var rule = TruleskActivityRule(PassViewActivity::class.java) {
|
||||
TestApp.populatePassStoreWithSinglePass()
|
||||
MockitoAnnotations.initMocks(this)
|
||||
}
|
||||
|
||||
@Mock
|
||||
lateinit var tracker: Tracker
|
||||
|
||||
private val prefs: SharedPreferences by lazy { InstrumentationRegistry.getInstrumentation().context.getSharedPreferences("" + System.currentTimeMillis() / 100000, Context.MODE_PRIVATE) }
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
prefs.edit().clear().apply()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPastLocationsStoreShouldNeverContainMoreThanMaxElements() {
|
||||
val tested = PastLocationsStore(prefs, tracker)
|
||||
|
||||
for (i in 0 until PastLocationsStore.MAX_ELEMENTS * 2) {
|
||||
tested.putLocation("" + i)
|
||||
}
|
||||
|
||||
assertThat(tested.locations.size).isEqualTo(PastLocationsStore.MAX_ELEMENTS)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPastLocationsStoreShouldStoreOnlyOneOfAKind() {
|
||||
val tested = PastLocationsStore(prefs, tracker)
|
||||
|
||||
for (i in 0..2) {
|
||||
tested.putLocation("foo")
|
||||
}
|
||||
|
||||
assertThat(tested.locations).containsOnly("foo")
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import com.linkedin.android.testbutler.TestButler
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.model.AndroidSettings
|
||||
import org.ligi.passandroid.model.comparator.PassByTimeComparator
|
||||
import org.ligi.passandroid.model.comparator.PassByTypeFirstAndTimeSecondComparator
|
||||
import org.ligi.passandroid.model.comparator.PassTemporalDistanceComparator
|
||||
import org.ligi.passandroid.ui.PreferenceActivity
|
||||
import org.ligi.trulesk.TruleskActivityRule
|
||||
|
||||
class ThePreferenceActivity {
|
||||
|
||||
@get:Rule
|
||||
val rule = TruleskActivityRule(PreferenceActivity::class.java) {
|
||||
TestButler.grantPermission(ApplicationProvider.getApplicationContext(), Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
TestButler.grantPermission(ApplicationProvider.getApplicationContext(), Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
}
|
||||
|
||||
private val androidSettings by lazy { AndroidSettings(rule.activity) }
|
||||
|
||||
@Test
|
||||
fun autoLightToggles() {
|
||||
rule.screenShot("preferences")
|
||||
|
||||
val automaticLightEnabled = androidSettings.isAutomaticLightEnabled()
|
||||
|
||||
onView(withText(R.string.preference_autolight_title)).perform(click())
|
||||
|
||||
assertThat(automaticLightEnabled).isEqualTo(!androidSettings.isAutomaticLightEnabled())
|
||||
|
||||
onView(withText(R.string.preference_autolight_title)).perform(click())
|
||||
|
||||
assertThat(automaticLightEnabled).isEqualTo(androidSettings.isAutomaticLightEnabled())
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun condensedToggles() {
|
||||
|
||||
val condensedModeEnabled = androidSettings.isCondensedModeEnabled()
|
||||
|
||||
onView(withText(R.string.preference_condensed_title)).perform(click())
|
||||
|
||||
assertThat(condensedModeEnabled).isEqualTo(!androidSettings.isCondensedModeEnabled())
|
||||
|
||||
onView(withText(R.string.preference_condensed_title)).perform(click())
|
||||
|
||||
assertThat(condensedModeEnabled).isEqualTo(androidSettings.isCondensedModeEnabled())
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun weCanSetAllSortOrders() {
|
||||
|
||||
val resources = rule.activity.resources
|
||||
val sortOrders = resources.getStringArray(R.array.sort_orders)
|
||||
sortOrders.forEach { sortOrder ->
|
||||
|
||||
onView(withText(R.string.preference_sort_title)).perform(click())
|
||||
onView(withText(sortOrder)).perform(click())
|
||||
|
||||
assertThat(androidSettings.getSortOrder().toComparator()).isInstanceOf(when (sortOrder) {
|
||||
resources.getString(R.string.sort_order_date_asc) -> PassByTimeComparator::class.java
|
||||
resources.getString(R.string.sort_order_date_desc) -> PassByTimeComparator::class.java
|
||||
resources.getString(R.string.sort_order_date_type) -> PassByTypeFirstAndTimeSecondComparator::class.java
|
||||
resources.getString(R.string.sort_order_date_temporaldistance) -> PassTemporalDistanceComparator::class.java
|
||||
else -> throw RuntimeException("unexpected sort order")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun weCanSetAllNightModes() {
|
||||
|
||||
val resources = rule.activity.resources
|
||||
val sortOrders = resources.getStringArray(R.array.night_modes)
|
||||
|
||||
sortOrders.filterNot { Build.VERSION.SDK_INT >= 21 && it == resources.getString(R.string.night_mode_auto) }.forEach { sortOrder ->
|
||||
|
||||
onView(withText(R.string.preference_daynight_title)).perform(click())
|
||||
onView(withText(sortOrder)).perform(click())
|
||||
|
||||
assertThat(androidSettings.getNightMode()).isEqualTo(when (sortOrder) {
|
||||
resources.getString(R.string.night_mode_day) -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
resources.getString(R.string.night_mode_night) -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
resources.getString(R.string.night_mode_auto) -> AppCompatDelegate.MODE_NIGHT_AUTO
|
||||
else -> throw RuntimeException("unexpected night-mode")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import org.ligi.passandroid.functions.loadPassFromAsset
|
||||
|
||||
class TheQuirkCorrector {
|
||||
|
||||
@Test
|
||||
fun testWestbahnDescriptionIsFixed() {
|
||||
loadPassFromAsset("passes/workarounds/westbahn/special.pkpass") {
|
||||
assertThat(it!!.description).isEqualTo("Wien Westbahnhof->Amstetten")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
package org.ligi.passandroid;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import java.io.InputStream;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.ligi.passandroid.model.InputStreamWithSource;
|
||||
import org.ligi.passandroid.model.PassStore;
|
||||
import org.ligi.passandroid.ui.UnzipPassController;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import static org.assertj.core.api.Fail.fail;
|
||||
import static org.ligi.passandroid.ui.UnzipPassController.InputStreamUnzipControllerSpec;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
public class TheUnzipPassController {
|
||||
|
||||
@Mock
|
||||
UnzipPassController.FailCallback failCallback;
|
||||
|
||||
@Mock
|
||||
UnzipPassController.SuccessCallback successCallback;
|
||||
|
||||
@Mock
|
||||
PassStore passStore;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShouldFailForBrokenPass() {
|
||||
|
||||
try {
|
||||
|
||||
final InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().getAssets().open("passes/broken/fail.pkpass");
|
||||
final InputStreamWithSource inputStreamWithSource = new InputStreamWithSource("none", inputStream);
|
||||
final InputStreamUnzipControllerSpec spec = new InputStreamUnzipControllerSpec(inputStreamWithSource,
|
||||
InstrumentationRegistry.getInstrumentation().getTargetContext(),
|
||||
passStore,
|
||||
successCallback,
|
||||
failCallback);
|
||||
UnzipPassController.INSTANCE.processInputStream(spec);
|
||||
|
||||
verify(successCallback, never()).call(any(String.class));
|
||||
verify(failCallback).fail(any(String.class));
|
||||
|
||||
} catch (Exception e) {
|
||||
fail("should be able to load file " + e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package org.ligi.passandroid.functions
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.MultiFormatReader
|
||||
import com.google.zxing.RGBLuminanceSource
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
|
||||
fun Bitmap.decodeBarCode(): String {
|
||||
val intArray = IntArray(this.width * this.height)
|
||||
this.getPixels(intArray, 0, this.width, 0, 0, this.width, this.height)
|
||||
|
||||
val source = RGBLuminanceSource(this.width, this.height, intArray)
|
||||
val bitmap = BinaryBitmap(HybridBinarizer(source))
|
||||
|
||||
val reader = MultiFormatReader()// use this otherwise ChecksumException
|
||||
|
||||
val result = reader.decode(bitmap)
|
||||
return result.text
|
||||
}
|
||||
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
package org.ligi.passandroid.functions
|
||||
|
||||
import androidx.test.espresso.UiController
|
||||
import androidx.test.espresso.ViewAction
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
|
||||
import android.view.View
|
||||
import net.i2p.android.ext.floatingactionbutton.FloatingActionsMenu
|
||||
import org.hamcrest.Description
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.TypeSafeMatcher
|
||||
|
||||
fun expand(): ViewAction? = ExpandFabAction()
|
||||
|
||||
class ExpandFabAction : ViewAction {
|
||||
|
||||
override fun getConstraints(): Matcher<View> = isAssignableFrom(FloatingActionsMenu::class.java)
|
||||
|
||||
override fun getDescription() = "expands the floating action menu"
|
||||
|
||||
override fun perform(uiController: UiController?, view: View?) {
|
||||
val fam = view as FloatingActionsMenu
|
||||
fam.expand()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CollapsedCheck : TypeSafeMatcher<FloatingActionsMenu>(FloatingActionsMenu::class.java) {
|
||||
|
||||
override fun describeTo(description: Description?) {
|
||||
description?.appendText("is in collapsed state")
|
||||
}
|
||||
|
||||
override fun matchesSafely(fam: FloatingActionsMenu?): Boolean {
|
||||
return !fam?.isExpanded!!
|
||||
}
|
||||
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package org.ligi.passandroid.functions
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import org.ligi.passandroid.R
|
||||
|
||||
fun checkThatHelpIsThere() {
|
||||
onView(withId(R.id.help_text)).check(matches(isDisplayed()))
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
package org.ligi.passandroid.functions
|
||||
|
||||
import android.view.View
|
||||
import org.hamcrest.Matcher
|
||||
|
||||
fun isCollapsed(): Matcher<in View>? = CollapsedCheck() as Matcher<in View>
|
|
@ -1,50 +0,0 @@
|
|||
package org.ligi.passandroid.functions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.assertj.core.api.Fail.fail
|
||||
import org.ligi.passandroid.TestApp
|
||||
import org.ligi.passandroid.model.InputStreamWithSource
|
||||
import org.ligi.passandroid.model.PassStore
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import org.ligi.passandroid.reader.AppleStylePassReader
|
||||
import org.ligi.passandroid.ui.UnzipPassController
|
||||
import org.ligi.passandroid.ui.UnzipPassController.FailCallback
|
||||
import org.ligi.passandroid.ui.UnzipPassController.InputStreamUnzipControllerSpec
|
||||
import org.mockito.Mockito.*
|
||||
import java.io.File
|
||||
|
||||
|
||||
private fun getTestTargetPath(context: Context) = File(context.cacheDir, "test_passes")
|
||||
|
||||
|
||||
fun loadPassFromAsset(asset: String, callback: (pass: Pass?) -> Unit) {
|
||||
try {
|
||||
|
||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
val inputStream = instrumentation.context.resources.assets.open(asset)
|
||||
val inputStreamWithSource = InputStreamWithSource("none", inputStream)
|
||||
|
||||
val mock = mock(FailCallback::class.java)
|
||||
val spec = InputStreamUnzipControllerSpec(inputStreamWithSource, instrumentation.targetContext, mock(
|
||||
PassStore::class.java),
|
||||
object : UnzipPassController.SuccessCallback {
|
||||
override fun call(uuid: String) {
|
||||
callback.invoke(AppleStylePassReader.read(File(getTestTargetPath(instrumentation.targetContext), uuid), "en",
|
||||
instrumentation.targetContext,TestApp.tracker))
|
||||
}
|
||||
},
|
||||
mock
|
||||
)
|
||||
|
||||
spec.overwrite = true
|
||||
spec.targetPath = getTestTargetPath(spec.context)
|
||||
UnzipPassController.processInputStream(spec)
|
||||
|
||||
verify(mock, never()).fail(anyString())
|
||||
|
||||
} catch (e: Exception) {
|
||||
fail("should be able to load file ", e)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package org.ligi.passandroid.injections
|
||||
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import org.ligi.passandroid.model.PassClassifier
|
||||
import org.ligi.passandroid.model.PassStore
|
||||
import org.ligi.passandroid.model.PassStoreUpdateEvent
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import java.io.File
|
||||
|
||||
class FixedPassListPassStore(private var passes: List<Pass>) : PassStore {
|
||||
|
||||
override lateinit var classifier: PassClassifier
|
||||
|
||||
init {
|
||||
classifier = PassClassifier(HashMap(), this)
|
||||
}
|
||||
|
||||
fun setList(newPasses: List<Pass>, newCurrentPass: Pass? = newPasses.firstOrNull()) {
|
||||
currentPass = newCurrentPass
|
||||
passes = newPasses
|
||||
passMap.clear()
|
||||
passMap.putAll(createHashMap())
|
||||
|
||||
classifier = PassClassifier(HashMap(), this)
|
||||
}
|
||||
|
||||
override var currentPass: Pass? = null
|
||||
|
||||
override val passMap: HashMap<String, Pass> by lazy {
|
||||
return@lazy createHashMap()
|
||||
}
|
||||
|
||||
private fun createHashMap(): HashMap<String, Pass> {
|
||||
val hashMap = HashMap<String, Pass>()
|
||||
|
||||
passes.forEach { hashMap[it.id] = it }
|
||||
return hashMap
|
||||
}
|
||||
|
||||
override fun getPassbookForId(id: String): Pass? {
|
||||
return passMap[id]
|
||||
}
|
||||
|
||||
|
||||
override fun deletePassWithId(id: String): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getPathForID(id: String): File {
|
||||
return File("")
|
||||
}
|
||||
|
||||
override val updateChannel: BroadcastChannel<PassStoreUpdateEvent> = ConflatedBroadcastChannel()
|
||||
|
||||
override fun save(pass: Pass) {
|
||||
// no effect in this impl
|
||||
}
|
||||
|
||||
override fun notifyChange() {
|
||||
// no effect in this impl
|
||||
}
|
||||
|
||||
override fun syncPassStoreWithClassifier(defaultTopic: String) {
|
||||
// no effect in this impl
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="market_url" translatable="false">amzn://apps/android?p=%s</string>
|
||||
<string name="nav_market" translatable="false">Play</string>
|
||||
</resources>
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="market_url" translatable="false">https://f-droid.org/repository/browse/?fdid=%s</string>
|
||||
</resources>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="market_url" translatable="false">https://play.google.com/store/apps/details?id=%s</string>
|
||||
<string name="nav_market" translatable="false">Play</string>
|
||||
</resources>
|
File diff suppressed because it is too large
Load diff
|
@ -1,585 +0,0 @@
|
|||
/*
|
||||
*******************************************************************************
|
||||
* Copyright (C) 2005 - 2012, International Business Machines Corporation and *
|
||||
* others. All Rights Reserved. *
|
||||
*******************************************************************************
|
||||
*/
|
||||
package com.ibm.icu.text;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* <code>CharsetDetector</code> provides a facility for detecting the
|
||||
* charset or encoding of character data in an unknown format.
|
||||
* The input data can either be from an input stream or an array of bytes.
|
||||
* The result of the detection operation is a list of possibly matching
|
||||
* charsets, or, for simple use, you can just ask for a Java Reader that
|
||||
* will will work over the input data.
|
||||
* <p/>
|
||||
* Character set detection is at best an imprecise operation. The detection
|
||||
* process will attempt to identify the charset that best matches the characteristics
|
||||
* of the byte data, but the process is partly statistical in nature, and
|
||||
* the results can not be guaranteed to always be correct.
|
||||
* <p/>
|
||||
* For best accuracy in charset detection, the input data should be primarily
|
||||
* in a single language, and a minimum of a few hundred bytes worth of plain text
|
||||
* in the language are needed. The detection process will attempt to
|
||||
* ignore html or xml style markup that could otherwise obscure the content.
|
||||
* <p/>
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
public class CharsetDetector {
|
||||
|
||||
// Question: Should we have getters corresponding to the setters for input text
|
||||
// and declared encoding?
|
||||
|
||||
// A thought: If we were to create our own type of Java Reader, we could defer
|
||||
// figuring out an actual charset for data that starts out with too much English
|
||||
// only ASCII until the user actually read through to something that didn't look
|
||||
// like 7 bit English. If nothing else ever appeared, we would never need to
|
||||
// actually choose the "real" charset. All assuming that the application just
|
||||
// wants the data, and doesn't care about a char set name.
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public CharsetDetector() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the declared encoding for charset detection.
|
||||
* The declared encoding of an input text is an encoding obtained
|
||||
* from an http header or xml declaration or similar source that
|
||||
* can be provided as additional information to the charset detector.
|
||||
* A match between a declared encoding and a possible detected encoding
|
||||
* will raise the quality of that detected encoding by a small delta,
|
||||
* and will also appear as a "reason" for the match.
|
||||
* <p/>
|
||||
* A declared encoding that is incompatible with the input data being
|
||||
* analyzed will not be added to the list of possible encodings.
|
||||
*
|
||||
* @param encoding The declared encoding
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public CharsetDetector setDeclaredEncoding(String encoding) {
|
||||
fDeclaredEncoding = encoding;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the input text (byte) data whose charset is to be detected.
|
||||
*
|
||||
* @param in the input text of unknown encoding
|
||||
*
|
||||
* @return This CharsetDetector
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public CharsetDetector setText(byte [] in) {
|
||||
fRawInput = in;
|
||||
fRawLength = in.length;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private static final int kBufSize = 8000;
|
||||
|
||||
/**
|
||||
* Set the input text (byte) data whose charset is to be detected.
|
||||
* <p/>
|
||||
* The input stream that supplies the character data must have markSupported()
|
||||
* == true; the charset detection process will read a small amount of data,
|
||||
* then return the stream to its original position via
|
||||
* the InputStream.reset() operation. The exact amount that will
|
||||
* be read depends on the characteristics of the data itself.
|
||||
*
|
||||
* @param in the input text of unknown encoding
|
||||
*
|
||||
* @return This CharsetDetector
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
|
||||
public CharsetDetector setText(InputStream in) throws IOException {
|
||||
fInputStream = in;
|
||||
fInputStream.mark(kBufSize);
|
||||
fRawInput = new byte[kBufSize]; // Always make a new buffer because the
|
||||
// previous one may have come from the caller,
|
||||
// in which case we can't touch it.
|
||||
fRawLength = 0;
|
||||
int remainingLength = kBufSize;
|
||||
while (remainingLength > 0 ) {
|
||||
// read() may give data in smallish chunks, esp. for remote sources. Hence, this loop.
|
||||
int bytesRead = fInputStream.read(fRawInput, fRawLength, remainingLength);
|
||||
if (bytesRead <= 0) {
|
||||
break;
|
||||
}
|
||||
fRawLength += bytesRead;
|
||||
remainingLength -= bytesRead;
|
||||
}
|
||||
fInputStream.reset();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the charset that best matches the supplied input data.
|
||||
*
|
||||
* Note though, that because the detection
|
||||
* only looks at the start of the input data,
|
||||
* there is a possibility that the returned charset will fail to handle
|
||||
* the full set of input data.
|
||||
* <p/>
|
||||
* Raise an exception if
|
||||
* <ul>
|
||||
* <li>no charset appears to match the data.</li>
|
||||
* <li>no input text has been provided</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return a CharsetMatch object representing the best matching charset, or
|
||||
* <code>null</code> if there are no matches.
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public CharsetMatch detect() {
|
||||
// TODO: A better implementation would be to copy the detect loop from
|
||||
// detectAll(), and cut it short as soon as a match with a high confidence
|
||||
// is found. This is something to be done later, after things are otherwise
|
||||
// working.
|
||||
CharsetMatch matches[] = detectAll();
|
||||
|
||||
if (matches == null || matches.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of all charsets that appear to be plausible
|
||||
* matches with the input data. The array is ordered with the
|
||||
* best quality match first.
|
||||
* <p/>
|
||||
* Raise an exception if
|
||||
* <ul>
|
||||
* <li>no charsets appear to match the input data.</li>
|
||||
* <li>no input text has been provided</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return An array of CharsetMatch objects representing possibly matching charsets.
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public CharsetMatch[] detectAll() {
|
||||
ArrayList<CharsetMatch> matches = new ArrayList<>();
|
||||
|
||||
MungeInput(); // Strip html markup, collect byte stats.
|
||||
|
||||
// Iterate over all possible charsets, remember all that
|
||||
// give a match quality > 0.
|
||||
for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) {
|
||||
CSRecognizerInfo rcinfo = ALL_CS_RECOGNIZERS.get(i);
|
||||
boolean active = (fEnabledRecognizers != null) ? fEnabledRecognizers[i] : rcinfo.isDefaultEnabled;
|
||||
if (active) {
|
||||
CharsetMatch m = rcinfo.recognizer.match(this);
|
||||
if (m != null) {
|
||||
matches.add(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
Collections.sort(matches); // CharsetMatch compares on confidence
|
||||
Collections.reverse(matches); // Put best match first.
|
||||
CharsetMatch [] resultArray = new CharsetMatch[matches.size()];
|
||||
resultArray = matches.toArray(resultArray);
|
||||
return resultArray;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Autodetect the charset of an inputStream, and return a Java Reader
|
||||
* to access the converted input data.
|
||||
* <p/>
|
||||
* This is a convenience method that is equivalent to
|
||||
* <code>this.setDeclaredEncoding(declaredEncoding).setText(in).detect().getReader();</code>
|
||||
* <p/>
|
||||
* For the input stream that supplies the character data, markSupported()
|
||||
* must be true; the charset detection will read a small amount of data,
|
||||
* then return the stream to its original position via
|
||||
* the InputStream.reset() operation. The exact amount that will
|
||||
* be read depends on the characteristics of the data itself.
|
||||
*<p/>
|
||||
* Raise an exception if no charsets appear to match the input data.
|
||||
*
|
||||
* @param in The source of the byte data in the unknown charset.
|
||||
*
|
||||
* @param declaredEncoding A declared encoding for the data, if available,
|
||||
* or null or an navigation_drawer_header string if none is available.
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public Reader getReader(InputStream in, String declaredEncoding) {
|
||||
fDeclaredEncoding = declaredEncoding;
|
||||
|
||||
try {
|
||||
setText(in);
|
||||
|
||||
CharsetMatch match = detect();
|
||||
|
||||
if (match == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match.getReader();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Autodetect the charset of an inputStream, and return a String
|
||||
* containing the converted input data.
|
||||
* <p/>
|
||||
* This is a convenience method that is equivalent to
|
||||
* <code>this.setDeclaredEncoding(declaredEncoding).setText(in).detect().getString();</code>
|
||||
*<p/>
|
||||
* Raise an exception if no charsets appear to match the input data.
|
||||
*
|
||||
* @param in The source of the byte data in the unknown charset.
|
||||
*
|
||||
* @param declaredEncoding A declared encoding for the data, if available,
|
||||
* or null or an navigation_drawer_header string if none is available.
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public String getString(byte[] in, String declaredEncoding)
|
||||
{
|
||||
fDeclaredEncoding = declaredEncoding;
|
||||
|
||||
try {
|
||||
setText(in);
|
||||
|
||||
CharsetMatch match = detect();
|
||||
|
||||
if (match == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match.getString(-1);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the names of all charsets supported by <code>CharsetDetector</code> class.
|
||||
* <p>
|
||||
* <b>Note:</b> Multiple different charset encodings in a same family may use
|
||||
* a single shared name in this implementation. For example, this method returns
|
||||
* an array including "ISO-8859-1" (ISO Latin 1), but not including "windows-1252"
|
||||
* (Windows Latin 1). However, actual detection result could be "windows-1252"
|
||||
* when the input data matches Latin 1 code points with any points only available
|
||||
* in "windows-1252".
|
||||
*
|
||||
* @return an array of the names of all charsets supported by
|
||||
* <code>CharsetDetector</code> class.
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public static String[] getAllDetectableCharsets() {
|
||||
String[] allCharsetNames = new String[ALL_CS_RECOGNIZERS.size()];
|
||||
for (int i = 0; i < allCharsetNames.length; i++) {
|
||||
allCharsetNames[i] = ALL_CS_RECOGNIZERS.get(i).recognizer.getName();
|
||||
}
|
||||
return allCharsetNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test whether or not input filtering is enabled.
|
||||
*
|
||||
* @return <code>true</code> if input text will be filtered.
|
||||
*
|
||||
* @see #enableInputFilter
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public boolean inputFilterEnabled()
|
||||
{
|
||||
return fStripTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable filtering of input text. If filtering is enabled,
|
||||
* text within angle brackets ("<" and ">") will be removed
|
||||
* before detection.
|
||||
*
|
||||
* @param filter <code>true</code> to enable input text filtering.
|
||||
*
|
||||
* @return The previous setting.
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public boolean enableInputFilter(boolean filter)
|
||||
{
|
||||
boolean previous = fStripTags;
|
||||
|
||||
fStripTags = filter;
|
||||
|
||||
return previous;
|
||||
}
|
||||
|
||||
/*
|
||||
* MungeInput - after getting a set of raw input data to be analyzed, preprocess
|
||||
* it by removing what appears to be html markup.
|
||||
*/
|
||||
private void MungeInput() {
|
||||
int srci;
|
||||
int dsti = 0;
|
||||
byte b;
|
||||
boolean inMarkup = false;
|
||||
int openTags = 0;
|
||||
int badTags = 0;
|
||||
|
||||
//
|
||||
// html / xml markup stripping.
|
||||
// quick and dirty, not 100% accurate, but hopefully good enough, statistically.
|
||||
// discard everything within < brackets >
|
||||
// Count how many total '<' and illegal (nested) '<' occur, so we can make some
|
||||
// guess as to whether the input was actually marked up at all.
|
||||
if (fStripTags) {
|
||||
for (srci = 0; srci < fRawLength && dsti < fInputBytes.length; srci++) {
|
||||
b = fRawInput[srci];
|
||||
if (b == (byte)'<') {
|
||||
if (inMarkup) {
|
||||
badTags++;
|
||||
}
|
||||
inMarkup = true;
|
||||
openTags++;
|
||||
}
|
||||
|
||||
if (! inMarkup) {
|
||||
fInputBytes[dsti++] = b;
|
||||
}
|
||||
|
||||
if (b == (byte)'>') {
|
||||
inMarkup = false;
|
||||
}
|
||||
}
|
||||
|
||||
fInputLen = dsti;
|
||||
}
|
||||
|
||||
//
|
||||
// If it looks like this input wasn't marked up, or if it looks like it's
|
||||
// essentially nothing but markup abandon the markup stripping.
|
||||
// Detection will have to work on the unstripped input.
|
||||
//
|
||||
if (openTags<5 || openTags/5 < badTags ||
|
||||
(fInputLen < 100 && fRawLength>600)) {
|
||||
int limit = fRawLength;
|
||||
|
||||
if (limit > kBufSize) {
|
||||
limit = kBufSize;
|
||||
}
|
||||
|
||||
for (srci=0; srci<limit; srci++) {
|
||||
fInputBytes[srci] = fRawInput[srci];
|
||||
}
|
||||
fInputLen = srci;
|
||||
}
|
||||
|
||||
//
|
||||
// Tally up the byte occurence statistics.
|
||||
// These are available for use by the various detectors.
|
||||
//
|
||||
Arrays.fill(fByteStats, (short)0);
|
||||
for (srci=0; srci<fInputLen; srci++) {
|
||||
int val = fInputBytes[srci] & 0x00ff;
|
||||
fByteStats[val]++;
|
||||
}
|
||||
|
||||
fC1Bytes = false;
|
||||
for (int i = 0x80; i <= 0x9F; i += 1) {
|
||||
if (fByteStats[i] != 0) {
|
||||
fC1Bytes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The following items are accessed by individual CharsetRecongizers during
|
||||
* the recognition process
|
||||
*
|
||||
*/
|
||||
byte[] fInputBytes = // The text to be checked. Markup will have been
|
||||
new byte[kBufSize]; // removed if appropriate.
|
||||
|
||||
int fInputLen; // Length of the byte data in fInputBytes.
|
||||
|
||||
short fByteStats[] = // byte frequency statistics for the input text.
|
||||
new short[256]; // Value is percent, not absolute.
|
||||
// Value is rounded up, so zero really means zero occurences.
|
||||
|
||||
boolean fC1Bytes = // True if any bytes in the range 0x80 - 0x9F are in the input;
|
||||
false;
|
||||
|
||||
String fDeclaredEncoding;
|
||||
|
||||
|
||||
byte[] fRawInput; // Original, untouched input bytes.
|
||||
// If user gave us a byte array, this is it.
|
||||
// If user gave us a stream, it's read to a
|
||||
// buffer here.
|
||||
int fRawLength; // Length of data in fRawInput array.
|
||||
|
||||
InputStream fInputStream; // User's input stream, or null if the user
|
||||
// gave us a byte array.
|
||||
|
||||
//
|
||||
// Stuff private to CharsetDetector
|
||||
//
|
||||
private boolean fStripTags = // If true, setText() will strip tags from input text.
|
||||
false;
|
||||
|
||||
private boolean[] fEnabledRecognizers; // If not null, active set of charset recognizers had
|
||||
// been changed from the default. The array index is
|
||||
// corresponding to ALL_RECOGNIZER. See setDetectableCharset().
|
||||
|
||||
private static class CSRecognizerInfo {
|
||||
CharsetRecognizer recognizer;
|
||||
boolean isDefaultEnabled;
|
||||
|
||||
CSRecognizerInfo(CharsetRecognizer recognizer, boolean isDefaultEnabled) {
|
||||
this.recognizer = recognizer;
|
||||
this.isDefaultEnabled = isDefaultEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* List of recognizers for all charsets known to the implementation.
|
||||
*/
|
||||
private static final List<CSRecognizerInfo> ALL_CS_RECOGNIZERS;
|
||||
|
||||
static {
|
||||
List<CSRecognizerInfo> list = new ArrayList<>();
|
||||
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_UTF8(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_16_BE(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_16_LE(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_32_BE(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_Unicode.CharsetRecog_UTF_32_LE(), true));
|
||||
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_sjis(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022JP(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022CN(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_2022.CharsetRecog_2022KR(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_gb_18030(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_euc_jp(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_euc.CharsetRecog_euc_kr(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_mbcs.CharsetRecog_big5(), true));
|
||||
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_1(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_2(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_5_ru(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_6_ar(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_7_el(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_8_I_he(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_8_he(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_windows_1251(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_windows_1256(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_KOI8_R(), true));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_8859_9_tr(), true));
|
||||
|
||||
// IBM 420/424 recognizers are disabled by default
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM424_he_rtl(), false));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM424_he_ltr(), false));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM420_ar_rtl(), false));
|
||||
list.add(new CSRecognizerInfo(new CharsetRecog_sbcs.CharsetRecog_IBM420_ar_ltr(), false));
|
||||
|
||||
ALL_CS_RECOGNIZERS = Collections.unmodifiableList(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names of charsets that can be recognized by this CharsetDetector instance.
|
||||
*
|
||||
* @return an array of the names of charsets that can be recognized by this CharsetDetector
|
||||
* instance.
|
||||
*
|
||||
* @internal
|
||||
* @deprecated This API is ICU internal only.
|
||||
*/
|
||||
@Deprecated
|
||||
public String[] getDetectableCharsets() {
|
||||
List<String> csnames = new ArrayList<>(ALL_CS_RECOGNIZERS.size());
|
||||
for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) {
|
||||
CSRecognizerInfo rcinfo = ALL_CS_RECOGNIZERS.get(i);
|
||||
boolean active = (fEnabledRecognizers == null) ? rcinfo.isDefaultEnabled : fEnabledRecognizers[i];
|
||||
if (active) {
|
||||
csnames.add(rcinfo.recognizer.getName());
|
||||
}
|
||||
}
|
||||
return csnames.toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable individual charset encoding.
|
||||
* A name of charset encoding must be included in the names returned by
|
||||
* {@link #getAllDetectableCharsets()}.
|
||||
*
|
||||
* @param encoding the name of charset encoding.
|
||||
* @param enabled <code>true</code> to enable, or <code>false</code> to disable the
|
||||
* charset encoding.
|
||||
* @return A reference to this <code>CharsetDetector</code>.
|
||||
* @throws IllegalArgumentException when the name of charset encoding is
|
||||
* not supported.
|
||||
*
|
||||
* @internal
|
||||
* @deprecated This API is ICU internal only.
|
||||
*/
|
||||
@Deprecated
|
||||
public CharsetDetector setDetectableCharset(String encoding, boolean enabled) {
|
||||
int modIdx = -1;
|
||||
boolean isDefaultVal = false;
|
||||
for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) {
|
||||
CSRecognizerInfo csrinfo = ALL_CS_RECOGNIZERS.get(i);
|
||||
if (csrinfo.recognizer.getName().equals(encoding)) {
|
||||
modIdx = i;
|
||||
isDefaultVal = (csrinfo.isDefaultEnabled == enabled);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (modIdx < 0) {
|
||||
// No matching encoding found
|
||||
throw new IllegalArgumentException("Invalid encoding: " + "\"" + encoding + "\"");
|
||||
}
|
||||
|
||||
if (fEnabledRecognizers == null && !isDefaultVal) {
|
||||
// Create an array storing the non default setting
|
||||
fEnabledRecognizers = new boolean[ALL_CS_RECOGNIZERS.size()];
|
||||
|
||||
// Initialize the array with default info
|
||||
for (int i = 0; i < ALL_CS_RECOGNIZERS.size(); i++) {
|
||||
fEnabledRecognizers[i] = ALL_CS_RECOGNIZERS.get(i).isDefaultEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
if (fEnabledRecognizers != null) {
|
||||
fEnabledRecognizers[modIdx] = enabled;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,246 +0,0 @@
|
|||
/*
|
||||
*******************************************************************************
|
||||
* Copyright (C) 2005 - 2012, International Business Machines Corporation and *
|
||||
* others. All Rights Reserved. *
|
||||
*******************************************************************************
|
||||
*/
|
||||
package com.ibm.icu.text;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
|
||||
|
||||
/**
|
||||
* This class represents a charset that has been identified by a CharsetDetector
|
||||
* as a possible encoding for a set of input data. From an instance of this
|
||||
* class, you can ask for a confidence level in the charset identification,
|
||||
* or for Java Reader or String to access the original byte data in Unicode form.
|
||||
* <p/>
|
||||
* Instances of this class are created only by CharsetDetectors.
|
||||
* <p/>
|
||||
* Note: this class has a natural ordering that is inconsistent with equals.
|
||||
* The natural ordering is based on the match confidence value.
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
public class CharsetMatch implements Comparable<CharsetMatch> {
|
||||
|
||||
|
||||
/**
|
||||
* Create a java.io.Reader for reading the Unicode character data corresponding
|
||||
* to the original byte data supplied to the Charset detect operation.
|
||||
* <p/>
|
||||
* CAUTION: if the source of the byte data was an InputStream, a Reader
|
||||
* can be created for only one matching char set using this method. If more
|
||||
* than one charset needs to be tried, the caller will need to reset
|
||||
* the InputStream and create InputStreamReaders itself, based on the charset name.
|
||||
*
|
||||
* @return the Reader for the Unicode character data.
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public Reader getReader() {
|
||||
InputStream inputStream = fInputStream;
|
||||
|
||||
if (inputStream == null) {
|
||||
inputStream = new ByteArrayInputStream(fRawInput, 0, fRawLength);
|
||||
}
|
||||
|
||||
try {
|
||||
inputStream.reset();
|
||||
return new InputStreamReader(inputStream, getName());
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Java String from Unicode character data corresponding
|
||||
* to the original byte data supplied to the Charset detect operation.
|
||||
*
|
||||
* @return a String created from the converted input data.
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public String getString() throws java.io.IOException {
|
||||
return getString(-1);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Java String from Unicode character data corresponding
|
||||
* to the original byte data supplied to the Charset detect operation.
|
||||
* The length of the returned string is limited to the specified size;
|
||||
* the string will be trunctated to this length if necessary. A limit value of
|
||||
* zero or less is ignored, and treated as no limit.
|
||||
*
|
||||
* @param maxLength The maximium length of the String to be created when the
|
||||
* source of the data is an input stream, or -1 for
|
||||
* unlimited length.
|
||||
* @return a String created from the converted input data.
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public String getString(int maxLength) throws java.io.IOException {
|
||||
String result;
|
||||
if (fInputStream != null) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char[] buffer = new char[1024];
|
||||
Reader reader = getReader();
|
||||
int max = maxLength < 0? Integer.MAX_VALUE : maxLength;
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = reader.read(buffer, 0, Math.min(max, 1024))) >= 0) {
|
||||
sb.append(buffer, 0, bytesRead);
|
||||
max -= bytesRead;
|
||||
}
|
||||
|
||||
reader.close();
|
||||
|
||||
return sb.toString();
|
||||
} else {
|
||||
String name = getName();
|
||||
/*
|
||||
* getName() may return a name with a suffix 'rtl' or 'ltr'. This cannot
|
||||
* be used to open a charset (e.g. IBM424_rtl). The ending '_rtl' or 'ltr'
|
||||
* should be stripped off before creating the string.
|
||||
*/
|
||||
//noinspection IndexOfReplaceableByContains
|
||||
int startSuffix = name.indexOf("_rtl") < 0 ? name.indexOf("_ltr") : name.indexOf("_rtl");
|
||||
if (startSuffix > 0) {
|
||||
name = name.substring(0, startSuffix);
|
||||
}
|
||||
result = new String(fRawInput, name);
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an indication of the confidence in the charset detected.
|
||||
* Confidence values range from 0-100, with larger numbers indicating
|
||||
* a better match of the input data to the characteristics of the
|
||||
* charset.
|
||||
*
|
||||
* @return the confidence in the charset match
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public int getConfidence() {
|
||||
return fConfidence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the detected charset.
|
||||
* The name will be one that can be used with other APIs on the
|
||||
* platform that accept charset names. It is the "Canonical name"
|
||||
* as defined by the class java.nio.charset.Charset; for
|
||||
* charsets that are registered with the IANA charset registry,
|
||||
* this is the MIME-preferred registerd name.
|
||||
*
|
||||
* @see java.nio.charset.Charset
|
||||
* @see java.io.InputStreamReader
|
||||
*
|
||||
* @return The name of the charset.
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public String getName() {
|
||||
return fCharsetName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ISO code for the language of the detected charset.
|
||||
*
|
||||
* @return The ISO code for the language or <code>null</code> if the language cannot be determined.
|
||||
*
|
||||
* @stable ICU 3.4
|
||||
*/
|
||||
public String getLanguage() {
|
||||
return fLang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare to other CharsetMatch objects.
|
||||
* Comparison is based on the match confidence value, which
|
||||
* allows CharsetDetector.detectAll() to order its results.
|
||||
*
|
||||
* @param other the CharsetMatch object to compare against.
|
||||
* @return a negative integer, zero, or a positive integer as the
|
||||
* confidence level of this CharsetMatch
|
||||
* is less than, equal to, or greater than that of
|
||||
* the argument.
|
||||
* @throws ClassCastException if the argument is not a CharsetMatch.
|
||||
* @stable ICU 4.4
|
||||
*/
|
||||
public int compareTo (CharsetMatch other) {
|
||||
int compareResult = 0;
|
||||
if (this.fConfidence > other.fConfidence) {
|
||||
compareResult = 1;
|
||||
} else if (this.fConfidence < other.fConfidence) {
|
||||
compareResult = -1;
|
||||
}
|
||||
return compareResult;
|
||||
}
|
||||
|
||||
/*
|
||||
* Constructor. Implementation internal
|
||||
*/
|
||||
CharsetMatch(CharsetDetector det, CharsetRecognizer rec, int conf) {
|
||||
fConfidence = conf;
|
||||
|
||||
// The references to the original application input data must be copied out
|
||||
// of the charset recognizer to here, in case the application resets the
|
||||
// recognizer before using this CharsetMatch.
|
||||
if (det.fInputStream == null) {
|
||||
// We only want the existing input byte data if it came straight from the user,
|
||||
// not if is just the head of a stream.
|
||||
fRawInput = det.fRawInput;
|
||||
fRawLength = det.fRawLength;
|
||||
}
|
||||
fInputStream = det.fInputStream;
|
||||
fCharsetName = rec.getName();
|
||||
fLang = rec.getLanguage();
|
||||
}
|
||||
|
||||
/*
|
||||
* Constructor. Implementation internal
|
||||
*/
|
||||
CharsetMatch(CharsetDetector det, CharsetRecognizer rec, int conf, String csName, String lang) {
|
||||
fConfidence = conf;
|
||||
|
||||
// The references to the original application input data must be copied out
|
||||
// of the charset recognizer to here, in case the application resets the
|
||||
// recognizer before using this CharsetMatch.
|
||||
if (det.fInputStream == null) {
|
||||
// We only want the existing input byte data if it came straight from the user,
|
||||
// not if is just the head of a stream.
|
||||
fRawInput = det.fRawInput;
|
||||
fRawLength = det.fRawLength;
|
||||
}
|
||||
fInputStream = det.fInputStream;
|
||||
fCharsetName = csName;
|
||||
fLang = lang;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Private Data
|
||||
//
|
||||
private int fConfidence;
|
||||
private byte[] fRawInput = null; // Original, untouched input bytes.
|
||||
// If user gave us a byte array, this is it.
|
||||
private int fRawLength; // Length of data in fRawInput array.
|
||||
|
||||
private InputStream fInputStream; // User's input stream, or null if the user
|
||||
// gave us a byte array.
|
||||
|
||||
private String fCharsetName; // The name of the charset this CharsetMatch
|
||||
// represents. Filled in by the recognizer.
|
||||
private String fLang; // The language, if one was determined by
|
||||
// the recognizer during the detect operation.
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
/*
|
||||
*******************************************************************************
|
||||
* Copyright (C) 2005 - 2012, International Business Machines Corporation and *
|
||||
* others. All Rights Reserved. *
|
||||
*******************************************************************************
|
||||
*/
|
||||
package com.ibm.icu.text;
|
||||
|
||||
/**
|
||||
* class CharsetRecog_2022 part of the ICU charset detection imlementation.
|
||||
* This is a superclass for the individual detectors for
|
||||
* each of the detectable members of the ISO 2022 family
|
||||
* of encodings.
|
||||
*
|
||||
* The separate classes are nested within this class.
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
abstract class CharsetRecog_2022 extends CharsetRecognizer {
|
||||
|
||||
|
||||
/**
|
||||
* Matching function shared among the 2022 detectors JP, CN and KR
|
||||
* Counts up the number of legal an unrecognized escape sequences in
|
||||
* the sample of text, and computes a score based on the total number &
|
||||
* the proportion that fit the encoding.
|
||||
*
|
||||
*
|
||||
* @param text the byte buffer containing text to analyse
|
||||
* @param textLen the size of the text in the byte.
|
||||
* @param escapeSequences the byte escape sequences to test for.
|
||||
* @return match quality, in the range of 0-100.
|
||||
*/
|
||||
int match(byte [] text, int textLen, byte [][] escapeSequences) {
|
||||
int i, j;
|
||||
int escN;
|
||||
int hits = 0;
|
||||
int misses = 0;
|
||||
int shifts = 0;
|
||||
int quality;
|
||||
scanInput:
|
||||
for (i=0; i<textLen; i++) {
|
||||
if (text[i] == 0x1b) {
|
||||
checkEscapes:
|
||||
for (escN=0; escN<escapeSequences.length; escN++) {
|
||||
byte [] seq = escapeSequences[escN];
|
||||
|
||||
if ((textLen - i) < seq.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (j=1; j<seq.length; j++) {
|
||||
if (seq[j] != text[i+j]) {
|
||||
continue checkEscapes;
|
||||
}
|
||||
}
|
||||
|
||||
hits++;
|
||||
i += seq.length-1;
|
||||
continue scanInput;
|
||||
}
|
||||
|
||||
misses++;
|
||||
}
|
||||
|
||||
if (text[i] == 0x0e || text[i] == 0x0f) {
|
||||
// Shift in/out
|
||||
shifts++;
|
||||
}
|
||||
}
|
||||
|
||||
if (hits == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
//
|
||||
// Initial quality is based on relative proportion of recongized vs.
|
||||
// unrecognized escape sequences.
|
||||
// All good: quality = 100;
|
||||
// half or less good: quality = 0;
|
||||
// linear inbetween.
|
||||
quality = (100*hits - 100*misses) / (hits + misses);
|
||||
|
||||
// Back off quality if there were too few escape sequences seen.
|
||||
// Include shifts in this computation, so that KR does not get penalized
|
||||
// for having only a single Escape sequence, but many shifts.
|
||||
if (hits+shifts < 5) {
|
||||
quality -= (5-(hits+shifts))*10;
|
||||
}
|
||||
|
||||
if (quality < 0) {
|
||||
quality = 0;
|
||||
}
|
||||
return quality;
|
||||
}
|
||||
|
||||
static class CharsetRecog_2022JP extends CharsetRecog_2022 {
|
||||
private byte [] [] escapeSequences = {
|
||||
{0x1b, 0x24, 0x28, 0x43}, // KS X 1001:1992
|
||||
{0x1b, 0x24, 0x28, 0x44}, // JIS X 212-1990
|
||||
{0x1b, 0x24, 0x40}, // JIS C 6226-1978
|
||||
{0x1b, 0x24, 0x41}, // GB 2312-80
|
||||
{0x1b, 0x24, 0x42}, // JIS X 208-1983
|
||||
{0x1b, 0x26, 0x40}, // JIS X 208 1990, 1997
|
||||
{0x1b, 0x28, 0x42}, // ASCII
|
||||
{0x1b, 0x28, 0x48}, // JIS-Roman
|
||||
{0x1b, 0x28, 0x49}, // Half-width katakana
|
||||
{0x1b, 0x28, 0x4a}, // JIS-Roman
|
||||
{0x1b, 0x2e, 0x41}, // ISO 8859-1
|
||||
{0x1b, 0x2e, 0x46} // ISO 8859-7
|
||||
};
|
||||
|
||||
String getName() {
|
||||
return "ISO-2022-JP";
|
||||
}
|
||||
|
||||
CharsetMatch match(CharsetDetector det) {
|
||||
int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences);
|
||||
return confidence == 0 ? null : new CharsetMatch(det, this, confidence);
|
||||
}
|
||||
}
|
||||
|
||||
static class CharsetRecog_2022KR extends CharsetRecog_2022 {
|
||||
private byte [] [] escapeSequences = {
|
||||
{0x1b, 0x24, 0x29, 0x43}
|
||||
};
|
||||
|
||||
String getName() {
|
||||
return "ISO-2022-KR";
|
||||
}
|
||||
|
||||
CharsetMatch match(CharsetDetector det) {
|
||||
int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences);
|
||||
return confidence == 0 ? null : new CharsetMatch(det, this, confidence);
|
||||
}
|
||||
}
|
||||
|
||||
static class CharsetRecog_2022CN extends CharsetRecog_2022 {
|
||||
private byte [] [] escapeSequences = {
|
||||
{0x1b, 0x24, 0x29, 0x41}, // GB 2312-80
|
||||
{0x1b, 0x24, 0x29, 0x47}, // CNS 11643-1992 Plane 1
|
||||
{0x1b, 0x24, 0x2A, 0x48}, // CNS 11643-1992 Plane 2
|
||||
{0x1b, 0x24, 0x29, 0x45}, // ISO-IR-165
|
||||
{0x1b, 0x24, 0x2B, 0x49}, // CNS 11643-1992 Plane 3
|
||||
{0x1b, 0x24, 0x2B, 0x4A}, // CNS 11643-1992 Plane 4
|
||||
{0x1b, 0x24, 0x2B, 0x4B}, // CNS 11643-1992 Plane 5
|
||||
{0x1b, 0x24, 0x2B, 0x4C}, // CNS 11643-1992 Plane 6
|
||||
{0x1b, 0x24, 0x2B, 0x4D}, // CNS 11643-1992 Plane 7
|
||||
{0x1b, 0x4e}, // SS2
|
||||
{0x1b, 0x4f}, // SS3
|
||||
};
|
||||
|
||||
String getName() {
|
||||
return "ISO-2022-CN";
|
||||
}
|
||||
|
||||
CharsetMatch match(CharsetDetector det) {
|
||||
int confidence = match(det.fInputBytes, det.fInputLen, escapeSequences);
|
||||
return confidence == 0 ? null : new CharsetMatch(det, this, confidence);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
/*
|
||||
*******************************************************************************
|
||||
* Copyright (C) 2005 - 2012, International Business Machines Corporation and *
|
||||
* others. All Rights Reserved. *
|
||||
*******************************************************************************
|
||||
*/
|
||||
package com.ibm.icu.text;
|
||||
|
||||
/**
|
||||
* Charset recognizer for UTF-8
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
class CharsetRecog_UTF8 extends CharsetRecognizer {
|
||||
|
||||
String getName() {
|
||||
return "UTF-8";
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see com.ibm.icu.text.CharsetRecognizer#match(com.ibm.icu.text.CharsetDetector)
|
||||
*/
|
||||
CharsetMatch match(CharsetDetector det) {
|
||||
boolean hasBOM = false;
|
||||
int numValid = 0;
|
||||
int numInvalid = 0;
|
||||
byte input[] = det.fRawInput;
|
||||
int i;
|
||||
int trailBytes;
|
||||
int confidence;
|
||||
|
||||
if (det.fRawLength >= 3 &&
|
||||
(input[0] & 0xFF) == 0xef && (input[1] & 0xFF) == 0xbb && (input[2] & 0xFF) == 0xbf) {
|
||||
hasBOM = true;
|
||||
}
|
||||
|
||||
// Scan for multi-byte sequences
|
||||
for (i=0; i<det.fRawLength; i++) {
|
||||
int b = input[i];
|
||||
if ((b & 0x80) == 0) {
|
||||
continue; // ASCII
|
||||
}
|
||||
|
||||
// Hi bit on char found. Figure out how long the sequence should be
|
||||
if ((b & 0x0e0) == 0x0c0) {
|
||||
trailBytes = 1;
|
||||
} else if ((b & 0x0f0) == 0x0e0) {
|
||||
trailBytes = 2;
|
||||
} else if ((b & 0x0f8) == 0xf0) {
|
||||
trailBytes = 3;
|
||||
} else {
|
||||
numInvalid++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify that we've got the right number of trail bytes in the sequence
|
||||
for (;;) {
|
||||
i++;
|
||||
if (i>=det.fRawLength) {
|
||||
break;
|
||||
}
|
||||
b = input[i];
|
||||
if ((b & 0xc0) != 0x080) {
|
||||
numInvalid++;
|
||||
break;
|
||||
}
|
||||
if (--trailBytes == 0) {
|
||||
numValid++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cook up some sort of confidence score, based on presense of a BOM
|
||||
// and the existence of valid and/or invalid multi-byte sequences.
|
||||
confidence = 0;
|
||||
if (hasBOM && numInvalid==0) {
|
||||
confidence = 100;
|
||||
} else if (hasBOM && numValid > numInvalid*10) {
|
||||
confidence = 80;
|
||||
} else if (numValid > 3 && numInvalid == 0) {
|
||||
confidence = 100;
|
||||
} else if (numValid > 0 && numInvalid == 0) {
|
||||
confidence = 80;
|
||||
} else if (numValid == 0 && numInvalid == 0) {
|
||||
// Plain ASCII. Confidence must be > 10, it's more likely than UTF-16, which
|
||||
// accepts ASCII with confidence = 10.
|
||||
// TODO: add plain ASCII as an explicitly detected type.
|
||||
confidence = 15;
|
||||
} else if (numValid > numInvalid*10) {
|
||||
// Probably corruput utf-8 data. Valid sequences aren't likely by chance.
|
||||
confidence = 25;
|
||||
}
|
||||
return confidence == 0 ? null : new CharsetMatch(det, this, confidence);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,199 +0,0 @@
|
|||
/*
|
||||
*******************************************************************************
|
||||
* Copyright (C) 1996-2013, International Business Machines Corporation and *
|
||||
* others. All Rights Reserved. *
|
||||
*******************************************************************************
|
||||
*
|
||||
*/
|
||||
|
||||
package com.ibm.icu.text;
|
||||
|
||||
/**
|
||||
* This class matches UTF-16 and UTF-32, both big- and little-endian. The
|
||||
* BOM will be used if it is present.
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
abstract class CharsetRecog_Unicode extends CharsetRecognizer {
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see com.ibm.icu.text.CharsetRecognizer#getName()
|
||||
*/
|
||||
abstract String getName();
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see com.ibm.icu.text.CharsetRecognizer#match(com.ibm.icu.text.CharsetDetector)
|
||||
*/
|
||||
abstract CharsetMatch match(CharsetDetector det);
|
||||
|
||||
static int codeUnit16FromBytes(byte hi, byte lo) {
|
||||
return ((hi & 0xff) << 8) | (lo & 0xff);
|
||||
}
|
||||
|
||||
// UTF-16 confidence calculation. Very simple minded, but better than nothing.
|
||||
// Any 8 bit non-control characters bump the confidence up. These have a zero high byte,
|
||||
// and are very likely to be UTF-16, although they could also be part of a UTF-32 code.
|
||||
// NULs are a contra-indication, they will appear commonly if the actual encoding is UTF-32.
|
||||
// NULs should be rare in actual text.
|
||||
static int adjustConfidence(int codeUnit, int confidence) {
|
||||
if (codeUnit == 0) {
|
||||
confidence -= 10;
|
||||
} else if ((codeUnit >= 0x20 && codeUnit <= 0xff) || codeUnit == 0x0a) {
|
||||
confidence += 10;
|
||||
}
|
||||
if (confidence < 0) {
|
||||
confidence = 0;
|
||||
} else if (confidence > 100) {
|
||||
confidence = 100;
|
||||
}
|
||||
return confidence;
|
||||
}
|
||||
|
||||
static class CharsetRecog_UTF_16_BE extends CharsetRecog_Unicode
|
||||
{
|
||||
String getName()
|
||||
{
|
||||
return "UTF-16BE";
|
||||
}
|
||||
|
||||
CharsetMatch match(CharsetDetector det)
|
||||
{
|
||||
byte[] input = det.fRawInput;
|
||||
int confidence = 10;
|
||||
|
||||
int bytesToCheck = Math.min(input.length, 30);
|
||||
for (int charIndex=0; charIndex<bytesToCheck-1; charIndex+=2) {
|
||||
int codeUnit = codeUnit16FromBytes(input[charIndex], input[charIndex + 1]);
|
||||
if (charIndex == 0 && codeUnit == 0xFEFF) {
|
||||
confidence = 100;
|
||||
break;
|
||||
}
|
||||
confidence = adjustConfidence(codeUnit, confidence);
|
||||
if (confidence == 0 || confidence == 100) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bytesToCheck < 4 && confidence < 100) {
|
||||
confidence = 0;
|
||||
}
|
||||
if (confidence > 0) {
|
||||
return new CharsetMatch(det, this, confidence);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static class CharsetRecog_UTF_16_LE extends CharsetRecog_Unicode
|
||||
{
|
||||
String getName()
|
||||
{
|
||||
return "UTF-16LE";
|
||||
}
|
||||
|
||||
CharsetMatch match(CharsetDetector det)
|
||||
{
|
||||
byte[] input = det.fRawInput;
|
||||
int confidence = 10;
|
||||
|
||||
int bytesToCheck = Math.min(input.length, 30);
|
||||
for (int charIndex=0; charIndex<bytesToCheck-1; charIndex+=2) {
|
||||
int codeUnit = codeUnit16FromBytes(input[charIndex+1], input[charIndex]);
|
||||
if (charIndex == 0 && codeUnit == 0xFEFF) {
|
||||
confidence = 100;
|
||||
break;
|
||||
}
|
||||
confidence = adjustConfidence(codeUnit, confidence);
|
||||
if (confidence == 0 || confidence == 100) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bytesToCheck < 4 && confidence < 100) {
|
||||
confidence = 0;
|
||||
}
|
||||
if (confidence > 0) {
|
||||
return new CharsetMatch(det, this, confidence);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static abstract class CharsetRecog_UTF_32 extends CharsetRecog_Unicode
|
||||
{
|
||||
abstract int getChar(byte[] input, int index);
|
||||
|
||||
abstract String getName();
|
||||
|
||||
CharsetMatch match(CharsetDetector det)
|
||||
{
|
||||
byte[] input = det.fRawInput;
|
||||
int limit = (det.fRawLength / 4) * 4;
|
||||
int numValid = 0;
|
||||
int numInvalid = 0;
|
||||
boolean hasBOM = false;
|
||||
int confidence = 0;
|
||||
|
||||
if (limit==0) {
|
||||
return null;
|
||||
}
|
||||
if (getChar(input, 0) == 0x0000FEFF) {
|
||||
hasBOM = true;
|
||||
}
|
||||
|
||||
for(int i = 0; i < limit; i += 4) {
|
||||
int ch = getChar(input, i);
|
||||
|
||||
if (ch < 0 || ch >= 0x10FFFF || (ch >= 0xD800 && ch <= 0xDFFF)) {
|
||||
numInvalid += 1;
|
||||
} else {
|
||||
numValid += 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Cook up some sort of confidence score, based on presence of a BOM
|
||||
// and the existence of valid and/or invalid multi-byte sequences.
|
||||
if (hasBOM && numInvalid==0) {
|
||||
confidence = 100;
|
||||
} else if (hasBOM && numValid > numInvalid*10) {
|
||||
confidence = 80;
|
||||
} else if (numValid > 3 && numInvalid == 0) {
|
||||
confidence = 100;
|
||||
} else if (numValid > 0 && numInvalid == 0) {
|
||||
confidence = 80;
|
||||
} else if (numValid > numInvalid*10) {
|
||||
// Probably corrupt UTF-32BE data. Valid sequences aren't likely by chance.
|
||||
confidence = 25;
|
||||
}
|
||||
|
||||
return confidence == 0 ? null : new CharsetMatch(det, this, confidence);
|
||||
}
|
||||
}
|
||||
|
||||
static class CharsetRecog_UTF_32_BE extends CharsetRecog_UTF_32
|
||||
{
|
||||
int getChar(byte[] input, int index)
|
||||
{
|
||||
return (input[index] & 0xFF) << 24 | (input[index + 1] & 0xFF) << 16 |
|
||||
(input[index + 2] & 0xFF) << 8 | (input[index + 3] & 0xFF);
|
||||
}
|
||||
|
||||
String getName()
|
||||
{
|
||||
return "UTF-32BE";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class CharsetRecog_UTF_32_LE extends CharsetRecog_UTF_32
|
||||
{
|
||||
int getChar(byte[] input, int index)
|
||||
{
|
||||
return (input[index + 3] & 0xFF) << 24 | (input[index + 2] & 0xFF) << 16 |
|
||||
(input[index + 1] & 0xFF) << 8 | (input[index] & 0xFF);
|
||||
}
|
||||
|
||||
String getName()
|
||||
{
|
||||
return "UTF-32LE";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,544 +0,0 @@
|
|||
/*
|
||||
*******************************************************************************
|
||||
* Copyright (C) 2005 - 2012, International Business Machines Corporation and *
|
||||
* others. All Rights Reserved. *
|
||||
*******************************************************************************
|
||||
*/
|
||||
package com.ibm.icu.text;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* CharsetRecognizer implemenation for Asian - double or multi-byte - charsets.
|
||||
* Match is determined mostly by the input data adhering to the
|
||||
* encoding scheme for the charset, and, optionally,
|
||||
* frequency-of-occurence of characters.
|
||||
* <p/>
|
||||
* Instances of this class are singletons, one per encoding
|
||||
* being recognized. They are created in the main
|
||||
* CharsetDetector class and kept in the global list of available
|
||||
* encodings to be checked. The specific encoding being recognized
|
||||
* is determined by subclass.
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
abstract class CharsetRecog_mbcs extends CharsetRecognizer {
|
||||
|
||||
/**
|
||||
* Get the IANA name of this charset.
|
||||
* @return the charset name.
|
||||
*/
|
||||
abstract String getName() ;
|
||||
|
||||
|
||||
/**
|
||||
* Test the match of this charset with the input text data
|
||||
* which is obtained via the CharsetDetector object.
|
||||
*
|
||||
* @param det The CharsetDetector, which contains the input text
|
||||
* to be checked for being in this charset.
|
||||
* @return Two values packed into one int (Damn java, anyhow)
|
||||
* <br/>
|
||||
* bits 0-7: the match confidence, ranging from 0-100
|
||||
* <br/>
|
||||
* bits 8-15: The match reason, an enum-like value.
|
||||
*/
|
||||
int match(CharsetDetector det, int [] commonChars) {
|
||||
@SuppressWarnings("unused")
|
||||
int singleByteCharCount = 0; //TODO Do we really need this?
|
||||
int doubleByteCharCount = 0;
|
||||
int commonCharCount = 0;
|
||||
int badCharCount = 0;
|
||||
int totalCharCount = 0;
|
||||
int confidence = 0;
|
||||
iteratedChar iter = new iteratedChar();
|
||||
|
||||
detectBlock: {
|
||||
for (iter.reset(); nextChar(iter, det);) {
|
||||
totalCharCount++;
|
||||
if (iter.error) {
|
||||
badCharCount++;
|
||||
} else {
|
||||
long cv = iter.charValue & 0xFFFFFFFFL;
|
||||
|
||||
if (cv <= 0xff) {
|
||||
singleByteCharCount++;
|
||||
} else {
|
||||
doubleByteCharCount++;
|
||||
if (commonChars != null) {
|
||||
// NOTE: This assumes that there are no 4-byte common chars.
|
||||
if (Arrays.binarySearch(commonChars, (int) cv) >= 0) {
|
||||
commonCharCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (badCharCount >= 2 && badCharCount*5 >= doubleByteCharCount) {
|
||||
// Bail out early if the byte data is not matching the encoding scheme.
|
||||
break detectBlock;
|
||||
}
|
||||
}
|
||||
|
||||
if (doubleByteCharCount <= 10 && badCharCount== 0) {
|
||||
// Not many multi-byte chars.
|
||||
if (doubleByteCharCount == 0 && totalCharCount < 10) {
|
||||
// There weren't any multibyte sequences, and there was a low density of non-ASCII single bytes.
|
||||
// We don't have enough data to have any confidence.
|
||||
// Statistical analysis of single byte non-ASCII charcters would probably help here.
|
||||
confidence = 0;
|
||||
}
|
||||
else {
|
||||
// ASCII or ISO file? It's probably not our encoding,
|
||||
// but is not incompatible with our encoding, so don't give it a zero.
|
||||
confidence = 10;
|
||||
}
|
||||
|
||||
break detectBlock;
|
||||
}
|
||||
|
||||
//
|
||||
// No match if there are too many characters that don't fit the encoding scheme.
|
||||
// (should we have zero tolerance for these?)
|
||||
//
|
||||
if (doubleByteCharCount < 20*badCharCount) {
|
||||
confidence = 0;
|
||||
break detectBlock;
|
||||
}
|
||||
|
||||
if (commonChars == null) {
|
||||
// We have no statistics on frequently occuring characters.
|
||||
// Assess confidence purely on having a reasonable number of
|
||||
// multi-byte characters (the more the better
|
||||
confidence = 30 + doubleByteCharCount - 20*badCharCount;
|
||||
if (confidence > 100) {
|
||||
confidence = 100;
|
||||
}
|
||||
}else {
|
||||
//
|
||||
// Frequency of occurence statistics exist.
|
||||
//
|
||||
double maxVal = Math.log((float)doubleByteCharCount / 4);
|
||||
double scaleFactor = 90.0 / maxVal;
|
||||
confidence = (int)(Math.log(commonCharCount+1) * scaleFactor + 10);
|
||||
confidence = Math.min(confidence, 100);
|
||||
}
|
||||
} // end of detectBlock:
|
||||
|
||||
return confidence;
|
||||
}
|
||||
|
||||
// "Character" iterated character class.
|
||||
// Recognizers for specific mbcs encodings make their "characters" available
|
||||
// by providing a nextChar() function that fills in an instance of iteratedChar
|
||||
// with the next char from the input.
|
||||
// The returned characters are not converted to Unicode, but remain as the raw
|
||||
// bytes (concatenated into an int) from the codepage data.
|
||||
//
|
||||
// For Asian charsets, use the raw input rather than the input that has been
|
||||
// stripped of markup. Detection only considers multi-byte chars, effectively
|
||||
// stripping markup anyway, and double byte chars do occur in markup too.
|
||||
//
|
||||
static class iteratedChar {
|
||||
int charValue = 0; // 1-4 bytes from the raw input data
|
||||
int index = 0;
|
||||
int nextIndex = 0;
|
||||
boolean error = false;
|
||||
boolean done = false;
|
||||
|
||||
void reset() {
|
||||
charValue = 0;
|
||||
index = -1;
|
||||
nextIndex = 0;
|
||||
error = false;
|
||||
done = false;
|
||||
}
|
||||
|
||||
int nextByte(CharsetDetector det) {
|
||||
if (nextIndex >= det.fRawLength) {
|
||||
done = true;
|
||||
return -1;
|
||||
}
|
||||
return (int)det.fRawInput[nextIndex++] & 0x00ff;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next character (however many bytes it is) from the input data
|
||||
* Subclasses for specific charset encodings must implement this function
|
||||
* to get characters according to the rules of their encoding scheme.
|
||||
*
|
||||
* This function is not a method of class iteratedChar only because
|
||||
* that would require a lot of extra derived classes, which is awkward.
|
||||
* @param it The iteratedChar "struct" into which the returned char is placed.
|
||||
* @param det The charset detector, which is needed to get at the input byte data
|
||||
* being iterated over.
|
||||
* @return True if a character was returned, false at end of input.
|
||||
*/
|
||||
abstract boolean nextChar(iteratedChar it, CharsetDetector det);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Shift-JIS charset recognizer.
|
||||
*
|
||||
*/
|
||||
static class CharsetRecog_sjis extends CharsetRecog_mbcs {
|
||||
static int [] commonChars =
|
||||
// TODO: This set of data comes from the character frequency-
|
||||
// of-occurence analysis tool. The data needs to be moved
|
||||
// into a resource and loaded from there.
|
||||
{0x8140, 0x8141, 0x8142, 0x8145, 0x815b, 0x8169, 0x816a, 0x8175, 0x8176, 0x82a0,
|
||||
0x82a2, 0x82a4, 0x82a9, 0x82aa, 0x82ab, 0x82ad, 0x82af, 0x82b1, 0x82b3, 0x82b5,
|
||||
0x82b7, 0x82bd, 0x82be, 0x82c1, 0x82c4, 0x82c5, 0x82c6, 0x82c8, 0x82c9, 0x82cc,
|
||||
0x82cd, 0x82dc, 0x82e0, 0x82e7, 0x82e8, 0x82e9, 0x82ea, 0x82f0, 0x82f1, 0x8341,
|
||||
0x8343, 0x834e, 0x834f, 0x8358, 0x835e, 0x8362, 0x8367, 0x8375, 0x8376, 0x8389,
|
||||
0x838a, 0x838b, 0x838d, 0x8393, 0x8e96, 0x93fa, 0x95aa};
|
||||
|
||||
boolean nextChar(iteratedChar it, CharsetDetector det) {
|
||||
it.index = it.nextIndex;
|
||||
it.error = false;
|
||||
int firstByte;
|
||||
firstByte = it.charValue = it.nextByte(det);
|
||||
if (firstByte < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firstByte <= 0x7f || (firstByte>0xa0 && firstByte<=0xdf)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int secondByte = it.nextByte(det);
|
||||
if (secondByte < 0) {
|
||||
return false;
|
||||
}
|
||||
it.charValue = (firstByte << 8) | secondByte;
|
||||
if (! ((secondByte>=0x40 && secondByte<=0x7f) || (secondByte>=0x80 && secondByte<=0xff))) {
|
||||
// Illegal second byte value.
|
||||
it.error = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
CharsetMatch match(CharsetDetector det) {
|
||||
int confidence = match(det, commonChars);
|
||||
return confidence == 0 ? null : new CharsetMatch(det, this, confidence);
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return "Shift_JIS";
|
||||
}
|
||||
|
||||
public String getLanguage()
|
||||
{
|
||||
return "ja";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Big5 charset recognizer.
|
||||
*
|
||||
*/
|
||||
static class CharsetRecog_big5 extends CharsetRecog_mbcs {
|
||||
static int [] commonChars =
|
||||
// TODO: This set of data comes from the character frequency-
|
||||
// of-occurence analysis tool. The data needs to be moved
|
||||
// into a resource and loaded from there.
|
||||
{0xa140, 0xa141, 0xa142, 0xa143, 0xa147, 0xa149, 0xa175, 0xa176, 0xa440, 0xa446,
|
||||
0xa447, 0xa448, 0xa451, 0xa454, 0xa457, 0xa464, 0xa46a, 0xa46c, 0xa477, 0xa4a3,
|
||||
0xa4a4, 0xa4a7, 0xa4c1, 0xa4ce, 0xa4d1, 0xa4df, 0xa4e8, 0xa4fd, 0xa540, 0xa548,
|
||||
0xa558, 0xa569, 0xa5cd, 0xa5e7, 0xa657, 0xa661, 0xa662, 0xa668, 0xa670, 0xa6a8,
|
||||
0xa6b3, 0xa6b9, 0xa6d3, 0xa6db, 0xa6e6, 0xa6f2, 0xa740, 0xa751, 0xa759, 0xa7da,
|
||||
0xa8a3, 0xa8a5, 0xa8ad, 0xa8d1, 0xa8d3, 0xa8e4, 0xa8fc, 0xa9c0, 0xa9d2, 0xa9f3,
|
||||
0xaa6b, 0xaaba, 0xaabe, 0xaacc, 0xaafc, 0xac47, 0xac4f, 0xacb0, 0xacd2, 0xad59,
|
||||
0xaec9, 0xafe0, 0xb0ea, 0xb16f, 0xb2b3, 0xb2c4, 0xb36f, 0xb44c, 0xb44e, 0xb54c,
|
||||
0xb5a5, 0xb5bd, 0xb5d0, 0xb5d8, 0xb671, 0xb7ed, 0xb867, 0xb944, 0xbad8, 0xbb44,
|
||||
0xbba1, 0xbdd1, 0xc2c4, 0xc3b9, 0xc440, 0xc45f};
|
||||
|
||||
boolean nextChar(iteratedChar it, CharsetDetector det) {
|
||||
it.index = it.nextIndex;
|
||||
it.error = false;
|
||||
int firstByte;
|
||||
firstByte = it.charValue = it.nextByte(det);
|
||||
if (firstByte < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (firstByte <= 0x7f || firstByte==0xff) {
|
||||
// single byte character.
|
||||
return true;
|
||||
}
|
||||
|
||||
int secondByte = it.nextByte(det);
|
||||
if (secondByte < 0) {
|
||||
return false;
|
||||
}
|
||||
it.charValue = (it.charValue << 8) | secondByte;
|
||||
|
||||
if (secondByte < 0x40 ||
|
||||
secondByte ==0x7f ||
|
||||
secondByte == 0xff) {
|
||||
it.error = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
CharsetMatch match(CharsetDetector det) {
|
||||
int confidence = match(det, commonChars);
|
||||
return confidence == 0 ? null : new CharsetMatch(det, this, confidence);
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return "Big5";
|
||||
}
|
||||
|
||||
|
||||
public String getLanguage()
|
||||
{
|
||||
return "zh";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* EUC charset recognizers. One abstract class that provides the common function
|
||||
* for getting the next character according to the EUC encoding scheme,
|
||||
* and nested derived classes for EUC_KR, EUC_JP, EUC_CN.
|
||||
*
|
||||
*/
|
||||
abstract static class CharsetRecog_euc extends CharsetRecog_mbcs {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* Get the next character value for EUC based encodings.
|
||||
* Character "value" is simply the raw bytes that make up the character
|
||||
* packed into an int.
|
||||
*/
|
||||
boolean nextChar(iteratedChar it, CharsetDetector det) {
|
||||
it.index = it.nextIndex;
|
||||
it.error = false;
|
||||
int firstByte;
|
||||
int secondByte;
|
||||
int thirdByte;
|
||||
//int fourthByte = 0;
|
||||
|
||||
buildChar: {
|
||||
firstByte = it.charValue = it.nextByte(det);
|
||||
if (firstByte < 0) {
|
||||
// Ran off the end of the input data
|
||||
it.done = true;
|
||||
break buildChar;
|
||||
}
|
||||
if (firstByte <= 0x8d) {
|
||||
// single byte char
|
||||
break buildChar;
|
||||
}
|
||||
|
||||
secondByte = it.nextByte(det);
|
||||
it.charValue = (it.charValue << 8) | secondByte;
|
||||
|
||||
if (firstByte >= 0xA1 && firstByte <= 0xfe) {
|
||||
// Two byte Char
|
||||
if (secondByte < 0xa1) {
|
||||
it.error = true;
|
||||
}
|
||||
break buildChar;
|
||||
}
|
||||
if (firstByte == 0x8e) {
|
||||
// Code Set 2.
|
||||
// In EUC-JP, total char size is 2 bytes, only one byte of actual char value.
|
||||
// In EUC-TW, total char size is 4 bytes, three bytes contribute to char value.
|
||||
// We don't know which we've got.
|
||||
// Treat it like EUC-JP. If the data really was EUC-TW, the following two
|
||||
// bytes will look like a well formed 2 byte char.
|
||||
if (secondByte < 0xa1) {
|
||||
it.error = true;
|
||||
}
|
||||
break buildChar;
|
||||
}
|
||||
|
||||
if (firstByte == 0x8f) {
|
||||
// Code set 3.
|
||||
// Three byte total char size, two bytes of actual char value.
|
||||
thirdByte = it.nextByte(det);
|
||||
it.charValue = (it.charValue << 8) | thirdByte;
|
||||
if (thirdByte < 0xa1) {
|
||||
it.error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (!it.done);
|
||||
}
|
||||
|
||||
/**
|
||||
* The charset recognize for EUC-JP. A singleton instance of this class
|
||||
* is created and kept by the public CharsetDetector class
|
||||
*/
|
||||
static class CharsetRecog_euc_jp extends CharsetRecog_euc {
|
||||
static int [] commonChars =
|
||||
// TODO: This set of data comes from the character frequency-
|
||||
// of-occurence analysis tool. The data needs to be moved
|
||||
// into a resource and loaded from there.
|
||||
{0xa1a1, 0xa1a2, 0xa1a3, 0xa1a6, 0xa1bc, 0xa1ca, 0xa1cb, 0xa1d6, 0xa1d7, 0xa4a2,
|
||||
0xa4a4, 0xa4a6, 0xa4a8, 0xa4aa, 0xa4ab, 0xa4ac, 0xa4ad, 0xa4af, 0xa4b1, 0xa4b3,
|
||||
0xa4b5, 0xa4b7, 0xa4b9, 0xa4bb, 0xa4bd, 0xa4bf, 0xa4c0, 0xa4c1, 0xa4c3, 0xa4c4,
|
||||
0xa4c6, 0xa4c7, 0xa4c8, 0xa4c9, 0xa4ca, 0xa4cb, 0xa4ce, 0xa4cf, 0xa4d0, 0xa4de,
|
||||
0xa4df, 0xa4e1, 0xa4e2, 0xa4e4, 0xa4e8, 0xa4e9, 0xa4ea, 0xa4eb, 0xa4ec, 0xa4ef,
|
||||
0xa4f2, 0xa4f3, 0xa5a2, 0xa5a3, 0xa5a4, 0xa5a6, 0xa5a7, 0xa5aa, 0xa5ad, 0xa5af,
|
||||
0xa5b0, 0xa5b3, 0xa5b5, 0xa5b7, 0xa5b8, 0xa5b9, 0xa5bf, 0xa5c3, 0xa5c6, 0xa5c7,
|
||||
0xa5c8, 0xa5c9, 0xa5cb, 0xa5d0, 0xa5d5, 0xa5d6, 0xa5d7, 0xa5de, 0xa5e0, 0xa5e1,
|
||||
0xa5e5, 0xa5e9, 0xa5ea, 0xa5eb, 0xa5ec, 0xa5ed, 0xa5f3, 0xb8a9, 0xb9d4, 0xbaee,
|
||||
0xbbc8, 0xbef0, 0xbfb7, 0xc4ea, 0xc6fc, 0xc7bd, 0xcab8, 0xcaf3, 0xcbdc, 0xcdd1};
|
||||
String getName() {
|
||||
return "EUC-JP";
|
||||
}
|
||||
|
||||
CharsetMatch match(CharsetDetector det) {
|
||||
int confidence = match(det, commonChars);
|
||||
return confidence == 0 ? null : new CharsetMatch(det, this, confidence);
|
||||
}
|
||||
|
||||
public String getLanguage()
|
||||
{
|
||||
return "ja";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The charset recognize for EUC-KR. A singleton instance of this class
|
||||
* is created and kept by the public CharsetDetector class
|
||||
*/
|
||||
static class CharsetRecog_euc_kr extends CharsetRecog_euc {
|
||||
static int [] commonChars =
|
||||
// TODO: This set of data comes from the character frequency-
|
||||
// of-occurence analysis tool. The data needs to be moved
|
||||
// into a resource and loaded from there.
|
||||
{0xb0a1, 0xb0b3, 0xb0c5, 0xb0cd, 0xb0d4, 0xb0e6, 0xb0ed, 0xb0f8, 0xb0fa, 0xb0fc,
|
||||
0xb1b8, 0xb1b9, 0xb1c7, 0xb1d7, 0xb1e2, 0xb3aa, 0xb3bb, 0xb4c2, 0xb4cf, 0xb4d9,
|
||||
0xb4eb, 0xb5a5, 0xb5b5, 0xb5bf, 0xb5c7, 0xb5e9, 0xb6f3, 0xb7af, 0xb7c2, 0xb7ce,
|
||||
0xb8a6, 0xb8ae, 0xb8b6, 0xb8b8, 0xb8bb, 0xb8e9, 0xb9ab, 0xb9ae, 0xb9cc, 0xb9ce,
|
||||
0xb9fd, 0xbab8, 0xbace, 0xbad0, 0xbaf1, 0xbbe7, 0xbbf3, 0xbbfd, 0xbcad, 0xbcba,
|
||||
0xbcd2, 0xbcf6, 0xbdba, 0xbdc0, 0xbdc3, 0xbdc5, 0xbec6, 0xbec8, 0xbedf, 0xbeee,
|
||||
0xbef8, 0xbefa, 0xbfa1, 0xbfa9, 0xbfc0, 0xbfe4, 0xbfeb, 0xbfec, 0xbff8, 0xc0a7,
|
||||
0xc0af, 0xc0b8, 0xc0ba, 0xc0bb, 0xc0bd, 0xc0c7, 0xc0cc, 0xc0ce, 0xc0cf, 0xc0d6,
|
||||
0xc0da, 0xc0e5, 0xc0fb, 0xc0fc, 0xc1a4, 0xc1a6, 0xc1b6, 0xc1d6, 0xc1df, 0xc1f6,
|
||||
0xc1f8, 0xc4a1, 0xc5cd, 0xc6ae, 0xc7cf, 0xc7d1, 0xc7d2, 0xc7d8, 0xc7e5, 0xc8ad};
|
||||
|
||||
String getName() {
|
||||
return "EUC-KR";
|
||||
}
|
||||
|
||||
CharsetMatch match(CharsetDetector det) {
|
||||
int confidence = match(det, commonChars);
|
||||
return confidence == 0 ? null : new CharsetMatch(det, this, confidence);
|
||||
}
|
||||
|
||||
public String getLanguage()
|
||||
{
|
||||
return "ko";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* GB-18030 recognizer. Uses simplified Chinese statistics.
|
||||
*
|
||||
*/
|
||||
static class CharsetRecog_gb_18030 extends CharsetRecog_mbcs {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* Get the next character value for EUC based encodings.
|
||||
* Character "value" is simply the raw bytes that make up the character
|
||||
* packed into an int.
|
||||
*/
|
||||
boolean nextChar(iteratedChar it, CharsetDetector det) {
|
||||
it.index = it.nextIndex;
|
||||
it.error = false;
|
||||
int firstByte;
|
||||
int secondByte;
|
||||
int thirdByte;
|
||||
int fourthByte;
|
||||
|
||||
buildChar: {
|
||||
firstByte = it.charValue = it.nextByte(det);
|
||||
|
||||
if (firstByte < 0) {
|
||||
// Ran off the end of the input data
|
||||
it.done = true;
|
||||
break buildChar;
|
||||
}
|
||||
|
||||
if (firstByte <= 0x80) {
|
||||
// single byte char
|
||||
break buildChar;
|
||||
}
|
||||
|
||||
secondByte = it.nextByte(det);
|
||||
it.charValue = (it.charValue << 8) | secondByte;
|
||||
|
||||
if (firstByte <= 0xFE) {
|
||||
// Two byte Char
|
||||
if ((secondByte >= 0x40 && secondByte <= 0x7E) || (secondByte >=80 && secondByte <=0xFE)) {
|
||||
break buildChar;
|
||||
}
|
||||
|
||||
// Four byte char
|
||||
if (secondByte >= 0x30 && secondByte <= 0x39) {
|
||||
thirdByte = it.nextByte(det);
|
||||
|
||||
if (thirdByte >= 0x81 && thirdByte <= 0xFE) {
|
||||
fourthByte = it.nextByte(det);
|
||||
|
||||
if (fourthByte >= 0x30 && fourthByte <= 0x39) {
|
||||
it.charValue = (it.charValue << 16) | (thirdByte << 8) | fourthByte;
|
||||
break buildChar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it.error = true;
|
||||
}
|
||||
}
|
||||
|
||||
return !it.done;
|
||||
}
|
||||
|
||||
static int [] commonChars =
|
||||
// TODO: This set of data comes from the character frequency-
|
||||
// of-occurence analysis tool. The data needs to be moved
|
||||
// into a resource and loaded from there.
|
||||
{0xa1a1, 0xa1a2, 0xa1a3, 0xa1a4, 0xa1b0, 0xa1b1, 0xa1f1, 0xa1f3, 0xa3a1, 0xa3ac,
|
||||
0xa3ba, 0xb1a8, 0xb1b8, 0xb1be, 0xb2bb, 0xb3c9, 0xb3f6, 0xb4f3, 0xb5bd, 0xb5c4,
|
||||
0xb5e3, 0xb6af, 0xb6d4, 0xb6e0, 0xb7a2, 0xb7a8, 0xb7bd, 0xb7d6, 0xb7dd, 0xb8b4,
|
||||
0xb8df, 0xb8f6, 0xb9ab, 0xb9c9, 0xb9d8, 0xb9fa, 0xb9fd, 0xbacd, 0xbba7, 0xbbd6,
|
||||
0xbbe1, 0xbbfa, 0xbcbc, 0xbcdb, 0xbcfe, 0xbdcc, 0xbecd, 0xbedd, 0xbfb4, 0xbfc6,
|
||||
0xbfc9, 0xc0b4, 0xc0ed, 0xc1cb, 0xc2db, 0xc3c7, 0xc4dc, 0xc4ea, 0xc5cc, 0xc6f7,
|
||||
0xc7f8, 0xc8ab, 0xc8cb, 0xc8d5, 0xc8e7, 0xc9cf, 0xc9fa, 0xcab1, 0xcab5, 0xcac7,
|
||||
0xcad0, 0xcad6, 0xcaf5, 0xcafd, 0xccec, 0xcdf8, 0xceaa, 0xcec4, 0xced2, 0xcee5,
|
||||
0xcfb5, 0xcfc2, 0xcfd6, 0xd0c2, 0xd0c5, 0xd0d0, 0xd0d4, 0xd1a7, 0xd2aa, 0xd2b2,
|
||||
0xd2b5, 0xd2bb, 0xd2d4, 0xd3c3, 0xd3d0, 0xd3fd, 0xd4c2, 0xd4da, 0xd5e2, 0xd6d0};
|
||||
|
||||
|
||||
String getName() {
|
||||
return "GB18030";
|
||||
}
|
||||
|
||||
CharsetMatch match(CharsetDetector det) {
|
||||
int confidence = match(det, commonChars);
|
||||
return confidence == 0 ? null : new CharsetMatch(det, this, confidence);
|
||||
}
|
||||
|
||||
public String getLanguage()
|
||||
{
|
||||
return "zh";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
*******************************************************************************
|
||||
* Copyright (C) 2005 - 2012, International Business Machines Corporation and *
|
||||
* others. All Rights Reserved. *
|
||||
*******************************************************************************
|
||||
*/
|
||||
package com.ibm.icu.text;
|
||||
|
||||
/**
|
||||
* Abstract class for recognizing a single charset.
|
||||
* Part of the implementation of ICU's CharsetDetector.
|
||||
*
|
||||
* Each specific charset that can be recognized will have an instance
|
||||
* of some subclass of this class. All interaction between the overall
|
||||
* CharsetDetector and the stuff specific to an individual charset happens
|
||||
* via the interface provided here.
|
||||
*
|
||||
* Instances of CharsetDetector DO NOT have or maintain
|
||||
* state pertaining to a specific match or detect operation.
|
||||
* The WILL be shared by multiple instances of CharsetDetector.
|
||||
* They encapsulate const charset-specific information.
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
abstract class CharsetRecognizer {
|
||||
/**
|
||||
* Get the IANA name of this charset.
|
||||
* @return the charset name.
|
||||
*/
|
||||
abstract String getName();
|
||||
|
||||
/**
|
||||
* Get the ISO language code for this charset.
|
||||
* @return the language code, or <code>null</code> if the language cannot be determined.
|
||||
*/
|
||||
public String getLanguage()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the match of this charset with the input text data
|
||||
* which is obtained via the CharsetDetector object.
|
||||
*
|
||||
* @param det The CharsetDetector, which contains the input text
|
||||
* to be checked for being in this charset.
|
||||
* @return A CharsetMatch object containing details of match
|
||||
* with this charset, or null if there was no match.
|
||||
*/
|
||||
abstract CharsetMatch match(CharsetDetector det);
|
||||
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
package org.ligi.passandroid
|
||||
|
||||
import android.app.Application
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.jakewharton.threetenabp.AndroidThreeTen
|
||||
import com.squareup.moshi.Moshi
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
import org.ligi.passandroid.json_adapter.ColorAdapter
|
||||
import org.ligi.passandroid.json_adapter.ZonedTimeAdapter
|
||||
import org.ligi.passandroid.model.AndroidFileSystemPassStore
|
||||
import org.ligi.passandroid.model.AndroidSettings
|
||||
import org.ligi.passandroid.model.PassStore
|
||||
import org.ligi.passandroid.model.Settings
|
||||
import org.ligi.passandroid.scan.events.PassScanEventChannelProvider
|
||||
import org.ligi.tracedroid.TraceDroid
|
||||
import org.ligi.tracedroid.logging.Log
|
||||
|
||||
open class App : Application() {
|
||||
|
||||
private val moshi = Moshi.Builder()
|
||||
.add(ZonedTimeAdapter())
|
||||
.add(ColorAdapter())
|
||||
.build()
|
||||
|
||||
private val settings by lazy { AndroidSettings(this) }
|
||||
|
||||
open fun createKoin(): Module {
|
||||
|
||||
return module {
|
||||
single { AndroidFileSystemPassStore(this@App, get(), moshi) as PassStore }
|
||||
single { settings as Settings }
|
||||
single { createTracker(this@App) }
|
||||
single { PassScanEventChannelProvider() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
startKoin {
|
||||
if (BuildConfig.DEBUG) androidLogger()
|
||||
androidContext(this@App)
|
||||
modules(createKoin())
|
||||
}
|
||||
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
AndroidThreeTen.init(this)
|
||||
initTraceDroid()
|
||||
|
||||
AppCompatDelegate.setDefaultNightMode(settings.getNightMode())
|
||||
}
|
||||
|
||||
|
||||
private fun initTraceDroid() {
|
||||
TraceDroid.init(this)
|
||||
Log.setTAG("PassAndroid")
|
||||
}
|
||||
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package org.ligi.passandroid;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import org.ligi.passandroid.ui.PassImportActivity;
|
||||
|
||||
public class InstallListener extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String rawReferrerString = intent.getStringExtra("referrer");
|
||||
if (rawReferrerString != null) {
|
||||
|
||||
final Intent newIntent = new Intent(context, PassImportActivity.class);
|
||||
newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
newIntent.setData(Uri.parse(rawReferrerString));
|
||||
|
||||
context.startActivity(newIntent);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package org.ligi.passandroid;
|
||||
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public interface Tracker {
|
||||
void trackException(String s, Throwable e, boolean fatal);
|
||||
|
||||
void trackException(String s, boolean fatal);
|
||||
|
||||
void trackEvent(@Nullable String category, @Nullable String action, @Nullable String label, @Nullable Long val);
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package org.ligi.passandroid.functions
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.provider.CalendarContract
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import android.view.View
|
||||
import org.ligi.passandroid.R
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
|
||||
const val DEFAULT_EVENT_LENGTH_IN_HOURS = 8L
|
||||
|
||||
fun tryAddDateToCalendar(pass: Pass, contextView: View, timeSpan: PassImpl.TimeSpan) {
|
||||
if (pass.calendarTimespan == null) {
|
||||
AlertDialog.Builder(contextView.context).setMessage(R.string.expiration_date_to_calendar_warning_message)
|
||||
.setTitle(R.string.expiration_date_to_calendar_warning_title)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> reallyAddToCalendar(pass, contextView, timeSpan) }
|
||||
.show()
|
||||
} else {
|
||||
reallyAddToCalendar(pass, contextView, timeSpan)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reallyAddToCalendar(pass: Pass, contextView: View, timeSpan: PassImpl.TimeSpan) = try {
|
||||
|
||||
val intent = createIntent(pass, timeSpan)
|
||||
contextView.context.startActivity(intent)
|
||||
|
||||
} catch (exception: ActivityNotFoundException) {
|
||||
// TODO maybe action to install calendar app
|
||||
Snackbar.make(contextView, R.string.no_calendar_app_found, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
|
||||
@VisibleForTesting
|
||||
fun createIntent(pass: Pass, timeSpan: PassImpl.TimeSpan) = Intent(Intent.ACTION_EDIT).apply {
|
||||
if (timeSpan.from == null && timeSpan.to == null) {
|
||||
throw IllegalArgumentException("span must have either a to or a from")
|
||||
}
|
||||
|
||||
type = "vnd.android.cursor.item/event"
|
||||
val from = timeSpan.from ?: timeSpan.to!!.minusHours(DEFAULT_EVENT_LENGTH_IN_HOURS)
|
||||
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, from.toEpochSecond() * 1000)
|
||||
|
||||
val to = timeSpan.to ?: timeSpan.from!!.plusHours(DEFAULT_EVENT_LENGTH_IN_HOURS)
|
||||
putExtra(CalendarContract.EXTRA_EVENT_END_TIME, to.toEpochSecond() * 1000)
|
||||
putExtra("title", pass.description)
|
||||
|
||||
|
||||
pass.locations.firstOrNull()?.name?.let {
|
||||
putExtra("eventLocation", it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package org.ligi.passandroid.functions
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
import org.ligi.passandroid.model.pass.PassBarCodeFormat
|
||||
import org.ligi.tracedroid.logging.Log
|
||||
|
||||
|
||||
fun generateBitmapDrawable(resources: Resources, data: String, type: PassBarCodeFormat): BitmapDrawable? {
|
||||
val bitmap = generateBarCodeBitmap(data, type) ?: return null
|
||||
|
||||
return BitmapDrawable(resources, bitmap).apply {
|
||||
isFilterBitmap = false
|
||||
setAntiAlias(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun generateBarCodeBitmap(data: String, type: PassBarCodeFormat): Bitmap? {
|
||||
|
||||
if (data.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
val matrix = getBitMatrix(data, type)
|
||||
val is1D = matrix.height == 1
|
||||
|
||||
// generate an image from the byte matrix
|
||||
val width = matrix.width
|
||||
val height = if (is1D) width / 5 else matrix.height
|
||||
|
||||
// create buffered image to draw to
|
||||
// NTFS Bitmap.Config.ALPHA_8 sounds like an awesome idea - been there - done that ..
|
||||
val barcodeImage = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
|
||||
|
||||
// iterate through the matrix and draw the pixels to the image
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
barcodeImage.setPixel(x, y, if (matrix.get(x, if (is1D) 0 else y)) 0 else 0xFFFFFF)
|
||||
}
|
||||
}
|
||||
|
||||
return barcodeImage
|
||||
} catch (e: com.google.zxing.WriterException) {
|
||||
Log.w("could not write image: $e")
|
||||
// TODO check if we should better return some rescue Image here
|
||||
return null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.w("could not write image: $e")
|
||||
return null
|
||||
} catch (e: ArrayIndexOutOfBoundsException) {
|
||||
// happens for ITF barcode on certain inputs
|
||||
Log.w("could not write image: $e")
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun getBitMatrix(data: String, type: PassBarCodeFormat)
|
||||
= MultiFormatWriter().encode(data, type.zxingBarCodeFormat(), 0, 0)!!
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package org.ligi.passandroid.functions
|
||||
|
||||
import android.graphics.Color
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import org.ligi.passandroid.R
|
||||
import org.ligi.passandroid.model.pass.PassType
|
||||
|
||||
@StringRes
|
||||
fun getHumanCategoryString(fromPass: PassType) = when (fromPass) {
|
||||
PassType.BOARDING -> R.string.boarding_pass
|
||||
PassType.EVENT -> R.string.category_event
|
||||
PassType.COUPON -> R.string.category_coupon
|
||||
PassType.LOYALTY -> R.string.category_storecard
|
||||
PassType.GENERIC -> R.string.category_generic
|
||||
PassType.VOUCHER -> R.string.categories_voucher
|
||||
|
||||
else -> R.string.category_none
|
||||
}
|
||||
|
||||
|
||||
@ColorInt
|
||||
fun getCategoryDefaultBG(category: PassType) = when (category) {
|
||||
PassType.BOARDING -> 0xFF3d73e9
|
||||
PassType.EVENT -> 0xFF9f3dd0
|
||||
PassType.COUPON -> 0xFF9ccb05
|
||||
PassType.LOYALTY -> 0xFFf29b21
|
||||
PassType.VOUCHER -> 0xFF2A2727
|
||||
PassType.GENERIC -> 0xFFea3c48
|
||||
|
||||
else -> Color.WHITE.toLong()
|
||||
}.toInt()
|
||||
|
||||
|
||||
@DrawableRes
|
||||
fun getCategoryTopImageRes(type: PassType) = when (type) {
|
||||
PassType.BOARDING -> R.drawable.cat_bp
|
||||
PassType.EVENT -> R.drawable.cat_et
|
||||
PassType.COUPON -> R.drawable.cat_cp
|
||||
PassType.LOYALTY -> R.drawable.cat_sc
|
||||
PassType.VOUCHER -> R.drawable.cat_ps
|
||||
PassType.GENERIC -> R.drawable.cat_none
|
||||
|
||||
else -> R.drawable.cat_none
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
package org.ligi.passandroid.functions
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.ligi.passandroid.Tracker
|
||||
import org.ligi.passandroid.model.InputStreamWithSource
|
||||
import java.io.BufferedInputStream
|
||||
import java.net.URL
|
||||
|
||||
const val IPHONE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53"
|
||||
|
||||
fun fromURI(context: Context, uri: Uri, tracker: Tracker): InputStreamWithSource? {
|
||||
tracker.trackEvent("protocol", "to_inputstream", uri.scheme, null)
|
||||
return when (uri.scheme) {
|
||||
"content" -> fromContent(context, uri)
|
||||
|
||||
"http", "https" ->
|
||||
// TODO check if SPDY should be here
|
||||
return fromOKHttp(uri, tracker)
|
||||
|
||||
"file" -> getDefaultInputStreamForUri(uri)
|
||||
else -> {
|
||||
tracker.trackException("unknown scheme in ImportAsyncTask" + uri.scheme, false)
|
||||
getDefaultInputStreamForUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fromOKHttp(uri: Uri, tracker: Tracker): InputStreamWithSource? {
|
||||
val client = OkHttpClient()
|
||||
val url = URL(uri.toString())
|
||||
val requestBuilder = Request.Builder().url(url)
|
||||
|
||||
// fake to be an iPhone in some cases when the server decides to send no passbook
|
||||
// to android phones - but only do it then - we are proud to be Android ;-)
|
||||
val iPhoneFakeMap = mapOf(
|
||||
"air_canada" to "//m.aircanada.ca/ebp/",
|
||||
"air_canada2" to "//services.aircanada.com/ebp/",
|
||||
"air_canada3" to "//mci.aircanada.com/mci/bp/",
|
||||
"icelandair" to "//checkin.si.amadeus.net",
|
||||
"mbk" to "//mbk.thy.com/",
|
||||
"heathrow" to "//passbook.heathrow.com/",
|
||||
"eventbrite" to "//www.eventbrite.com/passes/order"
|
||||
)
|
||||
|
||||
for ((key, value) in iPhoneFakeMap) {
|
||||
if (uri.toString().contains(value)) {
|
||||
tracker.trackEvent("quirk_fix", "ua_fake", key, null)
|
||||
requestBuilder.header("User-Agent", IPHONE_USER_AGENT)
|
||||
}
|
||||
}
|
||||
|
||||
val request = requestBuilder.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
val body = response.body()
|
||||
|
||||
if (body != null) {
|
||||
return InputStreamWithSource(uri.toString(), body.byteStream())
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun fromContent(ctx: Context, uri: Uri) = ctx.contentResolver.openInputStream(uri)?.let {
|
||||
InputStreamWithSource(uri.toString(), it)
|
||||
}
|
||||
|
||||
private fun getDefaultInputStreamForUri(uri: Uri) = InputStreamWithSource(uri.toString(), BufferedInputStream(URL(uri.toString()).openStream(), 4096))
|
|
@ -1,16 +0,0 @@
|
|||
package org.ligi.passandroid.functions
|
||||
|
||||
import android.app.Activity
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.ligi.passandroid.R
|
||||
import org.ligi.passandroid.model.PassClassifier
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
|
||||
fun moveWithUndoSnackbar(passClassifier: PassClassifier, pass: Pass, topic: String, activity: Activity) {
|
||||
val oldTopic = passClassifier.getTopic(pass, "")
|
||||
|
||||
Snackbar.make(activity.window.decorView.findViewById(R.id.fam), "Pass moved to $topic", Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.undo) { passClassifier.moveToTopic(pass, oldTopic) }
|
||||
.show()
|
||||
passClassifier.moveToTopic(pass, topic)
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package org.ligi.passandroid.functions
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import org.ligi.passandroid.R
|
||||
import org.ligi.passandroid.model.PassBitmapDefinitions
|
||||
import org.ligi.passandroid.model.PassStore
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import org.ligi.passandroid.model.pass.PassField
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
import org.ligi.passandroid.model.pass.PassType
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.util.*
|
||||
|
||||
const val APP = "passandroid"
|
||||
|
||||
fun createAndAddEmptyPass(passStore: PassStore, resources: Resources): Pass {
|
||||
val pass = createBasePass()
|
||||
|
||||
pass.description = "custom Pass"
|
||||
|
||||
passStore.currentPass = pass
|
||||
passStore.save(pass)
|
||||
|
||||
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_launcher)
|
||||
|
||||
try {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 90, FileOutputStream(File(passStore.getPathForID(pass.id), PassBitmapDefinitions.BITMAP_ICON + ".png")))
|
||||
} catch (ignored: FileNotFoundException) {
|
||||
}
|
||||
|
||||
return pass
|
||||
}
|
||||
|
||||
fun createPassForImageImport(resources: Resources): Pass {
|
||||
return createBasePass().apply {
|
||||
description = resources.getString(R.string.image_import)
|
||||
|
||||
fields = mutableListOf(
|
||||
PassField.create(R.string.field_source, R.string.field_source_image, resources),
|
||||
PassField.create(R.string.field_advice_label, R.string.field_advice_text, resources),
|
||||
PassField.create(R.string.field_note, R.string.field_note_image, resources, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createPassForPDFImport(resources: Resources): Pass {
|
||||
return createBasePass().apply {
|
||||
description = resources.getString(R.string.pdf_import)
|
||||
|
||||
fields = mutableListOf(
|
||||
PassField.create(R.string.field_source, R.string.field_source_pdf, resources),
|
||||
PassField.create(R.string.field_advice_label, R.string.field_advice_text, resources),
|
||||
PassField.create(R.string.field_note, R.string.field_note_pdf, resources, true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBasePass(): PassImpl {
|
||||
val pass = PassImpl(UUID.randomUUID().toString())
|
||||
pass.accentColor = 0xFF0000FF.toInt()
|
||||
pass.app = APP
|
||||
pass.type = PassType.EVENT
|
||||
return pass
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
package org.ligi.passandroid.functions
|
||||
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* I got a really broken passes with invalid json from users.
|
||||
* As it is not possible to change the problem in the generator side
|
||||
* It has to be worked around here
|
||||
*/
|
||||
|
||||
private val replacementMap = mapOf(
|
||||
|
||||
// first we try without fixing -> always positive and try to have minimal impact
|
||||
"" to "",
|
||||
|
||||
// but here the horror starts ..
|
||||
|
||||
// Fix for Virgin Australia
|
||||
// "value": "NTL",}
|
||||
// a comma should never be before a closing curly brace like this ,}
|
||||
|
||||
// note the \t are greetings to Empire Theatres Tickets - without it their passes do not work
|
||||
|
||||
",[\n\r\t ]*\\}" to "}",
|
||||
/*
|
||||
Entrada cine Entradas.com
|
||||
{
|
||||
"key": "passSourceUpdate",
|
||||
"label": "Actualiza tu entrada",
|
||||
"value": "http://www.entradas.com/entradas/passbook.do?cutout
|
||||
},
|
||||
],
|
||||
*/
|
||||
",[\n\r\t ]*\\]" to "]",
|
||||
|
||||
/*
|
||||
forgotten value aka ( also Entradas.com):
|
||||
"locations": [
|
||||
{
|
||||
"latitude": ,
|
||||
"longitude": ,
|
||||
"relevantText": "Bienvenido a yelmo cines espacio coruña"
|
||||
}
|
||||
*/
|
||||
|
||||
":[ ]*,[\n\r\t ]*\"" to ":\"\",",
|
||||
|
||||
/*
|
||||
from RENFE OPERADORA Billete de tren
|
||||
|
||||
],
|
||||
"transitType": "PKTransitTypeTrain"
|
||||
},
|
||||
,
|
||||
"relevantDate": "2013-08-10T19:15+02:00"
|
||||
*/
|
||||
|
||||
",[\n\r\t ]*," to ","
|
||||
)
|
||||
|
||||
@Throws(JSONException::class)
|
||||
fun readJSONSafely(str: String?): JSONObject? {
|
||||
if (str == null) {
|
||||
return null
|
||||
}
|
||||
var allReplaced: String = str
|
||||
|
||||
// first try with single fixes
|
||||
for ((key, value) in replacementMap) {
|
||||
try {
|
||||
allReplaced = allReplaced.replace(key.toRegex(), value)
|
||||
return JSONObject(str.replace(key.toRegex(), value))
|
||||
} catch (e: JSONException) {
|
||||
// expected because of problems in JSON we are trying to fix here
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// if that did not work do combination of all - if this is not working a JSONException is thrown
|
||||
return JSONObject(allReplaced)
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package org.ligi.passandroid.json_adapter
|
||||
|
||||
import android.graphics.Color
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.ToJson
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
|
||||
class ColorAdapter {
|
||||
@ToJson
|
||||
internal fun toJson(@PassImpl.HexColor rgb: Int) = String.format("#%06x", rgb)
|
||||
|
||||
@FromJson
|
||||
@PassImpl.HexColor
|
||||
internal fun fromJson(rgb: String) = Color.parseColor(rgb)
|
||||
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package org.ligi.passandroid.json_adapter
|
||||
|
||||
import com.squareup.moshi.FromJson
|
||||
import com.squareup.moshi.ToJson
|
||||
import org.threeten.bp.ZonedDateTime
|
||||
import org.threeten.bp.format.DateTimeFormatter
|
||||
|
||||
class ZonedTimeAdapter {
|
||||
|
||||
@ToJson
|
||||
internal fun toJson(zonedDateTime: ZonedDateTime) = zonedDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||
|
||||
@FromJson
|
||||
internal fun fromJson(zonedDateTime: String) = ZonedDateTime.parse(zonedDateTime)
|
||||
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
package org.ligi.passandroid.model
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.JsonDataException
|
||||
import com.squareup.moshi.Moshi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import org.ligi.passandroid.BuildConfig
|
||||
import org.ligi.passandroid.Tracker
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
import org.ligi.passandroid.reader.AppleStylePassReader
|
||||
import org.ligi.passandroid.reader.PassReader
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
object PassStoreUpdateEvent
|
||||
|
||||
class AndroidFileSystemPassStore(
|
||||
private val context: Context,
|
||||
settings: Settings,
|
||||
private val moshi: Moshi
|
||||
) : PassStore, KoinComponent {
|
||||
|
||||
override val updateChannel = ConflatedBroadcastChannel<PassStoreUpdateEvent>()
|
||||
|
||||
private val path: File = settings.getPassesDir()
|
||||
|
||||
override val passMap = HashMap<String, Pass>()
|
||||
|
||||
override var currentPass: Pass? = null
|
||||
|
||||
private val tracker: Tracker by inject()
|
||||
|
||||
override val classifier: PassClassifier by lazy {
|
||||
val classificationFile = File(settings.getStateDir(), "classifier_state.json")
|
||||
FileBackedPassClassifier(classificationFile, this, moshi)
|
||||
}
|
||||
|
||||
override fun save(pass: Pass) {
|
||||
val jsonAdapter = moshi.adapter(PassImpl::class.java)
|
||||
|
||||
val pathForID = getPathForID(pass.id)
|
||||
|
||||
if (!pathForID.exists()) {
|
||||
pathForID.mkdirs()
|
||||
}
|
||||
|
||||
val buffer = File(pathForID, "main.json").sink().buffer()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
val of = com.squareup.moshi.JsonWriter.of(buffer)
|
||||
of.indent = " "
|
||||
jsonAdapter.toJson(of, pass as PassImpl)
|
||||
buffer.close()
|
||||
of.close()
|
||||
} else {
|
||||
jsonAdapter.toJson(buffer, pass as PassImpl)
|
||||
buffer.close()
|
||||
}
|
||||
|
||||
passMap[pass.id] = pass
|
||||
}
|
||||
|
||||
private fun readPass(id: String): Pass? {
|
||||
val pathForID = getPathForID(id)
|
||||
val language = context.resources.configuration.locale.language
|
||||
|
||||
if (!pathForID.exists() || !pathForID.isDirectory) {
|
||||
return null
|
||||
}
|
||||
|
||||
val file = File(pathForID, "main.json")
|
||||
var result: Pass? = null
|
||||
var dirty = true
|
||||
if (file.exists()) {
|
||||
val jsonAdapter = moshi.adapter(PassImpl::class.java)
|
||||
dirty = false
|
||||
try {
|
||||
result = jsonAdapter.fromJson(file.source().buffer())
|
||||
} catch (ignored: JsonDataException) {
|
||||
tracker.trackException("invalid main.json", false)
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null && File(pathForID, "data.json").exists()) {
|
||||
result = PassReader.read(pathForID)
|
||||
File(pathForID, "data.json").delete()
|
||||
}
|
||||
|
||||
if (result == null && File(pathForID, "pass.json").exists()) {
|
||||
result = AppleStylePassReader.read(pathForID, language, context, tracker)
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
if (dirty) {
|
||||
save(result)
|
||||
}
|
||||
passMap[id] = result
|
||||
notifyChange()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
override fun getPassbookForId(id: String): Pass? {
|
||||
return passMap[id] ?: readPass(id)
|
||||
}
|
||||
|
||||
override fun deletePassWithId(id: String): Boolean {
|
||||
val result = getPathForID(id).deleteRecursively()
|
||||
if (result) {
|
||||
passMap.remove(id)
|
||||
classifier.removePass(id)
|
||||
notifyChange()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun getPathForID(id: String): File {
|
||||
return File(path, id)
|
||||
}
|
||||
|
||||
override fun notifyChange() {
|
||||
GlobalScope.launch {
|
||||
updateChannel.send(PassStoreUpdateEvent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun syncPassStoreWithClassifier(defaultTopic: String) {
|
||||
val keysToRemove = classifier.topicByIdMap.keys.filter { getPassbookForId(it) == null }
|
||||
|
||||
for (key in keysToRemove) {
|
||||
classifier.topicByIdMap.remove(key)
|
||||
}
|
||||
|
||||
val allPasses = path.listFiles()
|
||||
allPasses?.forEach {
|
||||
classifier.getTopic(it.name, defaultTopic)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package org.ligi.passandroid.model
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import org.ligi.passandroid.R
|
||||
import org.ligi.passandroid.R.string.preference_key_autolight
|
||||
import org.ligi.passandroid.R.string.preference_key_condensed
|
||||
import org.ligi.passandroid.model.comparator.PassSortOrder
|
||||
import java.io.File
|
||||
|
||||
class AndroidSettings(val context: Context) : Settings {
|
||||
|
||||
private val sharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(context) }
|
||||
|
||||
override fun getSortOrder(): PassSortOrder {
|
||||
val key = context.getString(R.string.preference_key_sort)
|
||||
val stringValue = sharedPreferences.getString(key, "0")
|
||||
val id = Integer.valueOf(stringValue!!)
|
||||
|
||||
return PassSortOrder.values().first { it.int == id }
|
||||
}
|
||||
|
||||
override fun doTraceDroidEmailSend() = true
|
||||
|
||||
override fun getPassesDir() = File(context.filesDir.absolutePath, "passes")
|
||||
|
||||
override fun getStateDir() = File(context.filesDir, "state")
|
||||
|
||||
override fun isCondensedModeEnabled() = sharedPreferences.getBoolean(context.getString(preference_key_condensed), false)
|
||||
|
||||
override fun isAutomaticLightEnabled() = sharedPreferences.getBoolean(context.getString(preference_key_autolight), true)
|
||||
|
||||
override fun getNightMode(): Int {
|
||||
val key = sharedPreferences.getString(context.getString(R.string.preference_key_nightmode), "auto")
|
||||
return when (key) {
|
||||
"day" -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
"night" -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
"auto" -> AppCompatDelegate.MODE_NIGHT_AUTO
|
||||
else -> AppCompatDelegate.MODE_NIGHT_AUTO
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,227 +0,0 @@
|
|||
package org.ligi.passandroid.model
|
||||
|
||||
import org.ligi.passandroid.Tracker
|
||||
import org.ligi.passandroid.model.pass.PassField
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
import org.threeten.bp.DateTimeException
|
||||
import org.threeten.bp.ZonedDateTime
|
||||
|
||||
class ApplePassbookQuirkCorrector(val tracker: Tracker) {
|
||||
|
||||
fun correctQuirks(pass: PassImpl) {
|
||||
// Vendor specific fixes
|
||||
careForTUIFlight(pass)
|
||||
careForAirBerlin(pass)
|
||||
careForWestbahn(pass)
|
||||
careForAirCanada(pass)
|
||||
careForUSAirways(pass)
|
||||
careForVirginAustralia(pass)
|
||||
careForCathayPacific(pass)
|
||||
careForSWISS(pass)
|
||||
careForRenfe(pass)
|
||||
|
||||
//general fixes
|
||||
tryToFindDate(pass)
|
||||
}
|
||||
|
||||
private fun tryToFindDate(pass: PassImpl) {
|
||||
|
||||
if (pass.calendarTimespan == null) {
|
||||
val foundDate = pass.fields.filter { "date" == it.key }.map {
|
||||
try {
|
||||
ZonedDateTime.parse(it.value)
|
||||
} catch (e: DateTimeException) {
|
||||
null
|
||||
}
|
||||
}.firstOrNull { it != null }
|
||||
|
||||
if (foundDate != null) {
|
||||
tracker.trackEvent("quirk_fix", "find_date", "find_date", 0L)
|
||||
pass.calendarTimespan = PassImpl.TimeSpan(from = foundDate)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun getPassFieldForKey(pass: PassImpl, key: String): PassField? {
|
||||
return pass.fields.firstOrNull { it.key != null && it.key == key }
|
||||
}
|
||||
|
||||
private fun getPassFieldThatMatchesLabel(pass: PassImpl, matcher: String): PassField? {
|
||||
return pass.fields.firstOrNull {
|
||||
val label = it.label
|
||||
label != null && label.matches(matcher.toRegex())
|
||||
}
|
||||
}
|
||||
|
||||
private fun careForRenfe(pass: PassImpl) {
|
||||
if (pass.creator == null || pass.creator != "RENFE OPERADORA") {
|
||||
return
|
||||
}
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "RENFE OPERADORA", 0L)
|
||||
|
||||
val optionalDepart = getPassFieldForKey(pass, "boardingTime")
|
||||
val optionalArrive = getPassFieldForKey(pass, "destino")
|
||||
|
||||
if (optionalDepart != null && optionalArrive != null) {
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "RENFE OPERADORA", 1L)
|
||||
pass.description = optionalDepart.label + " -> " + optionalArrive.label
|
||||
}
|
||||
}
|
||||
|
||||
private fun careForSWISS(pass: PassImpl) {
|
||||
if (pass.creator == null || pass.creator != "SWISS") {
|
||||
return
|
||||
}
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "SWISS", 0L)
|
||||
|
||||
val optionalDepart = getPassFieldForKey(pass, "depart")
|
||||
val optionalArrive = getPassFieldForKey(pass, "destination")
|
||||
|
||||
if (optionalDepart != null && optionalArrive != null) {
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "SWISS", 1L)
|
||||
pass.description = optionalDepart.value + " -> " + optionalArrive.value
|
||||
}
|
||||
}
|
||||
|
||||
private fun careForCathayPacific(pass: PassImpl) {
|
||||
if (pass.creator == null || pass.creator != "Cathay Pacific") {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "cathay_pacific", 0L)
|
||||
|
||||
val optionalDepart = getPassFieldForKey(pass, "departure")
|
||||
val optionalArrive = getPassFieldForKey(pass, "arrival")
|
||||
|
||||
if (optionalDepart != null && optionalArrive != null) {
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "cathay_pacific", 1L)
|
||||
pass.description = optionalDepart.label + " -> " + optionalArrive.label
|
||||
}
|
||||
}
|
||||
|
||||
private fun careForVirginAustralia(pass: PassImpl) {
|
||||
// also good identifier could be "passTypeIdentifier": "pass.com.virginaustralia.boardingpass
|
||||
if (pass.creator == null || pass.creator != "Virgin Australia") {
|
||||
return
|
||||
}
|
||||
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "virgin_australia", 0L)
|
||||
|
||||
val optionalDepart = getPassFieldForKey(pass, "origin")
|
||||
val optionalArrive = getPassFieldForKey(pass, "destination")
|
||||
|
||||
if (optionalDepart != null && optionalArrive != null) {
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "virgin_australia", 1L)
|
||||
pass.description = optionalDepart.label + " -> " + optionalArrive.label
|
||||
}
|
||||
}
|
||||
|
||||
private fun careForAirCanada(pass: PassImpl) {
|
||||
if (pass.creator == null || pass.creator != "Air Canada") {
|
||||
return
|
||||
}
|
||||
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "air_canada", 0L)
|
||||
|
||||
val optionalDepart = getPassFieldForKey(pass, "depart")
|
||||
val optionalArrive = getPassFieldForKey(pass, "arrive")
|
||||
|
||||
if (optionalDepart != null && optionalArrive != null) {
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "air_canada", 1L)
|
||||
pass.description = optionalDepart.label + " -> " + optionalArrive.label
|
||||
}
|
||||
}
|
||||
|
||||
private fun careForUSAirways(pass: PassImpl) {
|
||||
if (pass.creator == null || pass.creator != "US Airways") {
|
||||
return
|
||||
}
|
||||
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "usairways", 0L)
|
||||
|
||||
val optionalDepart = getPassFieldForKey(pass, "depart")
|
||||
val optionalArrive = getPassFieldForKey(pass, "destination")
|
||||
|
||||
if (optionalDepart != null && optionalArrive != null) {
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "usairways", 1L)
|
||||
pass.description = optionalDepart.label + " -> " + optionalArrive.label
|
||||
}
|
||||
}
|
||||
|
||||
private fun careForWestbahn(pass: PassImpl) {
|
||||
if (pass.calendarTimespan != null || (pass.creator ?: "") != "WESTbahn") {
|
||||
return
|
||||
}
|
||||
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "westbahn", 0L)
|
||||
|
||||
val originField = getPassFieldForKey(pass, "from")
|
||||
val destinationField = getPassFieldForKey(pass, "to")
|
||||
|
||||
var description = "WESTbahn"
|
||||
|
||||
if (originField != null) {
|
||||
val value = originField.value
|
||||
if (value != null) {
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "westbahn", 1L)
|
||||
description = value
|
||||
}
|
||||
}
|
||||
|
||||
if (destinationField != null) {
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "westbahn", 2L)
|
||||
description += "->" + destinationField.value!!
|
||||
}
|
||||
|
||||
pass.description = description
|
||||
}
|
||||
|
||||
private fun careForTUIFlight(pass: PassImpl) {
|
||||
if (pass.description != "TUIfly pass") {
|
||||
return
|
||||
}
|
||||
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "tuiflight", 0L)
|
||||
|
||||
val originField = getPassFieldForKey(pass, "Origin")
|
||||
val destinationField = getPassFieldForKey(pass, "Des")
|
||||
val seatField = getPassFieldForKey(pass, "SeatNumber")
|
||||
|
||||
if (originField != null && destinationField != null) {
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "tuiflight", 1L)
|
||||
var description = originField.value + "->" + destinationField.value
|
||||
if (seatField != null) {
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "tuiflight", 2L)
|
||||
description += " @" + seatField.value!!
|
||||
}
|
||||
pass.description = description
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun careForAirBerlin(pass: PassImpl) {
|
||||
if (pass.description != "boardcard") {
|
||||
return
|
||||
}
|
||||
|
||||
val flightRegex = "\\b\\w{1,3}\\d{3,4}\\b"
|
||||
|
||||
var flightField = getPassFieldThatMatchesLabel(pass, flightRegex)
|
||||
|
||||
if (flightField == null) {
|
||||
flightField = getPassFieldThatMatchesLabel(pass, flightRegex)
|
||||
}
|
||||
val seatField = getPassFieldForKey(pass, "seat")
|
||||
val boardingGroupField = getPassFieldForKey(pass, "boardingGroup")
|
||||
|
||||
if (flightField != null && seatField != null && boardingGroupField != null) {
|
||||
tracker.trackEvent("quirk_fix", "description_replace", "air_berlin", 0L)
|
||||
var description = flightField.label + " " + flightField.value
|
||||
description += " | " + seatField.label + " " + seatField.value
|
||||
description += " | " + boardingGroupField.label + " " + boardingGroupField.value
|
||||
pass.description = description
|
||||
} // otherwise fallback to default - better save than sorry
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
package org.ligi.passandroid.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.ibm.icu.text.CharsetDetector;
|
||||
import com.ibm.icu.text.CharsetMatch;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class AppleStylePassTranslation extends HashMap<String, String> {
|
||||
|
||||
public String translate(String key) {
|
||||
if (containsKey(key)) {
|
||||
return get(key);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
public void loadFromFile(final File file) {
|
||||
final String content = readFileAsStringGuessEncoding(file);
|
||||
loadFromString(content);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void loadFromString(final String inputString) {
|
||||
|
||||
if (inputString == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String pair : inputString.split("\";")) {
|
||||
final String[] kv = pair.split("\" ?= ?\"");
|
||||
if (kv.length == 2) {
|
||||
put(removeLeadingClutter(kv[0]), kv[1]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static String removeLeadingClutter(String s) {
|
||||
if (s.startsWith("\"") || s.startsWith("\n") || s.startsWith("\r") || s.startsWith(" ")) {
|
||||
return removeLeadingClutter(s.substring(1));
|
||||
} else {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nullable
|
||||
public static String readFileAsStringGuessEncoding(final @NonNull File file) {
|
||||
try {
|
||||
final byte[] fileData = new byte[(int) file.length()];
|
||||
final DataInputStream dataInputStream = new DataInputStream(new FileInputStream(file));
|
||||
dataInputStream.readFully(fileData);
|
||||
dataInputStream.close();
|
||||
|
||||
if (fileData[0] == (byte) 0xEF && fileData[1] == (byte) 0xBB && fileData[2] == (byte) 0xBF) {
|
||||
final byte[] crop = new byte[fileData.length - 3];
|
||||
System.arraycopy(fileData, 3, crop, 0, crop.length);
|
||||
//noinspection CharsetObjectCanBeUsed
|
||||
return new String(crop, "utf-8");
|
||||
}
|
||||
|
||||
final CharsetMatch match = new CharsetDetector().setText(fileData).detect();
|
||||
|
||||
if (match != null) try {
|
||||
return new String(fileData, match.getName());
|
||||
} catch (UnsupportedEncodingException ignored) {
|
||||
}
|
||||
return new String(fileData);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
package org.ligi.passandroid.model;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import okio.BufferedSink;
|
||||
import okio.Okio;
|
||||
|
||||
public class FileBackedPassClassifier extends PassClassifier {
|
||||
|
||||
private final JsonAdapter<Map> adapter;
|
||||
private final File backed_file;
|
||||
|
||||
public FileBackedPassClassifier(final File backed_file, final PassStore passStore, final Moshi moshi) {
|
||||
super(loadMap(backed_file, moshi), passStore);
|
||||
|
||||
this.backed_file = backed_file;
|
||||
adapter = getAdapter(moshi);
|
||||
}
|
||||
|
||||
private static JsonAdapter<Map> getAdapter(Moshi moshi) {
|
||||
return moshi.adapter(Map.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, String> loadMap(final File backed_file, final Moshi moshi) {
|
||||
|
||||
if (backed_file.exists()) {
|
||||
try {
|
||||
return (Map<String, String>) getAdapter(moshi).fromJson(Okio.buffer(Okio.source(backed_file)));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
private BufferedSink getBufferedSinkFromMaybeCreatedFile() {
|
||||
try {
|
||||
if (!backed_file.exists()) {
|
||||
final File parentFile = backed_file.getParentFile();
|
||||
if (!parentFile.exists()) {
|
||||
parentFile.mkdirs();
|
||||
}
|
||||
backed_file.createNewFile();
|
||||
}
|
||||
|
||||
return Okio.buffer(Okio.sink(backed_file));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processDataChange() {
|
||||
super.processDataChange();
|
||||
|
||||
// write
|
||||
if (adapter != null) {
|
||||
final BufferedSink buffer = getBufferedSinkFromMaybeCreatedFile();
|
||||
|
||||
if (buffer != null) {
|
||||
try {
|
||||
adapter.toJson(buffer, getTopicByIdMap());
|
||||
buffer.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package org.ligi.passandroid.model
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
class InputStreamWithSource(val source: String, val inputStream: InputStream)
|
|
@ -1,10 +0,0 @@
|
|||
package org.ligi.passandroid.model;
|
||||
|
||||
public class PassBitmapDefinitions {
|
||||
|
||||
public final static String BITMAP_ICON = "icon";
|
||||
public final static String BITMAP_THUMBNAIL = "thumbnail";
|
||||
public final static String BITMAP_STRIP = "strip";
|
||||
public final static String BITMAP_LOGO = "logo";
|
||||
public final static String BITMAP_FOOTER = "footer";
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package org.ligi.passandroid.model
|
||||
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
|
||||
open class PassClassifier(val topicByIdMap: MutableMap<String, String>, private val passStore: PassStore) {
|
||||
|
||||
open fun processDataChange() {
|
||||
val topicsToRemove = topicByIdMap.filter { it.value.isEmpty() }.map { it.value }
|
||||
|
||||
topicsToRemove.forEach {
|
||||
topicByIdMap.remove(it)
|
||||
}
|
||||
passStore.notifyChange()
|
||||
}
|
||||
|
||||
fun moveToTopic(pass: Pass, newTopic: String) {
|
||||
topicByIdMap[pass.id] = newTopic
|
||||
|
||||
processDataChange()
|
||||
}
|
||||
|
||||
fun getTopics(): Set<String> {
|
||||
return topicByIdMap.values.toSet()
|
||||
}
|
||||
|
||||
fun getPassListByTopic(topic: String): List<Pass> {
|
||||
return topicByIdMap.filter { it.value == topic }.map { passStore.getPassbookForId(it.key) }.filterNotNull()
|
||||
}
|
||||
|
||||
fun getTopic(pass: Pass, default: String): String {
|
||||
return getTopic(pass.id, default)
|
||||
}
|
||||
|
||||
fun getTopic(id: String, default: String): String {
|
||||
val s = topicByIdMap[id]
|
||||
if (s != null) {
|
||||
return s
|
||||
}
|
||||
|
||||
if (!default.isEmpty()) {
|
||||
topicByIdMap[id] = default
|
||||
processDataChange()
|
||||
}
|
||||
return default
|
||||
}
|
||||
|
||||
fun removePass(id: String) {
|
||||
topicByIdMap.remove(id)
|
||||
processDataChange()
|
||||
}
|
||||
|
||||
/*
|
||||
useful offsets are -1 and 1 to find the topic right and left from the pass
|
||||
*/
|
||||
fun getTopicWithOffset(pass: Pass, offset: Int): String? {
|
||||
val indexOf = getTopics().indexOf(getTopic(pass, ""))
|
||||
return getTopics().elementAtOrNull(indexOf + offset)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package org.ligi.passandroid.model
|
||||
|
||||
import org.ligi.passandroid.model.pass.PassType.*
|
||||
|
||||
object PassDefinitions {
|
||||
|
||||
val TYPE_TO_NAME = mapOf(COUPON to "coupon",
|
||||
EVENT to "eventTicket",
|
||||
BOARDING to "boardingPass",
|
||||
GENERIC to "generic",
|
||||
LOYALTY to "storeCard")
|
||||
|
||||
val NAME_TO_TYPE = TYPE_TO_NAME.entries.associate { it.value to it.key }
|
||||
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package org.ligi.passandroid.model
|
||||
|
||||
import kotlinx.coroutines.channels.BroadcastChannel
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import java.io.File
|
||||
|
||||
interface PassStore {
|
||||
|
||||
val updateChannel: BroadcastChannel<PassStoreUpdateEvent>
|
||||
|
||||
fun save(pass: Pass)
|
||||
|
||||
fun getPassbookForId(id: String): Pass?
|
||||
|
||||
fun deletePassWithId(id: String): Boolean
|
||||
|
||||
fun getPathForID(id: String): File
|
||||
|
||||
val passMap: Map<String, Pass>
|
||||
|
||||
var currentPass: Pass?
|
||||
|
||||
val classifier: PassClassifier
|
||||
|
||||
fun notifyChange()
|
||||
|
||||
fun syncPassStoreWithClassifier(defaultTopic: String)
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package org.ligi.passandroid.model
|
||||
|
||||
import org.ligi.passandroid.model.comparator.PassSortOrder
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import java.util.*
|
||||
|
||||
class PassStoreProjection(private val passStore: PassStore, private val topic: String, private val passSortOrder: PassSortOrder? = null) {
|
||||
|
||||
var passList: List<Pass> = ArrayList()
|
||||
private set
|
||||
|
||||
init {
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
val newPassList = passStore.classifier.getPassListByTopic(topic)
|
||||
if (passSortOrder != null) {
|
||||
Collections.sort(newPassList, passSortOrder.toComparator())
|
||||
}
|
||||
|
||||
passList = newPassList
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package org.ligi.passandroid.model
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import org.ligi.passandroid.Tracker
|
||||
import java.util.*
|
||||
|
||||
class PastLocationsStore constructor(private val sharedPreferences: SharedPreferences, private val tracker: Tracker) {
|
||||
|
||||
fun putLocation(path: String) {
|
||||
val pastLocations = sharedPreferences.getStringSet(KEY_PAST_LOCATIONS, HashSet<String>())
|
||||
|
||||
if (pastLocations!!.size >= MAX_ELEMENTS) {
|
||||
deleteOneElementFromSet(pastLocations)
|
||||
}
|
||||
|
||||
if (!pastLocations.contains(path)) {
|
||||
pastLocations.add(path)
|
||||
}
|
||||
|
||||
tracker.trackEvent("scan", "put location", "count", pastLocations.size.toLong())
|
||||
sharedPreferences.edit().putStringSet(KEY_PAST_LOCATIONS, pastLocations).apply()
|
||||
}
|
||||
|
||||
private fun deleteOneElementFromSet(pastLocations: MutableSet<String>) {
|
||||
val deleteAtPosition = (Math.random() * MAX_ELEMENTS).toInt()
|
||||
for ((pos, location) in pastLocations.withIndex()) {
|
||||
if (pos == deleteAtPosition) {
|
||||
pastLocations.remove(location)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// feature not available for these versions
|
||||
val locations: Set<String>
|
||||
get() = sharedPreferences.getStringSet(KEY_PAST_LOCATIONS, emptySet<String>())?: emptySet()
|
||||
|
||||
companion object {
|
||||
|
||||
const val KEY_PAST_LOCATIONS = "past_locations"
|
||||
const val MAX_ELEMENTS = 5
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package org.ligi.passandroid.model
|
||||
|
||||
import org.ligi.passandroid.model.comparator.PassSortOrder
|
||||
import java.io.File
|
||||
|
||||
interface Settings {
|
||||
|
||||
fun getSortOrder(): PassSortOrder
|
||||
|
||||
fun doTraceDroidEmailSend(): Boolean
|
||||
|
||||
fun getPassesDir(): File
|
||||
|
||||
fun getStateDir(): File
|
||||
|
||||
fun isCondensedModeEnabled(): Boolean
|
||||
|
||||
fun isAutomaticLightEnabled(): Boolean
|
||||
|
||||
fun getNightMode(): Int
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package org.ligi.passandroid.model
|
||||
|
||||
import com.chibatching.kotpref.KotprefModel
|
||||
|
||||
object State : KotprefModel() {
|
||||
|
||||
var lastSelectedTab: Int by intPref()
|
||||
var lastSelectedPassUUID: String by stringPref()
|
||||
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package org.ligi.passandroid.model.comparator
|
||||
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
|
||||
class DirectionAwarePassByTimeComparator(private val direction: Int) : PassByTimeComparator() {
|
||||
|
||||
override fun compare(lhs: Pass, rhs: Pass): Int {
|
||||
return super.compare(lhs, rhs) * direction
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DIRECTION_DESC = -1
|
||||
const val DIRECTION_ASC = 1
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package org.ligi.passandroid.model.comparator
|
||||
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import org.threeten.bp.ZonedDateTime
|
||||
import java.util.*
|
||||
|
||||
open class PassByTimeComparator : Comparator<Pass> {
|
||||
|
||||
override fun compare(lhs: Pass, rhs: Pass): Int {
|
||||
return calculateCompareForNullValues(lhs, rhs) { leftDate: ZonedDateTime, rightDate: ZonedDateTime ->
|
||||
return@calculateCompareForNullValues leftDate.compareTo(rightDate)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun calculateCompareForNullValues(lhs: Pass, rhs: Pass, foo: (leftDate: ZonedDateTime, rightDate: ZonedDateTime) -> Int): Int {
|
||||
val leftDate = extractPassDate(lhs)
|
||||
val rightDate = extractPassDate(rhs)
|
||||
|
||||
if (leftDate == rightDate) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (leftDate == null) {
|
||||
return 1
|
||||
}
|
||||
if (rightDate == null) {
|
||||
return -1
|
||||
}
|
||||
return foo(leftDate, rightDate)
|
||||
}
|
||||
|
||||
private fun extractPassDate(pass: Pass): ZonedDateTime? {
|
||||
if (pass.calendarTimespan != null && pass.calendarTimespan!!.from != null) {
|
||||
return pass.calendarTimespan!!.from
|
||||
}
|
||||
|
||||
if (pass.validTimespans != null && !pass.validTimespans!!.isEmpty()) {
|
||||
return pass.validTimespans!![0].from
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package org.ligi.passandroid.model.comparator
|
||||
|
||||
import org.ligi.passandroid.model.comparator.DirectionAwarePassByTimeComparator.Companion.DIRECTION_ASC
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import java.util.*
|
||||
|
||||
class PassByTypeFirstAndTimeSecondComparator : Comparator<Pass> {
|
||||
|
||||
private val passByTimeComparator = DirectionAwarePassByTimeComparator(DIRECTION_ASC)
|
||||
|
||||
override fun compare(lhs: Pass, rhs: Pass): Int {
|
||||
val compareResult = lhs.type.compareTo(rhs.type)
|
||||
|
||||
return if (compareResult != 0) {
|
||||
compareResult
|
||||
} else {
|
||||
passByTimeComparator.compare(lhs, rhs)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package org.ligi.passandroid.model.comparator
|
||||
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import java.util.*
|
||||
|
||||
enum class PassSortOrder constructor(val int: Int) {
|
||||
DATE_DESC(0),
|
||||
DATE_ASC(-1),
|
||||
TYPE(1),
|
||||
DATE_DIFF(2);
|
||||
|
||||
fun toComparator(): Comparator<Pass> = when (this) {
|
||||
TYPE -> PassByTypeFirstAndTimeSecondComparator()
|
||||
DATE_DESC -> DirectionAwarePassByTimeComparator(DirectionAwarePassByTimeComparator.DIRECTION_DESC)
|
||||
DATE_DIFF -> PassTemporalDistanceComparator()
|
||||
DATE_ASC -> DirectionAwarePassByTimeComparator(DirectionAwarePassByTimeComparator.DIRECTION_ASC)
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package org.ligi.passandroid.model.comparator
|
||||
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import org.threeten.bp.Duration
|
||||
import org.threeten.bp.LocalDateTime
|
||||
import org.threeten.bp.ZonedDateTime
|
||||
|
||||
class PassTemporalDistanceComparator : PassByTimeComparator() {
|
||||
|
||||
override fun compare(lhs: Pass, rhs: Pass): Int {
|
||||
return calculateCompareForNullValues(lhs, rhs) { leftDate: ZonedDateTime, rightDate: ZonedDateTime ->
|
||||
val durationLeft = Duration.between(LocalDateTime.now(), leftDate).abs()
|
||||
val durationRight = Duration.between(LocalDateTime.now(), rightDate).abs()
|
||||
|
||||
return@calculateCompareForNullValues durationLeft.compareTo(durationRight)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package org.ligi.passandroid.model.pass
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import org.ligi.passandroid.Tracker
|
||||
import org.ligi.passandroid.functions.generateBitmapDrawable
|
||||
import org.ligi.tracedroid.logging.Log
|
||||
import java.util.*
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class BarCode(val format: PassBarCodeFormat?, val message: String? = UUID.randomUUID().toString().toUpperCase()) : KoinComponent {
|
||||
|
||||
val tracker: Tracker by inject ()
|
||||
var alternativeText: String? = null
|
||||
|
||||
fun getBitmap(resources: Resources): BitmapDrawable? {
|
||||
if (message == null) {
|
||||
// no message -> no barcode
|
||||
tracker.trackException("No Barcode in pass - strange", false)
|
||||
return null
|
||||
}
|
||||
|
||||
if (format == null) {
|
||||
Log.w("Barcode format is null - fallback to QR")
|
||||
tracker.trackException("Barcode format is null - fallback to QR", false)
|
||||
return generateBitmapDrawable(resources, message, PassBarCodeFormat.QR_CODE)
|
||||
}
|
||||
|
||||
return generateBitmapDrawable(resources, message, format)
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun getFormatFromString(format: String): PassBarCodeFormat {
|
||||
return when {
|
||||
format.contains("417") -> PassBarCodeFormat.PDF_417
|
||||
format.toUpperCase(Locale.ENGLISH).contains("AZTEC") -> return PassBarCodeFormat.AZTEC
|
||||
format.toUpperCase(Locale.ENGLISH).contains("128") -> return PassBarCodeFormat.CODE_128
|
||||
format.toUpperCase(Locale.ENGLISH).contains("39") -> return PassBarCodeFormat.CODE_39
|
||||
|
||||
/*
|
||||
requested but not supported by xing (yet) https://github.com/ligi/PassAndroid/issues/43
|
||||
format.toUpperCase(Locale.ENGLISH).contains("93")->return BarcodeFormat.CODE_93;
|
||||
*/
|
||||
|
||||
else -> PassBarCodeFormat.QR_CODE
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package org.ligi.passandroid.model.pass
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.annotation.StringDef
|
||||
import org.ligi.passandroid.model.PassBitmapDefinitions.*
|
||||
import org.ligi.passandroid.model.PassStore
|
||||
|
||||
|
||||
interface Pass {
|
||||
|
||||
@StringDef(BITMAP_ICON, BITMAP_THUMBNAIL, BITMAP_STRIP, BITMAP_LOGO, BITMAP_FOOTER)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class PassBitmap
|
||||
|
||||
val description: String?
|
||||
|
||||
var type: PassType
|
||||
|
||||
val fields: List<PassField>
|
||||
|
||||
val locations: List<PassLocation>
|
||||
|
||||
var accentColor: Int
|
||||
|
||||
val id: String
|
||||
|
||||
val creator: String?
|
||||
|
||||
fun getSource(passStore: PassStore): String?
|
||||
|
||||
var barCode: BarCode?
|
||||
|
||||
val webServiceURL: String?
|
||||
|
||||
val authToken: String?
|
||||
|
||||
val serial: String?
|
||||
|
||||
val passIdent: String?
|
||||
|
||||
val app: String?
|
||||
|
||||
val validTimespans: List<PassImpl.TimeSpan>?
|
||||
var calendarTimespan: PassImpl.TimeSpan?
|
||||
|
||||
fun getBitmap(passStore: PassStore, passBitmap: String): Bitmap?
|
||||
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package org.ligi.passandroid.model.pass
|
||||
|
||||
import com.google.zxing.BarcodeFormat
|
||||
|
||||
enum class PassBarCodeFormat {
|
||||
|
||||
AZTEC,
|
||||
CODE_39,
|
||||
CODE_128,
|
||||
DATA_MATRIX,
|
||||
EAN_8,
|
||||
EAN_13,
|
||||
ITF,
|
||||
PDF_417,
|
||||
QR_CODE;
|
||||
|
||||
fun isQuadratic() = when (this) {
|
||||
QR_CODE, AZTEC -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun zxingBarCodeFormat() = when (this) {
|
||||
AZTEC -> BarcodeFormat.AZTEC
|
||||
CODE_39 -> BarcodeFormat.CODE_39
|
||||
CODE_128 -> BarcodeFormat.CODE_128
|
||||
DATA_MATRIX -> BarcodeFormat.DATA_MATRIX
|
||||
EAN_8 -> BarcodeFormat.EAN_8
|
||||
EAN_13 -> BarcodeFormat.EAN_13
|
||||
ITF -> BarcodeFormat.ITF
|
||||
PDF_417 -> BarcodeFormat.PDF_417
|
||||
QR_CODE -> BarcodeFormat.QR_CODE
|
||||
|
||||
else -> BarcodeFormat.QR_CODE
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package org.ligi.passandroid.model.pass
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.StringRes
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class PassField(var key: String?, var label: String?, var value: String?, var hide: Boolean) {
|
||||
|
||||
fun toHtmlSnippet(): String {
|
||||
val result = StringBuilder()
|
||||
|
||||
label?.let { result.append("<b>$label</b> ") }
|
||||
value?.let { result.append(value) }
|
||||
|
||||
if (label != null || value != null) {
|
||||
result.append("<br/>")
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(@StringRes label: Int, @StringRes value: Int, res: Resources, hide: Boolean = false) = PassField(null, res.getString(label), res.getString(value), hide)
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package org.ligi.passandroid.model.pass
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.JsonQualifier
|
||||
import org.ligi.passandroid.model.PassStore
|
||||
import org.threeten.bp.ZonedDateTime
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.util.*
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class PassImpl(override val id: String) : Pass {
|
||||
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@JsonQualifier
|
||||
annotation class HexColor
|
||||
|
||||
override var creator: String? = null
|
||||
|
||||
override var type: PassType = PassType.EVENT
|
||||
|
||||
override var barCode: BarCode? = null
|
||||
|
||||
@field:[HexColor]
|
||||
override var accentColor: Int = 0
|
||||
|
||||
override var description: String? = null
|
||||
get() {
|
||||
if (field == null) {
|
||||
return "" // better way of returning no description - so we can avoid optional / null checks and it is kind of the same thing
|
||||
// an navigation_drawer_header description - we can do kind of all String operations safely this way and do not have to care about the existence of a real description
|
||||
// if we want to know if one is there we can check length for being 0 still ( which we would have to do anyway for navigation_drawer_header descriptions )
|
||||
// See no way at the moment where we would have to distinguish between an navigation_drawer_header and an missing description
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class TimeRepeat(val offset: Int, val count: Int)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class TimeSpan(val from: ZonedDateTime? = null, val to: ZonedDateTime? = null, val repeat: TimeRepeat? = null)
|
||||
|
||||
override var validTimespans: List<TimeSpan> = ArrayList()
|
||||
|
||||
override var calendarTimespan: TimeSpan? = null
|
||||
|
||||
override var fields: MutableList<PassField> = ArrayList()
|
||||
|
||||
override var locations: List<PassLocation> = ArrayList()
|
||||
|
||||
override var app: String? = null
|
||||
|
||||
override var authToken: String? = null
|
||||
|
||||
override var webServiceURL: String? = null
|
||||
|
||||
override var serial: String? = null
|
||||
|
||||
override var passIdent: String? = null
|
||||
|
||||
override fun getBitmap(passStore: PassStore, @Pass.PassBitmap passBitmap: String): Bitmap? {
|
||||
return try {
|
||||
val file = File(passStore.getPathForID(id), passBitmap + FILETYPE_IMAGES)
|
||||
BitmapFactory.decodeStream(FileInputStream(file))
|
||||
} catch (expectedInSomeCases_willJustReturnNull: FileNotFoundException) {
|
||||
null
|
||||
} catch (e: OutOfMemoryError) {
|
||||
null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun getSource(passStore: PassStore): String? {
|
||||
val file = File(passStore.getPathForID(id), "source.txt")
|
||||
|
||||
if (file.exists()) {
|
||||
return file.bufferedReader().readText()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "ID=$id"
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FILETYPE_IMAGES = ".png"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package org.ligi.passandroid.model.pass
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class PassLocation {
|
||||
|
||||
var name: String? = null
|
||||
|
||||
var lat: Double = 0.toDouble()
|
||||
var lon: Double = 0.toDouble()
|
||||
|
||||
fun getNameWithFallback(pass: Pass) = if (name.isNullOrBlank()) {
|
||||
// fallback for passes with locations without description - e.g. AirBerlin
|
||||
pass.description
|
||||
} else {
|
||||
name
|
||||
}
|
||||
|
||||
fun getCommaSeparated() = "$lat,$lon"
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
package org.ligi.passandroid.model.pass
|
||||
|
||||
enum class PassType {
|
||||
GENERIC,
|
||||
EVENT,
|
||||
COUPON,
|
||||
BOARDING,
|
||||
LOYALTY,
|
||||
VOUCHER
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
/**
|
||||
* This is the root of the PassAndroid package
|
||||
*/
|
||||
@javax.annotation.ParametersAreNonnullByDefault package org.ligi.passandroid;
|
|
@ -1,114 +0,0 @@
|
|||
package org.ligi.passandroid.printing
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.CancellationSignal
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.print.PageRange
|
||||
import android.print.PrintAttributes
|
||||
import android.print.PrintDocumentAdapter
|
||||
import android.print.PrintDocumentInfo
|
||||
import android.print.pdf.PrintedPdfDocument
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
class PassPrintDocumentAdapter(private val context: Context, private val pass: Pass, private val jobName: String) : PrintDocumentAdapter() {
|
||||
|
||||
private var mPdfDocument: PrintedPdfDocument? = null
|
||||
|
||||
override fun onLayout(oldAttributes: PrintAttributes,
|
||||
newAttributes: PrintAttributes,
|
||||
cancellationSignal: CancellationSignal,
|
||||
layoutResultCallback: PrintDocumentAdapter.LayoutResultCallback,
|
||||
bundle: Bundle) {
|
||||
|
||||
if (cancellationSignal.isCanceled) {
|
||||
layoutResultCallback.onLayoutCancelled()
|
||||
return
|
||||
}
|
||||
|
||||
mPdfDocument = PrintedPdfDocument(context, newAttributes)
|
||||
|
||||
val info = PrintDocumentInfo.Builder(jobName).setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT).setPageCount(1).build()
|
||||
layoutResultCallback.onLayoutFinished(info, true)
|
||||
|
||||
}
|
||||
|
||||
override fun onWrite(pageRanges: Array<PageRange>,
|
||||
destination: ParcelFileDescriptor,
|
||||
cancellationSignal: CancellationSignal,
|
||||
callback: PrintDocumentAdapter.WriteResultCallback) {
|
||||
|
||||
val page = mPdfDocument!!.startPage(0)
|
||||
val canvas = page.canvas
|
||||
|
||||
drawPass(canvas)
|
||||
mPdfDocument!!.finishPage(page)
|
||||
|
||||
try {
|
||||
mPdfDocument!!.writeTo(FileOutputStream(destination.fileDescriptor))
|
||||
} catch (e: IOException) {
|
||||
callback.onWriteFailed(e.toString())
|
||||
return
|
||||
} finally {
|
||||
mPdfDocument!!.close()
|
||||
mPdfDocument = null
|
||||
}
|
||||
|
||||
callback.onWriteFinished(arrayOf(PageRange.ALL_PAGES))
|
||||
}
|
||||
|
||||
private fun drawPass(canvas: Canvas) {
|
||||
val centerPaint = Paint()
|
||||
centerPaint.textAlign = Paint.Align.CENTER
|
||||
|
||||
canvas.drawText(pass.description!!, canvas.width / 2f, centerPaint.textSize, centerPaint)
|
||||
var currentBottom = centerPaint.textSize * 3
|
||||
|
||||
val barCode = pass.barCode
|
||||
if (barCode != null) {
|
||||
val bitmapDrawable = barCode.getBitmap(context.resources)
|
||||
|
||||
if (bitmapDrawable != null) {
|
||||
val bitmap = bitmapDrawable.bitmap
|
||||
val srcRect = Rect(0, 0, bitmap.width, bitmap.height)
|
||||
val ratio = (canvas.width / 3f) / bitmap.width
|
||||
|
||||
val destRect = Rect(0, 0, (bitmap.width * ratio).toInt(), (bitmap.height * ratio).toInt())
|
||||
|
||||
destRect.offset((canvas.width - destRect.width()) / 2, currentBottom.toInt())
|
||||
|
||||
currentBottom += destRect.bottom
|
||||
|
||||
canvas.drawBitmap(bitmap, srcRect, destRect, centerPaint)
|
||||
|
||||
if (barCode.alternativeText != null) {
|
||||
canvas.drawText(barCode.alternativeText!!, destRect.centerX().toFloat(), destRect.bottom.toFloat() + 7 + centerPaint.textSize, centerPaint)
|
||||
currentBottom += 7 + centerPaint.textSize * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val leftPaint = Paint()
|
||||
leftPaint.textAlign = Paint.Align.RIGHT
|
||||
leftPaint.isFakeBoldText = true
|
||||
|
||||
val rightPaint = Paint()
|
||||
rightPaint.textAlign = Paint.Align.LEFT
|
||||
|
||||
pass.fields.forEach {
|
||||
if (!it.hide) {
|
||||
canvas.drawText(it.label + ": ", canvas.width / 2f, currentBottom, leftPaint)
|
||||
canvas.drawText(" " + it.value, canvas.width / 2f, currentBottom, rightPaint)
|
||||
currentBottom += (centerPaint.textSize * 1.5).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package org.ligi.passandroid.printing
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.print.PrintManager
|
||||
import org.ligi.passandroid.R
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
fun doPrint(context: Context, pass: Pass) {
|
||||
val printManager = context.getSystemService(Context.PRINT_SERVICE) as PrintManager
|
||||
val jobName = context.getString(R.string.app_name) + " print of " + pass.description
|
||||
printManager.print(jobName, PassPrintDocumentAdapter(context, pass, jobName), null)
|
||||
}
|
|
@ -1,329 +0,0 @@
|
|||
package org.ligi.passandroid.reader
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Color
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.ligi.kaxt.parseColor
|
||||
import org.ligi.passandroid.R
|
||||
import org.ligi.passandroid.Tracker
|
||||
import org.ligi.passandroid.functions.getHumanCategoryString
|
||||
import org.ligi.passandroid.functions.readJSONSafely
|
||||
import org.ligi.passandroid.model.ApplePassbookQuirkCorrector
|
||||
import org.ligi.passandroid.model.AppleStylePassTranslation
|
||||
import org.ligi.passandroid.model.PassBitmapDefinitions
|
||||
import org.ligi.passandroid.model.PassDefinitions
|
||||
import org.ligi.passandroid.model.pass.*
|
||||
import org.ligi.tracedroid.logging.Log
|
||||
import org.threeten.bp.DateTimeException
|
||||
import org.threeten.bp.ZonedDateTime
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.util.*
|
||||
|
||||
object AppleStylePassReader {
|
||||
|
||||
fun read(passFile: File, language: String, context: Context, tracker: Tracker): Pass? {
|
||||
|
||||
val translation = AppleStylePassTranslation()
|
||||
|
||||
val pass = PassImpl(passFile.name)
|
||||
|
||||
var passJSON: JSONObject? = null
|
||||
|
||||
val localizedPath = findLocalizedPath(passFile, language, tracker)
|
||||
|
||||
if (localizedPath != null) {
|
||||
val file = File(localizedPath, "pass.strings")
|
||||
translation.loadFromFile(file)
|
||||
}
|
||||
|
||||
copyBitmapFile(passFile, localizedPath, PassBitmapDefinitions.BITMAP_ICON)
|
||||
copyBitmapFile(passFile, localizedPath, PassBitmapDefinitions.BITMAP_LOGO)
|
||||
copyBitmapFile(passFile, localizedPath, PassBitmapDefinitions.BITMAP_STRIP)
|
||||
copyBitmapFile(passFile, localizedPath, PassBitmapDefinitions.BITMAP_THUMBNAIL)
|
||||
copyBitmapFile(passFile, localizedPath, PassBitmapDefinitions.BITMAP_FOOTER)
|
||||
|
||||
val file = File(passFile, "pass.json")
|
||||
|
||||
try {
|
||||
val plainJsonString = AppleStylePassTranslation.readFileAsStringGuessEncoding(file)
|
||||
passJSON = readJSONSafely(plainJsonString)
|
||||
} catch (e: Exception) {
|
||||
Log.i("PassParse Exception: $e")
|
||||
}
|
||||
|
||||
if (passJSON == null) {
|
||||
// I had got a strange passbook with UCS-2 which could not be parsed before
|
||||
// was searching for a auto-detection, but could not find one with support for this encoding
|
||||
// and the right license
|
||||
|
||||
for (charset in Charset.availableCharsets().values) {
|
||||
try {
|
||||
val json = file.bufferedReader(charset).readText()
|
||||
passJSON = readJSONSafely(json)
|
||||
} catch (ignored: Exception) {
|
||||
// we try with next charset
|
||||
}
|
||||
|
||||
if (passJSON != null) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (passJSON == null) {
|
||||
Log.w("could not load pass.json from passcode ")
|
||||
tracker.trackEvent("problem_event", "pass", "without_pass_json", null)
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
val barcodeJSON = passJSON.getBarcodeJson()
|
||||
if (barcodeJSON != null) {
|
||||
val barcodeFormatString = barcodeJSON.getString("format")
|
||||
|
||||
tracker.trackEvent("measure_event", "barcode_format", barcodeFormatString, 0L)
|
||||
val barcodeFormat = BarCode.getFormatFromString(barcodeFormatString)
|
||||
val barCode = BarCode(barcodeFormat, barcodeJSON.getString("message"))
|
||||
pass.barCode = barCode
|
||||
|
||||
if (barcodeJSON.has("altText")) {
|
||||
pass.barCode!!.alternativeText = barcodeJSON.getString("altText")
|
||||
}
|
||||
}
|
||||
// TODO should check a bit more with barcode here - this can be dangerous
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
|
||||
if (passJSON.has("relevantDate")) {
|
||||
try {
|
||||
pass.calendarTimespan = PassImpl.TimeSpan(from = ZonedDateTime.parse(passJSON.getString("relevantDate")))
|
||||
} catch (e: JSONException) {
|
||||
// be robust when it comes to bad dates - had a RL crash with "2013-12-25T00:00-57:00" here
|
||||
// OK then we just have no date here
|
||||
tracker.trackException("problem parsing relevant date", e, false)
|
||||
} catch (e: DateTimeException) {
|
||||
tracker.trackException("problem parsing relevant date", e, false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (passJSON.has("expirationDate")) {
|
||||
try {
|
||||
pass.validTimespans = listOf(PassImpl.TimeSpan(to = ZonedDateTime.parse(passJSON.getString("expirationDate"))))
|
||||
} catch (e: JSONException) {
|
||||
// be robust when it comes to bad dates - had a RL crash with "2013-12-25T00:00-57:00" here
|
||||
// OK then we just have no date here
|
||||
tracker.trackException("problem parsing expiration date", e, false)
|
||||
} catch (e: DateTimeException) {
|
||||
tracker.trackException("problem parsing expiration date", e, false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pass.serial = readJsonSafeAsOptional(passJSON, "serialNumber")
|
||||
pass.authToken = readJsonSafeAsOptional(passJSON, "authenticationToken")
|
||||
pass.webServiceURL = readJsonSafeAsOptional(passJSON, "webServiceURL")
|
||||
pass.passIdent = readJsonSafeAsOptional(passJSON, "passTypeIdentifier")
|
||||
|
||||
val locations = ArrayList<PassLocation>()
|
||||
try {
|
||||
|
||||
val locationsJSON = passJSON.getJSONArray("locations")
|
||||
for (i in 0 until locationsJSON.length()) {
|
||||
val obj = locationsJSON.getJSONObject(i)
|
||||
|
||||
val location = PassLocation()
|
||||
location.lat = obj.getDouble("latitude")
|
||||
location.lon = obj.getDouble("longitude")
|
||||
|
||||
if (obj.has("relevantText")) {
|
||||
location.name = translation.translate(obj.getString("relevantText"))
|
||||
}
|
||||
|
||||
locations.add(location)
|
||||
}
|
||||
|
||||
} catch (ignored: JSONException) {
|
||||
}
|
||||
|
||||
pass.locations = locations
|
||||
|
||||
readJsonSafe(passJSON, "backgroundColor", object : JsonStringReadCallback {
|
||||
override fun onString(string: String) {
|
||||
pass.accentColor = string.parseColor(Color.BLACK)
|
||||
}
|
||||
})
|
||||
|
||||
readJsonSafe(passJSON, "description", object : JsonStringReadCallback {
|
||||
override fun onString(string: String) {
|
||||
pass.description = translation.translate(string)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// try to find in a predefined set of tickets
|
||||
|
||||
PassDefinitions.TYPE_TO_NAME.forEach {
|
||||
if (passJSON.has(it.value)) {
|
||||
pass.type = it.key
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
val type = PassDefinitions.TYPE_TO_NAME[pass.type]
|
||||
val typeJSON = passJSON.getJSONObject(type)
|
||||
if (typeJSON != null) {
|
||||
val fieldList: ArrayList<PassField> = ArrayList()
|
||||
|
||||
addFields(fieldList, typeJSON, "primaryFields", translation)
|
||||
addFields(fieldList, typeJSON, "headerFields", translation)
|
||||
addFields(fieldList, typeJSON, "secondaryFields", translation)
|
||||
addFields(fieldList, typeJSON, "auxiliaryFields", translation)
|
||||
addFields(fieldList, typeJSON, "backFields", translation, hide = true)
|
||||
|
||||
fieldList.add(PassField("", context.getString(R.string.type), context.getString(getHumanCategoryString(pass.type)), false))
|
||||
pass.fields = fieldList
|
||||
}
|
||||
|
||||
} catch (ignored: JSONException) {
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
pass.creator = passJSON.getString("organizationName")
|
||||
tracker.trackEvent("measure_event", "organisation_parse", pass.creator, 1L)
|
||||
} catch (ignored: JSONException) {
|
||||
// ok - we have no organisation - big deal ..-)
|
||||
}
|
||||
|
||||
ApplePassbookQuirkCorrector(tracker).correctQuirks(pass)
|
||||
|
||||
return pass
|
||||
}
|
||||
|
||||
private fun getField(jsonObject: JSONObject, key: String, translation: AppleStylePassTranslation): String? {
|
||||
if (jsonObject.has(key)) {
|
||||
try {
|
||||
return translation.translate(jsonObject.getString(key))
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun addFields(list: ArrayList<PassField>, type_json: JSONObject, fieldsName: String, translation: AppleStylePassTranslation, hide: Boolean = false) {
|
||||
try {
|
||||
val jsonArray = type_json.getJSONArray(fieldsName)
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
try {
|
||||
val jsonObject = jsonArray.getJSONObject(i)
|
||||
val field = PassField(key = getField(jsonObject, "key", translation),
|
||||
label = getField(jsonObject, "label", translation),
|
||||
value = getField(jsonObject, "value", translation),
|
||||
hide = hide)
|
||||
list.add(field)
|
||||
|
||||
} catch (e: JSONException) {
|
||||
Log.w("could not process PassField from JSON for $fieldsName cause: $e")
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
Log.w("could not process PassFields $fieldsName from JSON: $e")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun findLocalizedPath(path: File, language: String, tracker: Tracker): String? {
|
||||
val localized = File(path, "$language.lproj")
|
||||
|
||||
if (localized.exists() && localized.isDirectory) {
|
||||
tracker.trackEvent("measure_event", "pass", language + "_native_lproj", null)
|
||||
return localized.path
|
||||
}
|
||||
|
||||
val fallback = File(path, "en.lproj")
|
||||
|
||||
if (fallback.exists() && fallback.isDirectory) {
|
||||
tracker.trackEvent("measure_event", "pass", "en_lproj", null)
|
||||
return fallback.path
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
internal interface JsonStringReadCallback {
|
||||
fun onString(string: String)
|
||||
}
|
||||
|
||||
private fun readJsonSafeAsOptional(json: JSONObject, key: String): String? {
|
||||
if (json.has(key)) {
|
||||
try {
|
||||
return json.getString(key)
|
||||
} catch (e: JSONException) {
|
||||
// some passes just do not have the field
|
||||
}
|
||||
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun readJsonSafe(json: JSONObject, key: String, callback: JsonStringReadCallback) {
|
||||
if (json.has(key)) {
|
||||
try {
|
||||
callback.onString(json.getString(key))
|
||||
} catch (e: JSONException) {
|
||||
// some passes just do not have the field
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyBitmapFile(path: File, localizedPath: String?, bitmapString: String) {
|
||||
val bitmap = findBitmap(path, localizedPath, bitmapString)
|
||||
if (bitmap != null) {
|
||||
try {
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, FileOutputStream(File(path, bitmapString + PassImpl.FILETYPE_IMAGES)))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findBitmap(path: File, localizedPath: String?, bitmap: String): Bitmap? {
|
||||
|
||||
val searchList = ArrayList<File>()
|
||||
if (localizedPath != null) {
|
||||
searchList.add(File(localizedPath, "$bitmap@2x.png"))
|
||||
searchList.add(File(localizedPath, "$bitmap.png"))
|
||||
}
|
||||
|
||||
searchList.add((File(path, "$bitmap@2x.png")))
|
||||
searchList.add((File(path, "$bitmap@2x.png")))
|
||||
|
||||
for (current in searchList) {
|
||||
|
||||
var res: Bitmap? = null
|
||||
|
||||
try {
|
||||
res = BitmapFactory.decodeFile(current.absolutePath)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
System.gc()
|
||||
try {
|
||||
res = BitmapFactory.decodeFile(current.absolutePath)
|
||||
} catch (e: OutOfMemoryError) {
|
||||
}
|
||||
}
|
||||
|
||||
if (res != null) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package org.ligi.passandroid.reader
|
||||
|
||||
import org.json.JSONObject
|
||||
|
||||
|
||||
fun JSONObject.getBarcodeJson(): JSONObject? {
|
||||
if (has("barcode")) {
|
||||
return getJSONObject("barcode")
|
||||
}
|
||||
|
||||
if (has("barcodes")) {
|
||||
|
||||
getJSONArray("barcodes").let {
|
||||
if (length() > 0) {
|
||||
return it.getJSONObject(0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
package org.ligi.passandroid.reader
|
||||
|
||||
import android.graphics.Color
|
||||
import org.ligi.passandroid.functions.readJSONSafely
|
||||
import org.ligi.passandroid.model.PassDefinitions
|
||||
import org.ligi.passandroid.model.pass.BarCode
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import org.ligi.passandroid.model.pass.PassImpl
|
||||
import org.ligi.passandroid.model.pass.PassType
|
||||
import org.ligi.tracedroid.logging.Log
|
||||
import org.threeten.bp.ZonedDateTime
|
||||
import java.io.File
|
||||
|
||||
object PassReader {
|
||||
|
||||
fun read(path: File): Pass {
|
||||
|
||||
val pass = PassImpl(path.name)
|
||||
|
||||
val file = File(path, "data.json")
|
||||
|
||||
try {
|
||||
val plainJsonString = file.bufferedReader().readText()
|
||||
val passJSON = readJSONSafely(plainJsonString)!!
|
||||
|
||||
if (passJSON.has("what")) {
|
||||
val whatJSON = passJSON.getJSONObject("what")
|
||||
pass.description = whatJSON.getString("description")
|
||||
}
|
||||
|
||||
if (passJSON.has("meta")) {
|
||||
val metaJSON = passJSON.getJSONObject("meta")
|
||||
pass.type = PassDefinitions.NAME_TO_TYPE[metaJSON.getString("type")] ?: PassType.GENERIC
|
||||
pass.creator = metaJSON.getString("organisation")
|
||||
pass.app = metaJSON.getString("app")
|
||||
}
|
||||
|
||||
if (passJSON.has("ui")) {
|
||||
val uiJSON = passJSON.getJSONObject("ui")
|
||||
pass.accentColor = Color.parseColor(uiJSON.getString("bgColor"))
|
||||
}
|
||||
|
||||
if (passJSON.has("barcode")) {
|
||||
val barcodeJSON = passJSON.getJSONObject("barcode")
|
||||
val barcodeFormatString = barcodeJSON.getString("type")
|
||||
|
||||
val barcodeFormat = BarCode.getFormatFromString(barcodeFormatString)
|
||||
val barCode = BarCode(barcodeFormat, barcodeJSON.getString("message"))
|
||||
pass.barCode = barCode
|
||||
|
||||
if (barcodeJSON.has("altText")) {
|
||||
barCode.alternativeText = barcodeJSON.getString("altText")
|
||||
}
|
||||
}
|
||||
|
||||
if (passJSON.has("when")) {
|
||||
val dateTime = passJSON.getJSONObject("when").getString("dateTime")
|
||||
|
||||
pass.calendarTimespan = PassImpl.TimeSpan()
|
||||
pass.calendarTimespan = PassImpl.TimeSpan(from = ZonedDateTime.parse(dateTime))
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.i("PassParse Exception: $e")
|
||||
}
|
||||
|
||||
return pass
|
||||
}
|
||||
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package org.ligi.passandroid.scan
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View.GONE
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.android.synthetic.main.activity_scan.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.ligi.passandroid.R
|
||||
import org.ligi.passandroid.scan.events.DirectoryProcessed
|
||||
import org.ligi.passandroid.scan.events.PassScanEventChannelProvider
|
||||
import org.ligi.passandroid.scan.events.ScanFinished
|
||||
import org.ligi.passandroid.ui.PassAndroidActivity
|
||||
|
||||
class PassScanActivity : PassAndroidActivity() {
|
||||
|
||||
private val progressChannelProvider: PassScanEventChannelProvider by inject()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_scan)
|
||||
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
lifecycleScope.launch {
|
||||
for (event in progressChannelProvider.channel.openSubscription()) {
|
||||
when (event) {
|
||||
is DirectoryProcessed -> progress_text.text = event.dir
|
||||
is ScanFinished -> {
|
||||
progress_container.visibility = GONE
|
||||
val message = getString(R.string.scan_finished_dialog_text, event.foundPasses.size)
|
||||
AlertDialog.Builder(this@PassScanActivity)
|
||||
.setTitle(R.string.scan_finished_dialog_title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
val intent = Intent(this, SearchPassesIntentService::class.java)
|
||||
startService(intent)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
finish()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
package org.ligi.passandroid.scan
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.ligi.passandroid.R
|
||||
import org.ligi.passandroid.Tracker
|
||||
import org.ligi.passandroid.functions.fromURI
|
||||
import org.ligi.passandroid.model.PassStore
|
||||
import org.ligi.passandroid.model.PastLocationsStore
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import org.ligi.passandroid.scan.events.DirectoryProcessed
|
||||
import org.ligi.passandroid.scan.events.PassScanEventChannelProvider
|
||||
import org.ligi.passandroid.scan.events.ScanFinished
|
||||
import org.ligi.passandroid.ui.PassListActivity
|
||||
import org.ligi.passandroid.ui.UnzipPassController
|
||||
import org.ligi.passandroid.ui.UnzipPassController.InputStreamUnzipControllerSpec
|
||||
import org.ligi.tracedroid.logging.Log
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
private const val NOTIFICATION_CHANNEL_ID = "transactions"
|
||||
|
||||
class SearchPassesIntentService : LifecycleService() {
|
||||
|
||||
private val notifyManager by lazy {
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
}
|
||||
|
||||
private var shouldFinish: Boolean = false
|
||||
private var progressNotificationBuilder: NotificationCompat.Builder? = null
|
||||
private var findNotificationBuilder: NotificationCompat.Builder? = null
|
||||
|
||||
private var foundList = ArrayList<Pass>()
|
||||
|
||||
private var lastProgressUpdate: Long = 0
|
||||
|
||||
private val passStore: PassStore by inject()
|
||||
private val tracker: Tracker by inject()
|
||||
private val progressChannelProvider: PassScanEventChannelProvider by inject()
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
lifecycleScope.launch {
|
||||
foundList.clear()
|
||||
|
||||
if (Build.VERSION.SDK_INT > 25) {
|
||||
|
||||
val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, "PassAndroid Pass scan", NotificationManager.IMPORTANCE_DEFAULT)
|
||||
channel.description = "Notifications when PassAndroid is scanning for passes"
|
||||
notifyManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(applicationContext, 1, Intent(baseContext, PassListActivity::class.java), 0)
|
||||
progressNotificationBuilder = NotificationCompat.Builder(this@SearchPassesIntentService, NOTIFICATION_CHANNEL_ID).setContentTitle(getString(R.string.scanning_for_passes))
|
||||
.setSmallIcon(R.drawable.ic_refresh)
|
||||
.setOngoing(true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setProgress(1, 1, true)
|
||||
|
||||
findNotificationBuilder = NotificationCompat.Builder(this@SearchPassesIntentService, NOTIFICATION_CHANNEL_ID)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
for (path in PastLocationsStore(preferences, tracker).locations) {
|
||||
searchIn(File(path), false)
|
||||
}
|
||||
|
||||
// note to future_me: yea one thinks we only need to search root here, but root was /system for me and so
|
||||
// did not contain "/SDCARD" #dontoptimize
|
||||
// on my phone:
|
||||
|
||||
// | /mnt/sdcard/Download << this looks kind of stupid as we do /mnt/sdcard later and hence will go here twice
|
||||
// but this helps finding passes in Downloads ( where they are very often ) fast - some users with lots of files on the SDCard gave
|
||||
// up the refreshing of passes as it took so long to traverse all files on the SDCard
|
||||
// one could think about not going there anymore but a short look at this showed that it seems cost more time to check than what it gains
|
||||
// in download there are mostly single files in a flat dir - no huge tree behind this imho
|
||||
searchIn(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), true)
|
||||
|
||||
// | /system
|
||||
searchIn(Environment.getRootDirectory(), true)
|
||||
|
||||
// | /mnt/sdcard
|
||||
searchIn(Environment.getExternalStorageDirectory(), true)
|
||||
|
||||
// | /cache
|
||||
searchIn(Environment.getDownloadCacheDirectory(), true)
|
||||
|
||||
// | /data
|
||||
searchIn(Environment.getDataDirectory(), true)
|
||||
notifyManager.cancel(PROGRESS_NOTIFICATION_ID)
|
||||
|
||||
progressChannelProvider.channel.send(ScanFinished(foundList))
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
/**
|
||||
* recursive voyage starting at path to find files named .pkpass
|
||||
*/
|
||||
private suspend fun searchIn(path: File, recursive: Boolean) {
|
||||
|
||||
if (System.currentTimeMillis() - lastProgressUpdate > 1000) {
|
||||
lastProgressUpdate = System.currentTimeMillis()
|
||||
val msg = path.toString()
|
||||
progressChannelProvider.channel.send(DirectoryProcessed(msg))
|
||||
progressNotificationBuilder!!.setContentText(msg)
|
||||
notifyManager.notify(PROGRESS_NOTIFICATION_ID, progressNotificationBuilder!!.build())
|
||||
}
|
||||
|
||||
val files = path.listFiles()
|
||||
|
||||
if (files == null || files.isEmpty()) {
|
||||
// no files here
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
for (file in files) {
|
||||
if (shouldFinish) {
|
||||
return
|
||||
}
|
||||
Log.i("search " + file.absoluteFile)
|
||||
if (recursive && file.isDirectory) {
|
||||
searchIn(file, true)
|
||||
} else if (file.name.toLowerCase().endsWith(".pkpass") || file.name.toLowerCase().endsWith(".espass")) {
|
||||
Log.i("found" + file.absolutePath)
|
||||
|
||||
try {
|
||||
val ins = fromURI(baseContext, Uri.parse("file://" + file.absolutePath), tracker)
|
||||
val onSuccessCallback = SearchSuccessCallback(baseContext,
|
||||
passStore,
|
||||
foundList,
|
||||
findNotificationBuilder!!,
|
||||
file,
|
||||
notifyManager)
|
||||
val spec = InputStreamUnzipControllerSpec(ins!!,
|
||||
baseContext,
|
||||
passStore,
|
||||
onSuccessCallback,
|
||||
object : UnzipPassController.FailCallback {
|
||||
override fun fail(reason: String) {
|
||||
Log.i("fail", reason)
|
||||
}
|
||||
})
|
||||
UnzipPassController.processInputStream(spec)
|
||||
} catch (e: Exception) {
|
||||
tracker.trackException("Error in SearchPassesIntentService", e, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
shouldFinish = true
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PROGRESS_NOTIFICATION_ID = 1
|
||||
const val FOUND_NOTIFICATION_ID = 2
|
||||
const val REQUEST_CODE = 1
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
package org.ligi.passandroid.scan
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import org.ligi.passandroid.R
|
||||
import org.ligi.passandroid.model.PassBitmapDefinitions
|
||||
import org.ligi.passandroid.model.PassStore
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
import org.ligi.passandroid.scan.SearchPassesIntentService
|
||||
import org.ligi.passandroid.ui.PassViewActivity
|
||||
import org.ligi.passandroid.ui.PassViewActivityBase
|
||||
import org.ligi.passandroid.ui.UnzipPassController
|
||||
import org.threeten.bp.ZonedDateTime
|
||||
import java.io.File
|
||||
|
||||
internal class SearchSuccessCallback(private val context: Context, private val passStore: PassStore, private val foundList: MutableList<Pass>, private val findNotificationBuilder: NotificationCompat.Builder, private val file: File, private val notifyManager: NotificationManager) : UnzipPassController.SuccessCallback {
|
||||
|
||||
override fun call(uuid: String) {
|
||||
|
||||
val pass = passStore.getPassbookForId(uuid)
|
||||
|
||||
val isDuplicate = foundList.any { it.id == uuid }
|
||||
if (pass != null && !isDuplicate) {
|
||||
foundList.add(pass)
|
||||
val iconBitmap = pass.getBitmap(passStore, PassBitmapDefinitions.BITMAP_ICON)
|
||||
passStore.classifier.moveToTopic(pass, getInitialTopic(pass))
|
||||
if (iconBitmap != null) {
|
||||
val bitmap = scale2maxSize(iconBitmap, context.resources.getDimensionPixelSize(R.dimen.finger))
|
||||
findNotificationBuilder.setLargeIcon(bitmap)
|
||||
}
|
||||
|
||||
val foundString = context.getString(R.string.found_pass, pass.description)
|
||||
findNotificationBuilder.setContentTitle(foundString)
|
||||
|
||||
if (foundList.size > 1) {
|
||||
val foundMoreString = context.getString(R.string.found__pass, foundList.size - 1)
|
||||
findNotificationBuilder.setContentText(foundMoreString)
|
||||
} else {
|
||||
findNotificationBuilder.setContentText(file.absolutePath)
|
||||
}
|
||||
|
||||
val intent = Intent(context, PassViewActivity::class.java)
|
||||
intent.putExtra(PassViewActivityBase.EXTRA_KEY_UUID, uuid)
|
||||
findNotificationBuilder.setContentIntent(PendingIntent.getActivity(context, SearchPassesIntentService.REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
notifyManager.notify(SearchPassesIntentService.FOUND_NOTIFICATION_ID, findNotificationBuilder.build())
|
||||
}
|
||||
}
|
||||
|
||||
private fun scale2maxSize(bitmap: Bitmap, dimensionPixelSize: Int): Bitmap {
|
||||
val scale = dimensionPixelSize.toFloat() / if (bitmap.width > bitmap.height) bitmap.width else bitmap.height
|
||||
return Bitmap.createScaledBitmap(bitmap, (bitmap.width * scale).toInt(), (bitmap.height * scale).toInt(), false)
|
||||
}
|
||||
|
||||
private fun getInitialTopic(pass: Pass): String {
|
||||
val passDate = getDateOfPassForComparison(pass)
|
||||
if (passDate != null && passDate.isBefore(ZonedDateTime.now())) {
|
||||
return context.getString(R.string.topic_archive)
|
||||
}
|
||||
return context.getString(R.string.topic_new)
|
||||
}
|
||||
|
||||
private fun getDateOfPassForComparison(pass: Pass): ZonedDateTime? {
|
||||
if (pass.calendarTimespan != null && pass.calendarTimespan!!.from != null) {
|
||||
return pass.calendarTimespan!!.from
|
||||
} else if (pass.validTimespans != null && pass.validTimespans!!.isNotEmpty() && pass.validTimespans!![0].to != null) {
|
||||
return pass.validTimespans!![0].to
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package org.ligi.passandroid.scan.events
|
||||
|
||||
import org.ligi.passandroid.model.pass.Pass
|
||||
|
||||
sealed class PassScanEvent
|
||||
|
||||
data class DirectoryProcessed(val dir: String) : PassScanEvent()
|
||||
data class ScanFinished(val foundPasses: List<Pass>) : PassScanEvent()
|
|
@ -1,8 +0,0 @@
|
|||
package org.ligi.passandroid.scan.events
|
||||
|
||||
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||
import org.ligi.passandroid.scan.events.PassScanEvent
|
||||
|
||||
class PassScanEventChannelProvider {
|
||||
val channel = ConflatedBroadcastChannel<PassScanEvent>()
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue