Compare commits

..

2 commits

Author SHA1 Message Date
ligi
22433427bf remove connection change from here 2014-07-11 21:36:14 +02:00
ligi
b75fd52909 prepare widget 2014-07-11 21:06:35 +02:00
434 changed files with 5967 additions and 18742 deletions

View file

@ -1,22 +0,0 @@
{
"type": "android",
"stages": [
{
"name": "testWithMapsWithAnalyticsForPlayDebugComposer",
"needsEmulator": true
},
{
"name": "lint",
"needsEmulator": false
},
{
"name": "test",
"needsEmulator": false
},
{
"name": "assembleRelease",
"needsEmulator": false
}
]
}

View file

@ -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
View file

@ -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']

View file

@ -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.

View file

@ -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
View file

@ -4,7 +4,7 @@ build
#assets
bin
gen
proguard-project.txt
project.properties
gradle.properties
local.properties
*iml

View file

@ -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
View file

@ -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'
}

View file

@ -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.

View file

@ -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
}

View file

@ -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

View file

@ -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"
}
}

View file

@ -1,9 +0,0 @@
package org.ligi.passandroid
import org.ligi.trulesk.AppReplacingRunnerBase
class AppReplacingRunner : AppReplacingRunnerBase() {
override fun testAppClass() = TestApp::class.java
}

View file

@ -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
}
}

View file

@ -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())))
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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")
}
}

View file

@ -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()
}
}

View file

@ -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 */
}

View file

@ -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)
}
}

View file

@ -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()))
}
}

View file

@ -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))
}
}

View file

@ -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))
}
}

View file

@ -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)))
}
}
}

View file

@ -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)
}

View file

@ -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())))
}
}

View file

@ -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")
}
}

View file

@ -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")
}
}

View file

@ -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")
})
}
}
}

View file

@ -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")
}
}
}

View file

@ -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);
}
}
}

View file

@ -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
}

View file

@ -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!!
}
}

View file

@ -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()))
}

View file

@ -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>

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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;
}
}

View file

@ -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.
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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";
}
}
}

View file

@ -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

View file

@ -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);
}

View file

@ -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")
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}

View file

@ -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)
}
}

View file

@ -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)!!

View file

@ -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
}

View file

@ -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))

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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;
}
}
}

View file

@ -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();
}
}
}
}
}

View file

@ -1,5 +0,0 @@
package org.ligi.passandroid.model
import java.io.InputStream
class InputStreamWithSource(val source: String, val inputStream: InputStream)

View file

@ -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";
}

View file

@ -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)
}
}

View file

@ -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 }
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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?
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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"
}
}

View file

@ -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"
}

View file

@ -1,10 +0,0 @@
package org.ligi.passandroid.model.pass
enum class PassType {
GENERIC,
EVENT,
COUPON,
BOARDING,
LOYALTY,
VOUCHER
}

View file

@ -1,4 +0,0 @@
/**
* This is the root of the PassAndroid package
*/
@javax.annotation.ParametersAreNonnullByDefault package org.ligi.passandroid;

View file

@ -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()
}
}
}
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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()

View file

@ -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