From 3e99af10d9c4e088ba943d53302d5e7e04454f44 Mon Sep 17 00:00:00 2001 From: Eric Butler Date: Thu, 5 Feb 2026 08:53:57 -0500 Subject: [PATCH 001/119] Modernize build system: Gradle KTS, version catalog, Kotlin 2.3.0, AGP 9.0 Convert the entire build system from Groovy to Kotlin DSL. Add a Gradle version catalog (libs.versions.toml) for centralized dependency management. Update all dependencies to latest stable versions. Key version changes: - Kotlin: 2.3.0 - Android Gradle Plugin: 9.0.0 - Compose Multiplatform: 1.10.0 - kotlinx-serialization: 1.10.0 - kotlinx-coroutines: 1.10.2 - kotlinx-datetime: 0.7.1 - SQLDelight: 2.2.1 - Koin: 4.1.1 - Compile SDK: 35, Min SDK: 23 --- .gitignore | 11 + .gitmodules | 3 - build.gradle | 107 -------- build.gradle.kts | 67 +++++ dependencies.gradle | 47 ---- farebot-android/build.gradle.kts | 194 +++++++++++++++ farebot-app-persist/build.gradle | 25 -- farebot-app-persist/build.gradle.kts | 41 +++ farebot-base/build.gradle | 16 -- farebot-base/build.gradle.kts | 42 ++++ farebot-card-cepas/build.gradle | 20 -- farebot-card-cepas/build.gradle.kts | 24 ++ farebot-card-classic/build.gradle | 20 -- farebot-card-classic/build.gradle.kts | 31 +++ farebot-card-desfire/build.gradle | 20 -- farebot-card-desfire/build.gradle.kts | 24 ++ farebot-card-felica/build.gradle | 20 -- farebot-card-felica/build.gradle.kts | 28 +++ farebot-card-ultralight/build.gradle | 18 -- farebot-card-ultralight/build.gradle.kts | 28 +++ farebot-card/build.gradle | 16 -- farebot-card/build.gradle.kts | 24 ++ farebot-transit-bilhete/build.gradle | 17 -- farebot-transit-bilhete/build.gradle.kts | 29 +++ farebot-transit-clipper/build.gradle | 19 -- farebot-transit-clipper/build.gradle.kts | 31 +++ farebot-transit-easycard/build.gradle | 14 -- farebot-transit-easycard/build.gradle.kts | 31 +++ farebot-transit-edy/build.gradle | 17 -- farebot-transit-edy/build.gradle.kts | 29 +++ farebot-transit-ezlink/build.gradle | 19 -- farebot-transit-ezlink/build.gradle.kts | 30 +++ farebot-transit-hsl/build.gradle | 19 -- farebot-transit-hsl/build.gradle.kts | 29 +++ farebot-transit-kmt/build.gradle | 19 -- farebot-transit-kmt/build.gradle.kts | 30 +++ farebot-transit-manly/build.gradle | 17 -- farebot-transit-manly/build.gradle.kts | 29 +++ farebot-transit-myki/build.gradle | 19 -- farebot-transit-myki/build.gradle.kts | 29 +++ farebot-transit-octopus/build.gradle | 18 -- farebot-transit-octopus/build.gradle.kts | 29 +++ farebot-transit-opal/build.gradle | 21 -- farebot-transit-opal/build.gradle.kts | 29 +++ farebot-transit-orca/build.gradle | 21 -- farebot-transit-orca/build.gradle.kts | 30 +++ farebot-transit-ovc/build.gradle | 20 -- farebot-transit-ovc/build.gradle.kts | 30 +++ farebot-transit-seqgo/build.gradle | 21 -- farebot-transit-seqgo/build.gradle.kts | 30 +++ farebot-transit-stub/build.gradle | 21 -- farebot-transit-suica/build.gradle | 21 -- farebot-transit-suica/build.gradle.kts | 36 +++ farebot-transit/build.gradle | 17 -- farebot-transit/build.gradle.kts | 32 +++ gradle/libs.versions.toml | 87 +++++++ gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 46175 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 288 +++++++++++++--------- gradlew.bat | 63 +++-- settings.gradle | 29 --- settings.gradle.kts | 43 ++++ third_party/nfc-felica-lib | 1 - 63 files changed, 1332 insertions(+), 812 deletions(-) delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 dependencies.gradle create mode 100644 farebot-android/build.gradle.kts delete mode 100644 farebot-app-persist/build.gradle create mode 100644 farebot-app-persist/build.gradle.kts delete mode 100644 farebot-base/build.gradle create mode 100644 farebot-base/build.gradle.kts delete mode 100644 farebot-card-cepas/build.gradle create mode 100644 farebot-card-cepas/build.gradle.kts delete mode 100644 farebot-card-classic/build.gradle create mode 100644 farebot-card-classic/build.gradle.kts delete mode 100644 farebot-card-desfire/build.gradle create mode 100644 farebot-card-desfire/build.gradle.kts delete mode 100644 farebot-card-felica/build.gradle create mode 100644 farebot-card-felica/build.gradle.kts delete mode 100644 farebot-card-ultralight/build.gradle create mode 100644 farebot-card-ultralight/build.gradle.kts delete mode 100644 farebot-card/build.gradle create mode 100644 farebot-card/build.gradle.kts delete mode 100644 farebot-transit-bilhete/build.gradle create mode 100644 farebot-transit-bilhete/build.gradle.kts delete mode 100644 farebot-transit-clipper/build.gradle create mode 100644 farebot-transit-clipper/build.gradle.kts delete mode 100644 farebot-transit-easycard/build.gradle create mode 100644 farebot-transit-easycard/build.gradle.kts delete mode 100644 farebot-transit-edy/build.gradle create mode 100644 farebot-transit-edy/build.gradle.kts delete mode 100644 farebot-transit-ezlink/build.gradle create mode 100644 farebot-transit-ezlink/build.gradle.kts delete mode 100644 farebot-transit-hsl/build.gradle create mode 100644 farebot-transit-hsl/build.gradle.kts delete mode 100644 farebot-transit-kmt/build.gradle create mode 100644 farebot-transit-kmt/build.gradle.kts delete mode 100644 farebot-transit-manly/build.gradle create mode 100644 farebot-transit-manly/build.gradle.kts delete mode 100644 farebot-transit-myki/build.gradle create mode 100644 farebot-transit-myki/build.gradle.kts delete mode 100644 farebot-transit-octopus/build.gradle create mode 100644 farebot-transit-octopus/build.gradle.kts delete mode 100644 farebot-transit-opal/build.gradle create mode 100644 farebot-transit-opal/build.gradle.kts delete mode 100644 farebot-transit-orca/build.gradle create mode 100644 farebot-transit-orca/build.gradle.kts delete mode 100644 farebot-transit-ovc/build.gradle create mode 100644 farebot-transit-ovc/build.gradle.kts delete mode 100644 farebot-transit-seqgo/build.gradle create mode 100644 farebot-transit-seqgo/build.gradle.kts delete mode 100644 farebot-transit-stub/build.gradle delete mode 100644 farebot-transit-suica/build.gradle create mode 100644 farebot-transit-suica/build.gradle.kts delete mode 100644 farebot-transit/build.gradle create mode 100644 farebot-transit/build.gradle.kts create mode 100644 gradle/libs.versions.toml delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts delete mode 160000 third_party/nfc-felica-lib diff --git a/.gitignore b/.gitignore index 2cb544944..b08ea09f7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,18 @@ local.properties projectFilesBackup/ project.properties .gradle +.kotlin release.keystore com_crashlytics_export_strings.xml crashlytics-build.properties crashlytics.properties + +# Xcode +xcuserdata/ +*.xcscmblueprint +DerivedData/ + +metrodroid/ +metrodroid-commits/ + +*.hprof diff --git a/.gitmodules b/.gitmodules index 96a0adfcf..e69de29bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "third_party/nfc-felica-lib"] - path = third_party/nfc-felica-lib - url = https://github.com/codebutler/nfc-felica-lib.git diff --git a/build.gradle b/build.gradle deleted file mode 100644 index cdd056e0f..000000000 --- a/build.gradle +++ /dev/null @@ -1,107 +0,0 @@ -buildscript { - repositories { - mavenLocal() - jcenter() - maven { url 'https://maven.google.com' } - maven { url 'https://maven.fabric.io/public' } - google() - } - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0-alpha13' - classpath 'io.fabric.tools:gradle:1.28.1' - classpath 'com.squareup.sqldelight:gradle-plugin:1.1.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.31" - } -} - -plugins { - id 'com.github.ben-manes.versions' version '0.21.0' -} - -allprojects { - repositories { - mavenLocal() - jcenter() - maven { url "https://maven.google.com" } - maven { url 'https://maven.fabric.io/public' } - } -} - -apply from: 'dependencies.gradle' - -subprojects { - apply plugin: 'checkstyle' - - dependencies { - checkstyle libs.checkstyle - } - - afterEvaluate {project -> - if (project.name.contains('farebot')) { - check.dependsOn 'checkstyle' - task checkstyle(type: Checkstyle) { - configFile file('config/checkstyle/checkstyle.xml') - source 'src' - include '**/*.java' - exclude '**/gen/**' - exclude '**/IOUtils.java' - exclude '**/Charsets.java' - classpath = files() - } - checkstyle { - ignoreFailures = false - } - } - if (project.hasProperty("android")) { - android { - compileSdkVersion vers.compileSdkVersion - - defaultConfig { - minSdkVersion vers.minSdkVersion - targetSdkVersion vers.targetSdkVersion - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 - } - - lintOptions { - abortOnError true - disable 'InvalidPackage','MissingTranslation' - } - - dexOptions { - dexInProcess = true - } - } - } - } -} - -dependencyUpdates.resolutionStrategy = { - componentSelection { rules -> - rules.all { ComponentSelection selection -> - boolean rejected = ['alpha', 'beta', 'rc', 'cr', 'm'].any { qualifier -> - selection.candidate.version ==~ /(?i).*[.-]${qualifier}[.\d-]*/ - } - if (rejected) { - selection.reject('Release candidate') - } - } - } -} - -configurations { - ktlint -} - -dependencies { - ktlint 'com.github.shyiko:ktlint:0.31.0' -} - -task lintKotlin(type: JavaExec) { - main = "com.github.shyiko.ktlint.Main" - classpath = configurations.ktlint - args "*/src/**/*.kt" -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..b2a061d9c --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,67 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.android.kotlin.multiplatform.library) apply false + alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.compose.multiplatform) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.sqldelight) apply false +} + +subprojects { + apply(plugin = "checkstyle") + + dependencies { + "checkstyle"(rootProject.libs.checkstyle) + } + + plugins.withId("org.jetbrains.kotlin.multiplatform") { + extensions.configure { + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + } + + configurations.configureEach { + resolutionStrategy { + force("com.google.errorprone:error_prone_annotations:2.28.0") + } + } + + afterEvaluate { + if (project.name.contains("farebot")) { + tasks.named("check") { + dependsOn("checkstyle") + } + tasks.register("checkstyle") { + configFile = file("config/checkstyle/checkstyle.xml") + source("src") + include("**/*.java") + exclude("**/gen/**") + classpath = files() + } + extensions.findByType()?.apply { + isIgnoreFailures = false + } + } + } + + plugins.withType { + @Suppress("DEPRECATION") + extensions.findByType()?.apply { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + lintOptions { + isAbortOnError = true + disable("InvalidPackage", "MissingTranslation") + } + } + } +} diff --git a/dependencies.gradle b/dependencies.gradle deleted file mode 100644 index 387e6d1c7..000000000 --- a/dependencies.gradle +++ /dev/null @@ -1,47 +0,0 @@ -ext { - vers = [ - compileSdkVersion: 28, - targetSdkVersion: 28, - minSdkVersion: 21 - ] - - def autoDisposeVersion = '0.8.0' - def autoValueGsonVersion = '0.8.0' - def autoValueVersion = '1.6.5' - def daggerVersion = '2.22.1' - def groupieVersion = '2.3.0' - def kotlinVersion = '1.3.31' - def magellanVersion = '1.1.0' - def roomVersion = '2.1.0-alpha07' - - libs = [ - autoDispose: "com.uber.autodispose:autodispose:${autoDisposeVersion}", - autoDisposeAndroid: "com.uber.autodispose:autodispose-android:${autoDisposeVersion}", - autoDisposeAndroidKotlin: "com.uber.autodispose:autodispose-android-kotlin:${autoDisposeVersion}", - autoDisposeKotlin: "com.uber.autodispose:autodispose-kotlin:${autoDisposeVersion}", - autoValue: "com.google.auto.value:auto-value:${autoValueVersion}", - autoValueAnnotations: "com.google.auto.value:auto-value-annotations:${autoValueVersion}", - autoValueGson: "com.ryanharter.auto.value:auto-value-gson:${autoValueGsonVersion}", - autoValueGsonAnnotations: "com.ryanharter.auto.value:auto-value-gson-annotations:${autoValueGsonVersion}", - checkstyle: 'com.puppycrawl.tools:checkstyle:8.20', - crashlytics: 'com.crashlytics.sdk.android:crashlytics:2.10.0', - dagger: "com.google.dagger:dagger:${daggerVersion}", - daggerCompiler: "com.google.dagger:dagger-compiler:${daggerVersion}", - groupie: "com.xwray:groupie:${groupieVersion}", - groupieDatabinding: "com.xwray:groupie-databinding:${groupieVersion}", - gson: 'com.google.code.gson:gson:2.8.5', - guava: 'com.google.guava:guava:27.1-android', - kotlinStdlib: "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVersion}", - magellan: "com.wealthfront:magellan:${magellanVersion}", - playServicesMaps: 'com.google.android.gms:play-services-maps:16.1.0', - roomRuntime: "androidx.room:room-runtime:${roomVersion}", - roomCompiler: "androidx.room:room-compiler:${roomVersion}", - rxBroadcast: 'com.cantrowitz:rxbroadcast:2.0.0', - rxJava2: 'io.reactivex.rxjava2:rxjava:2.2.8', - rxRelay2: 'com.jakewharton.rxrelay2:rxrelay:2.1.0', - supportDesign: "com.google.android.material:material:1.0.0", - supportV4: "androidx.legacy:legacy-support-v4:1.0.0", - supportV7CardView: "androidx.cardview:cardview:1.0.0", - supportV7RecyclerView: "androidx.recyclerview:recyclerview:1.0.0" - ] -} diff --git a/farebot-android/build.gradle.kts b/farebot-android/build.gradle.kts new file mode 100644 index 000000000..b56c51ade --- /dev/null +++ b/farebot-android/build.gradle.kts @@ -0,0 +1,194 @@ +/* + * build.gradle.kts + * + * This file is part of FareBot. + * Learn more at: https://codebutler.github.io/farebot/ + * + * Copyright (C) 2017 Eric Butler + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) +} + +dependencies { + implementation(project(":farebot-base")) + implementation(project(":farebot-app-persist")) + implementation(project(":farebot-card")) + implementation(project(":farebot-card-cepas")) + implementation(project(":farebot-card-classic")) + implementation(project(":farebot-card-desfire")) + implementation(project(":farebot-card-felica")) + implementation(project(":farebot-card-ultralight")) + implementation(project(":farebot-card-ksx6924")) + implementation(project(":farebot-card-china")) + implementation(project(":farebot-transit")) + implementation(project(":farebot-transit-china")) + implementation(project(":farebot-transit-bilhete")) + implementation(project(":farebot-transit-bip")) + implementation(project(":farebot-transit-clipper")) + implementation(project(":farebot-transit-easycard")) + implementation(project(":farebot-transit-edy")) + implementation(project(":farebot-transit-kmt")) + implementation(project(":farebot-transit-ezlink")) + implementation(project(":farebot-transit-hsl")) + implementation(project(":farebot-transit-manly")) + implementation(project(":farebot-transit-myki")) + implementation(project(":farebot-transit-octopus")) + implementation(project(":farebot-transit-opal")) + implementation(project(":farebot-transit-orca")) + implementation(project(":farebot-transit-ovc")) + implementation(project(":farebot-transit-seqgo")) + implementation(project(":farebot-transit-suica")) + implementation(project(":farebot-transit-nextfareul")) + implementation(project(":farebot-transit-ventra")) + implementation(project(":farebot-transit-yvr-compass")) + implementation(project(":farebot-transit-troika")) + implementation(project(":farebot-transit-oyster")) + implementation(project(":farebot-transit-charlie")) + implementation(project(":farebot-transit-gautrain")) + implementation(project(":farebot-transit-smartrider")) + implementation(project(":farebot-transit-podorozhnik")) + implementation(project(":farebot-transit-touchngo")) + implementation(project(":farebot-transit-tfi-leap")) + implementation(project(":farebot-transit-lax-tap")) + implementation(project(":farebot-transit-ricaricami")) + implementation(project(":farebot-transit-yargor")) + implementation(project(":farebot-transit-chc-metrocard")) + implementation(project(":farebot-transit-komuterlink")) + implementation(project(":farebot-transit-magnacarta")) + implementation(project(":farebot-transit-tampere")) + implementation(project(":farebot-transit-bonobus")) + implementation(project(":farebot-transit-cifial")) + implementation(project(":farebot-transit-hafilat")) + implementation(project(":farebot-transit-intercard")) + implementation(project(":farebot-transit-kazan")) + implementation(project(":farebot-transit-kiev")) + implementation(project(":farebot-transit-metromoney")) + implementation(project(":farebot-transit-metroq")) + implementation(project(":farebot-transit-otago")) + implementation(project(":farebot-transit-pilet")) + implementation(project(":farebot-transit-selecta")) + implementation(project(":farebot-transit-umarsh")) + implementation(project(":farebot-transit-warsaw")) + implementation(project(":farebot-transit-zolotayakorona")) + implementation(project(":farebot-transit-serialonly")) + implementation(project(":farebot-transit-tmoney")) + implementation(project(":farebot-transit-krocap")) + implementation(project(":farebot-transit-ndef")) + implementation(project(":farebot-transit-unknown")) + implementation(project(":farebot-shared")) + + implementation(libs.kotlinx.serialization.json) + implementation(libs.sqldelight.android.driver) + implementation(libs.guava) + implementation(libs.kotlin.stdlib) + implementation(libs.play.services.maps) + implementation(libs.material) + implementation(libs.appcompat) + + // Koin + implementation(libs.koin.android) + + // Compose + implementation(platform(libs.compose.bom)) + implementation("androidx.compose.ui:ui") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.compose.foundation:foundation") + implementation("androidx.compose.ui:ui-tooling-preview") + debugImplementation("androidx.compose.ui:ui-tooling") + + // Navigation Compose + implementation(libs.navigation.compose) + + // Activity Compose + implementation(libs.activity.compose) + + // Lifecycle + implementation(libs.lifecycle.viewmodel.compose) + implementation(libs.lifecycle.runtime.compose) + + // Coroutines + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + + // Compose Multiplatform Resources (needed for StringResource interface) + implementation(libs.compose.resources) +} + +fun askPassword(): String { + return Runtime.getRuntime().exec(arrayOf("security", "-q", "find-generic-password", "-w", "-g", "-l", "farebot-release")) + .inputStream.bufferedReader().readText().trim() +} + +gradle.taskGraph.whenReady { + if (hasTask(":farebot-android:packageRelease")) { + val password = askPassword() + android.signingConfigs.getByName("release").storePassword = password + android.signingConfigs.getByName("release").keyPassword = password + } +} + +android { + compileSdk = libs.versions.compileSdk.get().toInt() + namespace = "com.codebutler.farebot" + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = 29 + versionName = "3.1.1" + multiDexEnabled = true + } + + signingConfigs { + getByName("debug") { + storeFile = file("../debug.keystore") + } + create("release") { + storeFile = file("../release.keystore") + keyAlias = "ericbutler" + storePassword = "" + keyPassword = "" + } + } + + buildTypes { + debug { + } + release { + isShrinkResources = false + isMinifyEnabled = true + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "../config/proguard/proguard-rules.pro") + signingConfig = signingConfigs.getByName("release") + } + } + + packaging { + resources { + excludes += listOf("META-INF/LICENSE.txt", "META-INF/NOTICE.txt") + } + } + + buildFeatures { + buildConfig = true + compose = true + } +} diff --git a/farebot-app-persist/build.gradle b/farebot-app-persist/build.gradle deleted file mode 100644 index c937344b8..000000000 --- a/farebot-app-persist/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' - -dependencies { - implementation libs.supportV4 - implementation libs.guava - implementation libs.gson - implementation libs.kotlinStdlib - - api libs.roomRuntime - kapt libs.roomCompiler - - implementation project(path: ':farebot-card') -} - -android { - defaultConfig { - kapt { - arguments { - arg("room.schemaLocation", "$projectDir/schemas".toString()) - } - } - } -} diff --git a/farebot-app-persist/build.gradle.kts b/farebot-app-persist/build.gradle.kts new file mode 100644 index 000000000..249f2e06c --- /dev/null +++ b/farebot-app-persist/build.gradle.kts @@ -0,0 +1,41 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.sqldelight) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.app.persist" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.serialization.json) + implementation(libs.sqldelight.runtime) + implementation(project(":farebot-card")) + } + androidMain.dependencies { + implementation(libs.sqldelight.android.driver) + } + iosMain.dependencies { + implementation(libs.sqldelight.native.driver) + } + } +} + +sqldelight { + databases { + create("FareBotDb") { + packageName.set("com.codebutler.farebot.persist.db") + } + } +} diff --git a/farebot-base/build.gradle b/farebot-base/build.gradle deleted file mode 100644 index b63719d3c..000000000 --- a/farebot-base/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' - -dependencies { - implementation libs.guava - implementation libs.supportV4 - implementation libs.kotlinStdlib - - compileOnly libs.autoValueAnnotations - - kapt libs.autoValue - -} - -android { } diff --git a/farebot-base/build.gradle.kts b/farebot-base/build.gradle.kts new file mode 100644 index 000000000..1f68eb472 --- /dev/null +++ b/farebot-base/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.util" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonTest.dependencies { + implementation(kotlin("test")) + } + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.serialization.protobuf) + implementation(libs.kotlincrypto.hash.md) + api(libs.kotlinx.datetime) + api(libs.sqldelight.runtime) + } + androidMain.dependencies { + implementation(libs.sqldelight.android.driver) + } + iosMain.dependencies { + implementation(libs.sqldelight.native.driver) + } + } +} diff --git a/farebot-card-cepas/build.gradle b/farebot-card-cepas/build.gradle deleted file mode 100644 index 4e83163c9..000000000 --- a/farebot-card-cepas/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -apply plugin: 'com.android.library' - -dependencies { - implementation project(':farebot-card') - - implementation libs.guava - - compileOnly libs.autoValueAnnotations - compileOnly libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'cepas_' -} diff --git a/farebot-card-cepas/build.gradle.kts b/farebot-card-cepas/build.gradle.kts new file mode 100644 index 000000000..2e7373a8c --- /dev/null +++ b/farebot-card-cepas/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.card.cepas" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(project(":farebot-card")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-card-classic/build.gradle b/farebot-card-classic/build.gradle deleted file mode 100644 index 26154f9bf..000000000 --- a/farebot-card-classic/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -apply plugin: 'com.android.library' - -dependencies { - implementation project(':farebot-card') - - implementation libs.guava - - compileOnly libs.autoValueAnnotations - compileOnly libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'classic_' -} diff --git a/farebot-card-classic/build.gradle.kts b/farebot-card-classic/build.gradle.kts new file mode 100644 index 000000000..7d159b49f --- /dev/null +++ b/farebot-card-classic/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.card.classic" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonTest.dependencies { + implementation(kotlin("test")) + } + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-card")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-card-desfire/build.gradle b/farebot-card-desfire/build.gradle deleted file mode 100644 index 61ad4d235..000000000 --- a/farebot-card-desfire/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -apply plugin: 'com.android.library' - -dependencies { - implementation project(':farebot-card') - - implementation libs.guava - - compileOnly libs.autoValueAnnotations - compileOnly libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'desfire_' -} diff --git a/farebot-card-desfire/build.gradle.kts b/farebot-card-desfire/build.gradle.kts new file mode 100644 index 000000000..30826a882 --- /dev/null +++ b/farebot-card-desfire/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.card.desfire" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(project(":farebot-card")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-card-felica/build.gradle b/farebot-card-felica/build.gradle deleted file mode 100644 index c87ef40d8..000000000 --- a/farebot-card-felica/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -apply plugin: 'com.android.library' - -dependencies { - api project(':nfc-felica-lib') - - implementation project(':farebot-card') - - compileOnly libs.autoValueAnnotations - compileOnly libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'felica_' -} diff --git a/farebot-card-felica/build.gradle.kts b/farebot-card-felica/build.gradle.kts new file mode 100644 index 000000000..b9df17c0a --- /dev/null +++ b/farebot-card-felica/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.card.felica" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-card")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-card-ultralight/build.gradle b/farebot-card-ultralight/build.gradle deleted file mode 100644 index 5438c0110..000000000 --- a/farebot-card-ultralight/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -apply plugin: 'com.android.library' - -dependencies { - implementation project(':farebot-card') - - compileOnly libs.autoValueAnnotations - compileOnly libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'ultralight_' -} diff --git a/farebot-card-ultralight/build.gradle.kts b/farebot-card-ultralight/build.gradle.kts new file mode 100644 index 000000000..253076739 --- /dev/null +++ b/farebot-card-ultralight/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.card.ultralight" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-card")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-card/build.gradle b/farebot-card/build.gradle deleted file mode 100644 index 623797019..000000000 --- a/farebot-card/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -apply plugin: 'com.android.library' - -dependencies { - api project(':farebot-base') - - api libs.supportV4 - api libs.gson - - implementation libs.guava - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValue -} - -android { } diff --git a/farebot-card/build.gradle.kts b/farebot-card/build.gradle.kts new file mode 100644 index 000000000..52d25253f --- /dev/null +++ b/farebot-card/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.card" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + api(project(":farebot-base")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-bilhete/build.gradle b/farebot-transit-bilhete/build.gradle deleted file mode 100644 index 12e7aa562..000000000 --- a/farebot-transit-bilhete/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-classic') - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { } diff --git a/farebot-transit-bilhete/build.gradle.kts b/farebot-transit-bilhete/build.gradle.kts new file mode 100644 index 000000000..3b4873116 --- /dev/null +++ b/farebot-transit-bilhete/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.bilhete_unico" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-classic")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-clipper/build.gradle b/farebot-transit-clipper/build.gradle deleted file mode 100644 index 158cec8b4..000000000 --- a/farebot-transit-clipper/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-desfire') - - implementation libs.guava - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { } diff --git a/farebot-transit-clipper/build.gradle.kts b/farebot-transit-clipper/build.gradle.kts new file mode 100644 index 000000000..b0e839006 --- /dev/null +++ b/farebot-transit-clipper/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.clipper" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-base")) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-desfire")) + implementation(project(":farebot-card-ultralight")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-easycard/build.gradle b/farebot-transit-easycard/build.gradle deleted file mode 100644 index db0c03ad5..000000000 --- a/farebot-transit-easycard/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-classic') - - implementation libs.kotlinStdlib - implementation libs.guava -} - -android { - resourcePrefix 'easycard_' -} diff --git a/farebot-transit-easycard/build.gradle.kts b/farebot-transit-easycard/build.gradle.kts new file mode 100644 index 000000000..cbf63aaa6 --- /dev/null +++ b/farebot-transit-easycard/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.easycard" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-base")) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-classic")) + implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-edy/build.gradle b/farebot-transit-edy/build.gradle deleted file mode 100644 index 9a6b5c770..000000000 --- a/farebot-transit-edy/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-felica') - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { } diff --git a/farebot-transit-edy/build.gradle.kts b/farebot-transit-edy/build.gradle.kts new file mode 100644 index 000000000..134bdeabe --- /dev/null +++ b/farebot-transit-edy/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.edy" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-felica")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-ezlink/build.gradle b/farebot-transit-ezlink/build.gradle deleted file mode 100644 index ce4b7cc91..000000000 --- a/farebot-transit-ezlink/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-cepas') - - implementation libs.guava - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { } diff --git a/farebot-transit-ezlink/build.gradle.kts b/farebot-transit-ezlink/build.gradle.kts new file mode 100644 index 000000000..8c6e7826e --- /dev/null +++ b/farebot-transit-ezlink/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.ezlink" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-base")) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-cepas")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-hsl/build.gradle b/farebot-transit-hsl/build.gradle deleted file mode 100644 index 158cec8b4..000000000 --- a/farebot-transit-hsl/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-desfire') - - implementation libs.guava - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { } diff --git a/farebot-transit-hsl/build.gradle.kts b/farebot-transit-hsl/build.gradle.kts new file mode 100644 index 000000000..ba39e5730 --- /dev/null +++ b/farebot-transit-hsl/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.hsl" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-desfire")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-kmt/build.gradle b/farebot-transit-kmt/build.gradle deleted file mode 100644 index 6465e7b2e..000000000 --- a/farebot-transit-kmt/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-felica') - - implementation libs.guava - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { } diff --git a/farebot-transit-kmt/build.gradle.kts b/farebot-transit-kmt/build.gradle.kts new file mode 100644 index 000000000..a8c3a7dac --- /dev/null +++ b/farebot-transit-kmt/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.kmt" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-base")) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-felica")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-manly/build.gradle b/farebot-transit-manly/build.gradle deleted file mode 100644 index 12e7aa562..000000000 --- a/farebot-transit-manly/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-classic') - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { } diff --git a/farebot-transit-manly/build.gradle.kts b/farebot-transit-manly/build.gradle.kts new file mode 100644 index 000000000..2a5b5f680 --- /dev/null +++ b/farebot-transit-manly/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.manly_fast_ferry" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-classic")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-myki/build.gradle b/farebot-transit-myki/build.gradle deleted file mode 100644 index e11f3e00d..000000000 --- a/farebot-transit-myki/build.gradle +++ /dev/null @@ -1,19 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit-stub') - implementation project(':farebot-card-desfire') - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'myki_' -} diff --git a/farebot-transit-myki/build.gradle.kts b/farebot-transit-myki/build.gradle.kts new file mode 100644 index 000000000..fa7f3fef4 --- /dev/null +++ b/farebot-transit-myki/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.myki" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + api(project(":farebot-transit")) + implementation(project(":farebot-card-desfire")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-octopus/build.gradle b/farebot-transit-octopus/build.gradle deleted file mode 100644 index 3c1799744..000000000 --- a/farebot-transit-octopus/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -apply plugin: 'com.android.library' - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-felica') - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'octopus_' -} diff --git a/farebot-transit-octopus/build.gradle.kts b/farebot-transit-octopus/build.gradle.kts new file mode 100644 index 000000000..22423ef9e --- /dev/null +++ b/farebot-transit-octopus/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.octopus" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-felica")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-opal/build.gradle b/farebot-transit-opal/build.gradle deleted file mode 100644 index ccd45c927..000000000 --- a/farebot-transit-opal/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-desfire') - - implementation libs.guava - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'opal_' -} diff --git a/farebot-transit-opal/build.gradle.kts b/farebot-transit-opal/build.gradle.kts new file mode 100644 index 000000000..cc65aeda0 --- /dev/null +++ b/farebot-transit-opal/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.opal" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-desfire")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-orca/build.gradle b/farebot-transit-orca/build.gradle deleted file mode 100644 index fdbc9d202..000000000 --- a/farebot-transit-orca/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-desfire') - - implementation libs.guava - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'transit_orca_' -} diff --git a/farebot-transit-orca/build.gradle.kts b/farebot-transit-orca/build.gradle.kts new file mode 100644 index 000000000..de34fd1c4 --- /dev/null +++ b/farebot-transit-orca/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.orca" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-base")) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-desfire")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-ovc/build.gradle b/farebot-transit-ovc/build.gradle deleted file mode 100644 index 2715123af..000000000 --- a/farebot-transit-ovc/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit-stub') - implementation project(':farebot-card-classic') - implementation libs.guava - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'ovc_' -} diff --git a/farebot-transit-ovc/build.gradle.kts b/farebot-transit-ovc/build.gradle.kts new file mode 100644 index 000000000..a4ccbd2a7 --- /dev/null +++ b/farebot-transit-ovc/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.ovc" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-base")) + api(project(":farebot-transit")) + implementation(project(":farebot-card-classic")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-seqgo/build.gradle b/farebot-transit-seqgo/build.gradle deleted file mode 100644 index a33c9b01e..000000000 --- a/farebot-transit-seqgo/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-classic') - - implementation libs.guava - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'seqgo_' -} diff --git a/farebot-transit-seqgo/build.gradle.kts b/farebot-transit-seqgo/build.gradle.kts new file mode 100644 index 000000000..d9cf04b6b --- /dev/null +++ b/farebot-transit-seqgo/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.seq_go" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-base")) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-classic")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit-stub/build.gradle b/farebot-transit-stub/build.gradle deleted file mode 100644 index 56cf10695..000000000 --- a/farebot-transit-stub/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - api project(':farebot-transit') - - implementation project(':farebot-card-classic') - implementation project(':farebot-card-desfire') - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'stub_' -} diff --git a/farebot-transit-suica/build.gradle b/farebot-transit-suica/build.gradle deleted file mode 100644 index 084189c42..000000000 --- a/farebot-transit-suica/build.gradle +++ /dev/null @@ -1,21 +0,0 @@ -apply plugin: 'com.android.library' - - -dependencies { - implementation project(':farebot-transit') - implementation project(':farebot-card-felica') - - implementation libs.guava - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { - resourcePrefix 'suica_' -} diff --git a/farebot-transit-suica/build.gradle.kts b/farebot-transit-suica/build.gradle.kts new file mode 100644 index 000000000..8fe66b0ff --- /dev/null +++ b/farebot-transit-suica/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit.suica" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.datetime) + implementation(compose.components.resources) + implementation(project(":farebot-base")) + } + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + implementation(project(":farebot-base")) + implementation(project(":farebot-transit")) + implementation(project(":farebot-card-felica")) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/farebot-transit/build.gradle b/farebot-transit/build.gradle deleted file mode 100644 index d629609a8..000000000 --- a/farebot-transit/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -apply plugin: 'com.android.library' - -dependencies { - api project(':farebot-card') - - implementation libs.supportV4 - - compileOnly libs.autoValueAnnotations - - annotationProcessor libs.autoValueAnnotations - annotationProcessor libs.autoValueGsonAnnotations - - annotationProcessor libs.autoValue - annotationProcessor libs.autoValueGson -} - -android { } diff --git a/farebot-transit/build.gradle.kts b/farebot-transit/build.gradle.kts new file mode 100644 index 000000000..ac778ba12 --- /dev/null +++ b/farebot-transit/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.kotlin.compose) +} + +kotlin { + androidLibrary { + namespace = "com.codebutler.farebot.transit" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonTest.dependencies { + implementation(kotlin("test")) + } + commonMain.dependencies { + implementation(compose.components.resources) + implementation(compose.runtime) + api(project(":farebot-card")) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.datetime) + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..ab96562f1 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,87 @@ +[versions] +agp = "9.0.0" +kotlin = "2.3.0" +ksp = "2.3.0" +sqldelight = "2.2.1" +serialization = "1.10.0" +datetime = "0.7.1" +coroutines = "1.10.2" +compose-bom = "2026.01.01" +compose-multiplatform = "1.10.0" +lifecycle = "2.9.6" +navigation = "2.9.1" +activity = "1.10.1" +material = "1.13.0" +appcompat = "1.7.1" +kotlincrypto-hash = "0.8.0" +koin = "4.1.1" +checkstyle = "13.0.0" +guava = "33.5.0-android" +play-services-maps = "20.0.0" +maps-compose = "6.4.1" +compileSdk = "35" +targetSdk = "35" +minSdk = "23" + +[libraries] +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } +checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } +play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "play-services-maps" } +maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "maps-compose" } + +# SQLDelight +sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" } +sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } +sqldelight-native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" } + +# Material / AndroidX +material = { module = "com.google.android.material:material", version.ref = "material" } +appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } + +# KotlinCrypto +kotlincrypto-hash-md = { module = "org.kotlincrypto.hash:md", version.ref = "kotlincrypto-hash" } + +# Koin +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } +koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } +koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" } +koin-compose-viewmodel-navigation = { module = "io.insert-koin:koin-compose-viewmodel-navigation", version.ref = "koin" } + +# Compose BOM +compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" } +compose-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-multiplatform" } + +# Kotlinx Serialization +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } +kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization" } + +# Kotlinx Datetime +kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" } + +# Coroutines +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } + +# Lifecycle (JetBrains KMP versions) +lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } +lifecycle-runtime-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } + +# Navigation (JetBrains KMP version) +navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation" } + +# Activity +activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } +android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..61285a659d17295f1de7c53e24fdf13ad755c379 100644 GIT binary patch literal 46175 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

pVX@HRij*jw$g4?}Pn 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 literal 55616 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 \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,92 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 15e1ee37a..e509b2dd8 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -5,7 +5,7 @@ @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem -@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,10 +27,14 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -51,48 +57,35 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 92d49b32e..000000000 --- a/settings.gradle +++ /dev/null @@ -1,29 +0,0 @@ -include 'nfc-felica-lib' -project(':nfc-felica-lib').projectDir = new File('third_party/nfc-felica-lib') - -include ':farebot-base' -include ':farebot-card' -include ':farebot-card-cepas' -include ':farebot-card-classic' -include ':farebot-card-desfire' -include ':farebot-card-felica' -include ':farebot-card-ultralight' -include ':farebot-transit' -include ':farebot-transit-bilhete' -include ':farebot-transit-clipper' -include ':farebot-transit-easycard' -include ':farebot-transit-edy' -include ':farebot-transit-kmt' -include ':farebot-transit-ezlink' -include ':farebot-transit-hsl' -include ':farebot-transit-manly' -include ':farebot-transit-myki' -include ':farebot-transit-octopus' -include ':farebot-transit-opal' -include ':farebot-transit-orca' -include ':farebot-transit-ovc' -include ':farebot-transit-seqgo' -include ':farebot-transit-stub' -include ':farebot-transit-suica' -include ':farebot-app-persist' -include ':farebot-app' \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..62d2381f2 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,43 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode = RepositoriesMode.PREFER_SETTINGS + repositories { + mavenLocal() + google() + mavenCentral() + } +} + +include(":farebot-base") +include(":farebot-card") +include(":farebot-card-cepas") +include(":farebot-card-classic") +include(":farebot-card-desfire") +include(":farebot-card-felica") +include(":farebot-card-ultralight") +include(":farebot-transit") +include(":farebot-transit-bilhete") +include(":farebot-transit-clipper") +include(":farebot-transit-easycard") +include(":farebot-transit-edy") +include(":farebot-transit-kmt") +include(":farebot-transit-ezlink") +include(":farebot-transit-hsl") +include(":farebot-transit-manly") +include(":farebot-transit-myki") +include(":farebot-transit-octopus") +include(":farebot-transit-opal") +include(":farebot-transit-orca") +include(":farebot-transit-ovc") +include(":farebot-transit-seqgo") +include(":farebot-transit-stub") +include(":farebot-transit-suica") +include(":farebot-app-persist") +include(":farebot-android") diff --git a/third_party/nfc-felica-lib b/third_party/nfc-felica-lib deleted file mode 160000 index 2b4a4c9cb..000000000 --- a/third_party/nfc-felica-lib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2b4a4c9cb997c0e1be2fbbe2c0ca32d4b83a3474 From 7d2badd4041d29f12bf15d79f94265eb14e7b32c Mon Sep 17 00:00:00 2001 From: Eric Butler Date: Thu, 5 Feb 2026 08:53:57 -0500 Subject: [PATCH 002/119] Convert to Kotlin Multiplatform: core modules, abstractions, and app restructure Migrate all core modules to Kotlin Multiplatform with commonMain/androidMain/iosMain source sets. Convert all remaining Java files to Kotlin. Absorb nfc-felica-lib submodule into farebot-card-felica. Major changes: - farebot-app renamed to farebot-android - Replace Hilt DI with Koin (cross-platform) - Replace Gson with kotlinx.serialization - Replace custom ByteArray wrapper with kotlin.ByteArray + extension functions - Remove Room, use SQLDelight for cross-platform persistence - Add MDST (Metrodroid Station Data Table) reader for protobuf station databases - Add NFC abstraction layer for KMP readiness (CardTransceiver, NfcTechnology) - Add StringResource abstraction for cross-platform string formatting - Add TransitCurrency, TransitBalance, Transaction, TransactionTrip abstractions - Add MDST station databases (38 files) for worldwide transit station lookups - Add 220+ tests across 29 test files --- .../src/main/AndroidManifest.xml | 19 +- .../farebot/app/core/analytics/Analytics.kt | 6 +- .../app/core/app/FareBotApplication.kt | 43 +- .../farebot/app/core/di/AndroidModule.kt | 70 ++ .../farebot/app/core/kotlin/Array.kt | 0 .../farebot/app/core/kotlin/Color.kt | 0 .../farebot/app/core/kotlin/Date.kt | 0 .../farebot/app/core/nfc/NfcStream.kt | 26 +- .../farebot/app/core/nfc/TagReaderFactory.kt | 0 .../app/core/nfc/UnsupportedTagException.kt | 0 .../core/platform/AndroidPlatformActions.kt | 173 +++ .../farebot/app/core/sample/RawSampleCard.kt | 8 +- .../farebot/app/core/sample/SampleCard.kt | 13 +- .../farebot/app/core/sample/SampleRefill.kt | 8 +- .../app/core/sample/SampleSubscription.kt | 23 +- .../app/core/sample/SampleTransitFactory.kt | 1 + .../app/core/sample/SampleTransitInfo.kt | 22 +- .../farebot/app/core/sample/SampleTrip.kt | 31 +- .../app/core/serialize/CardKeysSerializer.kt | 0 .../serialize/KotlinxCardKeysSerializer.kt | 55 + .../core/transit/TransitFactoryRegistry.kt | 243 ++++ .../app/core/util/AndroidStringResource.kt | 14 + .../app/feature/bg/BackgroundTagActivity.kt | 2 +- .../app/feature/home/AndroidCardScanner.kt | 94 ++ .../farebot/app/feature/main/MainActivity.kt | 157 +++ .../prefs/FareBotPreferenceActivity.kt | 7 +- .../res/drawable-hdpi/bilheteunicosp_card.png | Bin .../main/res/drawable-hdpi/clipper_card.png | Bin .../src/main/res/drawable-hdpi/edy_card.png | Bin .../main/res/drawable-hdpi/ezlink_card.png | Bin .../src/main/res/drawable-hdpi/hsl_card.png | Bin .../src/main/res/drawable-hdpi/icoca_card.png | Bin .../src/main/res/drawable-hdpi/kmt_card.png | Bin .../drawable-hdpi/manly_fast_ferry_card.png | Bin .../src/main/res/drawable-hdpi/marker_end.png | Bin .../main/res/drawable-hdpi/marker_start.png | Bin .../src/main/res/drawable-hdpi/myki_card.png | Bin .../src/main/res/drawable-hdpi/nets_card.png | Bin .../main/res/drawable-hdpi/octopus_card.png | Bin .../src/main/res/drawable-hdpi/opal_card.png | Bin .../src/main/res/drawable-hdpi/orca_card.png | Bin .../main/res/drawable-hdpi/ovchip_card.png | Bin .../src/main/res/drawable-hdpi/pasmo_card.png | Bin .../src/main/res/drawable-hdpi/seqgo_card.png | Bin .../src/main/res/drawable-hdpi/suica_card.png | Bin .../src/main/res/drawable-xhdpi/easycard.png | Bin .../main/res/drawable/fg_item_selectable.xml | 0 .../main/res/drawable/ic_add_black_24dp.xml | 0 .../res/drawable/ic_delete_black_24dp.xml | 0 .../drawable/ic_help_outline_grey_24dp.xml | 0 .../res/drawable/ic_history_grey_24dp.xml | 0 .../main/res/drawable/ic_lock_black_24dp.xml | 0 .../drawable/ic_transaction_banned_32dp.xml | 0 .../res/drawable/ic_transaction_bus_32dp.xml | 0 .../drawable/ic_transaction_ferry_32dp.xml | 0 .../drawable/ic_transaction_handheld_32dp.xml | 0 .../drawable/ic_transaction_metro_32dp.xml | 0 .../res/drawable/ic_transaction_pos_32dp.xml | 0 .../drawable/ic_transaction_train_32dp.xml | 0 .../res/drawable/ic_transaction_tram_32dp.xml | 0 .../res/drawable/ic_transaction_tvm_32dp.xml | 0 .../drawable/ic_transaction_unknown_32dp.xml | 0 .../res/drawable/ic_transaction_vend_32dp.xml | 0 .../src/main/res/drawable/img_home_splash.xml | 0 .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../res/mipmap-xhdpi/ic_launcher_round.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin .../src/main/res/values-fi/strings.xml | 0 .../main/res/values-fr/strings-location.xml | 0 .../src/main/res/values-fr/strings.xml | 0 .../src/main/res/values-iw/strings.xml | 0 .../main/res/values-ja/strings-location.xml | 0 .../src/main/res/values-ja/strings.xml | 0 .../main/res/values-nl/strings-location.xml | 0 .../src/main/res/values-nl/strings.xml | 0 .../src/main/res/values/colors.xml | 0 .../src/main/res/values/dimens.xml | 0 .../src/main/res/values/plurals.xml | 0 .../src/main/res/values/strings-location.xml | 0 .../src/main/res/values/strings.xml | 5 + .../src/main/res/values/styles.xml | 0 .../src/main/res/values/themes.xml | 0 .../src/main/res/xml/filter_nfc.xml | 0 .../src/main/res/xml/prefs.xml | 0 .../farebot/persist/CardKeysPersister.kt | 10 + .../farebot/persist/CardPersister.kt | 32 + .../farebot/persist/db/DbCardKeysPersister.kt | 37 + .../farebot/persist/db/DbCardPersister.kt | 37 + .../farebot/persist/db/model/SavedCard.kt | 13 + .../farebot/persist/db/model/SavedKey.kt | 13 + .../farebot/persist/db/SavedCard.sq | 19 + .../codebutler/farebot/persist/db/SavedKey.sq | 19 + .../src/main/AndroidManifest.xml | 25 - .../farebot/persist/CardKeysPersister.java | 21 - .../farebot/persist/CardPersister.java | 43 - .../farebot/persist/db/DbCardKeysPersister.kt | 11 - .../farebot/persist/db/DbCardPersister.kt | 11 - .../farebot/persist/db/FareBotDb.kt | 86 -- .../farebot/persist/db/FareBotDbConverters.kt | 20 - .../farebot/persist/db/dao/SavedCardDao.kt | 22 - .../farebot/persist/db/dao/SavedKeyDao.kt | 22 - .../farebot/persist/db/model/SavedCard.kt | 16 - .../farebot/persist/db/model/SavedKey.kt | 16 - farebot-app/build.gradle | 135 --- .../app/core/activity/ActivityOperations.kt | 43 - .../core/app/FareBotApplicationComponent.kt | 69 -- .../app/core/app/FareBotApplicationModule.kt | 105 -- .../farebot/app/core/kotlin/KotterKnife.kt | 164 --- .../farebot/app/core/kotlin/Optional.kt | 43 - .../app/core/kotlin/ViewGroupExtensions.kt | 31 - .../farebot/app/core/nfc/NfcStream.kt | 82 -- .../farebot/app/core/rx/LastValueRelay.kt | 53 - .../gson/CardKeysGsonTypeAdapterFactory.kt | 79 -- .../serialize/gson/GsonCardKeysSerializer.kt | 38 - .../gson/RawCardGsonTypeAdapterFactory.kt | 90 -- .../core/transit/TransitFactoryRegistry.kt | 99 -- .../app/core/ui/FareBotCrossfadeTransition.kt | 63 - .../farebot/app/core/ui/FareBotScreen.kt | 105 -- .../farebot/app/core/util/ErrorUtils.kt | 57 - .../farebot/app/core/util/ExportHelper.kt | 56 - .../farebot/app/feature/card/CardScreen.kt | 164 --- .../app/feature/card/CardScreenView.kt | 78 -- .../app/feature/card/TransactionAdapter.kt | 191 --- .../app/feature/card/TransactionViewModel.kt | 100 -- .../card/advanced/CardAdvancedAdapter.kt | 126 -- .../card/advanced/CardAdvancedScreen.kt | 71 -- .../card/advanced/CardAdvancedScreenView.kt | 54 - .../card/advanced/CardAdvancedTabView.kt | 85 -- .../app/feature/card/map/TripMapScreen.kt | 105 -- .../app/feature/card/map/TripMapScreenView.kt | 128 -- .../farebot/app/feature/help/HelpScreen.kt | 58 - .../app/feature/help/HelpScreenView.kt | 276 ----- .../app/feature/history/HistoryAdapter.kt | 115 -- .../app/feature/history/HistoryScreen.kt | 276 ----- .../app/feature/history/HistoryScreenView.kt | 118 -- .../app/feature/history/HistoryViewModel.kt | 35 - .../farebot/app/feature/home/CardStream.kt | 112 -- .../farebot/app/feature/home/HomeScreen.kt | 160 --- .../app/feature/home/HomeScreenView.kt | 118 -- .../farebot/app/feature/keys/KeysAdapter.kt | 89 -- .../farebot/app/feature/keys/KeysScreen.kt | 121 -- .../app/feature/keys/KeysScreenView.kt | 106 -- .../app/feature/keys/add/AddKeyScreen.kt | 156 --- .../app/feature/keys/add/AddKeyScreenView.kt | 68 -- .../farebot/app/feature/main/MainActivity.kt | 297 ----- .../src/main/res/layout/activity_main.xml | 58 - .../main/res/layout/item_card_advanced.xml | 56 - .../src/main/res/layout/item_history.xml | 79 -- farebot-app/src/main/res/layout/item_key.xml | 49 - .../main/res/layout/item_supported_card.xml | 114 -- .../src/main/res/layout/item_transaction.xml | 47 - .../res/layout/item_transaction_refill.xml | 74 -- .../layout/item_transaction_subscription.xml | 63 - .../main/res/layout/item_transaction_trip.xml | 104 -- .../src/main/res/layout/screen_card.xml | 75 -- .../main/res/layout/screen_card_advanced.xml | 42 - .../src/main/res/layout/screen_help.xml | 36 - .../src/main/res/layout/screen_history.xml | 48 - .../src/main/res/layout/screen_home.xml | 79 -- .../src/main/res/layout/screen_keys.xml | 34 - .../src/main/res/layout/screen_keys_add.xml | 123 -- .../src/main/res/layout/screen_trip_map.xml | 29 - .../src/main/res/layout/tab_card_advanced.xml | 33 - .../src/main/res/menu/action_history.xml | 32 - farebot-app/src/main/res/menu/action_keys.xml | 32 - farebot-app/src/main/res/menu/screen_card.xml | 28 - .../src/main/res/menu/screen_history.xml | 44 - farebot-app/src/main/res/menu/screen_keys.xml | 33 - farebot-app/src/main/res/menu/screen_main.xml | 55 - .../farebot/base/mdst/ResourceAccessor.kt | 37 + .../base/util/BundledDatabaseDriverFactory.kt | 33 + .../farebot/base/util/SystemLocale.kt | 3 + .../farebot/base/mdst/TestFileLoader.kt | 21 + .../composeResources/files/adelaide.mdst | Bin 0 -> 4702 bytes .../composeResources/files/amiibo.mdst | Bin 0 -> 13885 bytes .../composeResources/files/cadiz.mdst | Bin 0 -> 4762 bytes .../composeResources/files/chc_metrocard.mdst | Bin 0 -> 88 bytes .../composeResources/files/clipper.mdst | Bin 0 -> 9798 bytes .../composeResources/files/compass.mdst | Bin 0 -> 3639 bytes .../composeResources/files/easycard.mdst | Bin 0 -> 5615 bytes .../composeResources/files/ezlink.mdst | Bin 0 -> 8952 bytes .../composeResources/files/gautrain.mdst | Bin 0 -> 387 bytes .../composeResources/files/gironde.mdst | Bin 0 -> 1934 bytes .../composeResources/files/hafilat.mdst | Bin 0 -> 31 bytes .../composeResources/files/kmt.mdst | Bin 0 -> 2763 bytes .../composeResources/files/lax_tap.mdst | Bin 0 -> 3056 bytes .../composeResources/files/lisboa_viva.mdst | Bin 0 -> 32321 bytes .../composeResources/files/mobib.mdst | Bin 0 -> 91204 bytes .../composeResources/files/navigo.mdst | Bin 0 -> 23814 bytes .../composeResources/files/opus.mdst | Bin 0 -> 4815 bytes .../composeResources/files/orca.mdst | Bin 0 -> 2504 bytes .../composeResources/files/orca_brt.mdst | Bin 0 -> 13149 bytes .../files/orca_streetcar.mdst | Bin 0 -> 1899 bytes .../composeResources/files/oura.mdst | Bin 0 -> 1153 bytes .../composeResources/files/ovc.mdst | Bin 0 -> 1131223 bytes .../composeResources/files/passpass.mdst | Bin 0 -> 162 bytes .../composeResources/files/podorozhnik.mdst | Bin 0 -> 5364 bytes .../composeResources/files/ravkav.mdst | Bin 0 -> 1829 bytes .../composeResources/files/ricaricami.mdst | Bin 0 -> 818 bytes .../composeResources/files/rkf.mdst | Bin 0 -> 325 bytes .../composeResources/files/seq_go.mdst | Bin 0 -> 9223 bytes .../composeResources/files/shenzhen.mdst | Bin 0 -> 3407 bytes .../composeResources/files/smartrider.mdst | Bin 0 -> 2989 bytes .../composeResources/files/suica_bus.mdst | Bin 0 -> 225870 bytes .../composeResources/files/suica_rail.mdst | Bin 0 -> 329330 bytes .../composeResources/files/tfi_leap.mdst | Bin 0 -> 2103 bytes .../composeResources/files/tisseo.mdst | Bin 0 -> 3594 bytes .../composeResources/files/touchngo.mdst | Bin 0 -> 813 bytes .../composeResources/files/troika.mdst | Bin 0 -> 341 bytes .../composeResources/files/waltti_region.mdst | Bin 0 -> 942 bytes .../composeResources/files/yargor.mdst | Bin 0 -> 135 bytes .../codebutler/farebot/base/mdst/MdstData.kt | 101 ++ .../farebot/base/mdst/MdstStationLookup.kt | 189 +++ .../base/mdst/MdstStationTableReader.kt | 268 +++++ .../farebot/base/mdst/ResourceAccessor.kt | 32 + .../farebot/base/ui/FareBotUiTree.kt | 88 ++ .../farebot/base/ui/HeaderListItem.kt | 47 + .../codebutler/farebot/base/ui/ListItem.kt | 66 ++ .../farebot/base/ui/ListItemInterface.kt | 31 + .../farebot/base/ui/ListItemRecursive.kt | 43 + .../farebot/base/ui/UiTreeBuilder.kt | 26 +- .../base/util/BundledDatabaseDriverFactory.kt | 9 + .../farebot/base/util/ByteArrayExt.kt | 247 ++++ .../codebutler/farebot/base/util/ByteUtils.kt | 154 +++ .../farebot/base/util/CurrencyFormatter.kt | 135 +++ .../farebot/base/util/DateFormatting.kt | 28 + .../base/util/DefaultStringResource.kt | 18 + .../codebutler/farebot/base/util/HashUtils.kt | 115 ++ .../com/codebutler/farebot/base/util/Luhn.kt | 80 ++ .../com/codebutler/farebot/base/util/Md5.kt | 19 +- .../farebot/base/util/NumberUtils.kt | 146 +++ .../farebot/base/util/StringResource.kt | 12 + .../farebot/base/util/SystemLocale.kt | 6 + .../base/mdst/MdstStationTableReaderTest.kt | 624 ++++++++++ .../farebot/base/mdst/TestFileLoader.kt | 3 + .../codebutler/farebot/base/util/CrcTest.kt | 53 + .../farebot/base/util/DateTimeTest.kt | 255 ++++ .../farebot/base/util/KeyHashTest.kt | 134 +++ .../codebutler/farebot/base/util/LuhnTest.kt | 333 ++++++ .../farebot/base/util/NumberUtilsTest.kt | 61 + .../farebot/base/mdst/ResourceAccessor.kt | 38 + .../base/util/BundledDatabaseDriverFactory.kt | 58 + .../farebot/base/util/SystemLocale.kt | 7 + .../farebot/base/mdst/TestFileLoader.kt | 38 + farebot-base/src/main/AndroidManifest.xml | 25 - .../farebot/base/ui/FareBotUiTree.java | 113 -- .../farebot/base/util/ArrayUtils.java | 1046 ----------------- .../farebot/base/util/ByteArray.java | 118 -- .../farebot/base/util/ByteUtils.java | 162 --- .../farebot/base/util/Charsets.java | 193 --- .../codebutler/farebot/base/util/DBUtil.java | 146 --- .../codebutler/farebot/base/util/IOUtils.java | 773 ------------ .../codebutler/farebot/base/util/Luhn.java | 83 -- .../com/codebutler/farebot/card/TagReader.kt | 64 + .../card/nfc/AndroidCardTransceiver.kt | 29 +- .../card/nfc/AndroidClassicTechnology.kt | 57 + .../farebot/card/nfc/AndroidNfcFTechnology.kt | 42 + .../card/nfc/AndroidUltralightTechnology.kt | 29 +- .../com/codebutler/farebot/card/Card.kt} | 29 +- .../com/codebutler/farebot/card/CardType.kt} | 34 +- .../com/codebutler/farebot/card/RawCard.kt} | 20 +- .../farebot/card/nfc/CardTransceiver.kt | 13 +- .../farebot/card/nfc/ClassicTechnology.kt | 34 +- .../farebot/card/nfc/NfcFTechnology.kt | 13 +- .../farebot/card/nfc/NfcTechnology.kt | 15 +- .../farebot/card/nfc/UltralightTechnology.kt | 21 +- .../farebot/card/serialize/CardSerializer.kt | 19 +- .../com/codebutler/farebot/key/CardKeys.kt} | 13 +- .../farebot/card/nfc/IosCardTransceiver.kt | 86 ++ .../farebot/card/nfc/IosNfcFTechnology.kt | 51 + .../card/nfc/IosUltralightTechnology.kt | 88 ++ .../farebot/card/nfc/NfcDataConversions.kt | 49 + farebot-card/src/main/AndroidManifest.xml | 29 - .../com/codebutler/farebot/card/Card.java | 56 - .../codebutler/farebot/card/TagReader.java | 71 -- .../composeResources/values/strings.xml | 15 + .../codebutler/farebot/transit/CardInfo.kt | 71 ++ .../farebot/transit/CardInfoRegistry.kt | 80 ++ .../farebot/transit/ObfuscatedTrip.kt | 155 +++ .../com/codebutler/farebot/transit/Refill.kt} | 28 +- .../codebutler/farebot/transit/RefillTrip.kt | 65 + .../com/codebutler/farebot/transit/Station.kt | 128 ++ .../farebot/transit/Subscription.kt | 290 +++++ .../codebutler/farebot/transit/Transaction.kt | 97 ++ .../farebot/transit/TransactionTrip.kt | 164 +++ .../farebot/transit/TransitBalance.kt | 28 +- .../farebot/transit/TransitCurrency.kt | 123 ++ .../farebot/transit/TransitFactory.kt | 25 + .../farebot/transit/TransitIdentity.kt | 37 + .../codebutler/farebot/transit/TransitInfo.kt | 122 ++ .../farebot/transit/TransitRegion.kt | 180 +++ .../com/codebutler/farebot/transit/Trip.kt | 220 ++++ .../farebot/transit/TripObfuscator.kt | 282 +++++ .../farebot/transit/UnknownTransitInfo.kt | 41 + .../farebot/transit/CardInfoRegistryTest.kt | 165 +++ .../farebot/transit/TransitCurrencyTest.kt | 152 +++ .../transit/TransitSerializationTest.kt | 248 ++++ .../farebot/transit/TripObfuscatorTest.kt | 197 ++++ farebot-transit/src/main/AndroidManifest.xml | 25 - .../farebot/transit/RefillTrip.java | 115 -- .../codebutler/farebot/transit/Station.java | 114 -- .../farebot/transit/Subscription.java | 51 - .../farebot/transit/TransitFactory.java | 16 - .../farebot/transit/TransitIdentity.java | 43 - .../farebot/transit/TransitInfo.java | 69 -- .../com/codebutler/farebot/transit/Trip.java | 138 --- 309 files changed, 8079 insertions(+), 9863 deletions(-) rename {farebot-app => farebot-android}/src/main/AndroidManifest.xml (86%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/analytics/Analytics.kt (84%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplication.kt (51%) create mode 100644 farebot-android/src/main/java/com/codebutler/farebot/app/core/di/AndroidModule.kt rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/kotlin/Array.kt (100%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/kotlin/Color.kt (100%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/kotlin/Date.kt (100%) rename farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/CardTypeGsonTypeAdapter.kt => farebot-android/src/main/java/com/codebutler/farebot/app/core/nfc/NfcStream.kt (61%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/nfc/TagReaderFactory.kt (100%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/nfc/UnsupportedTagException.kt (100%) create mode 100644 farebot-android/src/main/java/com/codebutler/farebot/app/core/platform/AndroidPlatformActions.kt rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/sample/RawSampleCard.kt (84%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/sample/SampleCard.kt (81%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/sample/SampleRefill.kt (78%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/sample/SampleSubscription.kt (62%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitFactory.kt (96%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitInfo.kt (74%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/sample/SampleTrip.kt (53%) rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/core/serialize/CardKeysSerializer.kt (100%) create mode 100644 farebot-android/src/main/java/com/codebutler/farebot/app/core/serialize/KotlinxCardKeysSerializer.kt create mode 100644 farebot-android/src/main/java/com/codebutler/farebot/app/core/transit/TransitFactoryRegistry.kt create mode 100644 farebot-android/src/main/java/com/codebutler/farebot/app/core/util/AndroidStringResource.kt rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/feature/bg/BackgroundTagActivity.kt (97%) create mode 100644 farebot-android/src/main/java/com/codebutler/farebot/app/feature/home/AndroidCardScanner.kt create mode 100644 farebot-android/src/main/java/com/codebutler/farebot/app/feature/main/MainActivity.kt rename {farebot-app => farebot-android}/src/main/java/com/codebutler/farebot/app/feature/prefs/FareBotPreferenceActivity.kt (94%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/bilheteunicosp_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/clipper_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/edy_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/ezlink_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/hsl_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/icoca_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/kmt_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/manly_fast_ferry_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/marker_end.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/marker_start.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/myki_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/nets_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/octopus_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/opal_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/orca_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/ovchip_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/pasmo_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/seqgo_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-hdpi/suica_card.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable-xhdpi/easycard.png (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/fg_item_selectable.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_add_black_24dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_delete_black_24dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_help_outline_grey_24dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_history_grey_24dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_lock_black_24dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_transaction_banned_32dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_transaction_bus_32dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_transaction_ferry_32dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_transaction_handheld_32dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_transaction_metro_32dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_transaction_pos_32dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_transaction_train_32dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_transaction_tram_32dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_transaction_tvm_32dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_transaction_unknown_32dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/ic_transaction_vend_32dp.xml (100%) rename {farebot-app => farebot-android}/src/main/res/drawable/img_home_splash.xml (100%) rename {farebot-app => farebot-android}/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {farebot-app => farebot-android}/src/main/res/mipmap-xhdpi/ic_launcher_round.png (100%) rename {farebot-app => farebot-android}/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {farebot-app => farebot-android}/src/main/res/mipmap-xxhdpi/ic_launcher_round.png (100%) rename {farebot-app => farebot-android}/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename {farebot-app => farebot-android}/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png (100%) rename {farebot-app => farebot-android}/src/main/res/values-fi/strings.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values-fr/strings-location.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values-fr/strings.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values-iw/strings.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values-ja/strings-location.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values-ja/strings.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values-nl/strings-location.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values-nl/strings.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values/colors.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values/dimens.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values/plurals.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values/strings-location.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values/strings.xml (92%) rename {farebot-app => farebot-android}/src/main/res/values/styles.xml (100%) rename {farebot-app => farebot-android}/src/main/res/values/themes.xml (100%) rename {farebot-app => farebot-android}/src/main/res/xml/filter_nfc.xml (100%) rename {farebot-app => farebot-android}/src/main/res/xml/prefs.xml (100%) create mode 100644 farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/CardKeysPersister.kt create mode 100644 farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/CardPersister.kt create mode 100644 farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/DbCardKeysPersister.kt create mode 100644 farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/DbCardPersister.kt create mode 100644 farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/model/SavedCard.kt create mode 100644 farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/model/SavedKey.kt create mode 100644 farebot-app-persist/src/commonMain/sqldelight/com/codebutler/farebot/persist/db/SavedCard.sq create mode 100644 farebot-app-persist/src/commonMain/sqldelight/com/codebutler/farebot/persist/db/SavedKey.sq delete mode 100644 farebot-app-persist/src/main/AndroidManifest.xml delete mode 100644 farebot-app-persist/src/main/java/com/codebutler/farebot/persist/CardKeysPersister.java delete mode 100644 farebot-app-persist/src/main/java/com/codebutler/farebot/persist/CardPersister.java delete mode 100644 farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/DbCardKeysPersister.kt delete mode 100644 farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/DbCardPersister.kt delete mode 100644 farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/FareBotDb.kt delete mode 100644 farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/FareBotDbConverters.kt delete mode 100644 farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/dao/SavedCardDao.kt delete mode 100644 farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/dao/SavedKeyDao.kt delete mode 100644 farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/model/SavedCard.kt delete mode 100644 farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/model/SavedKey.kt delete mode 100644 farebot-app/build.gradle delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/activity/ActivityOperations.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplicationComponent.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplicationModule.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/KotterKnife.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/Optional.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/ViewGroupExtensions.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/nfc/NfcStream.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/rx/LastValueRelay.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/CardKeysGsonTypeAdapterFactory.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/GsonCardKeysSerializer.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/RawCardGsonTypeAdapterFactory.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/transit/TransitFactoryRegistry.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/ui/FareBotCrossfadeTransition.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/ui/FareBotScreen.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/util/ErrorUtils.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/core/util/ExportHelper.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/CardScreen.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/CardScreenView.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/TransactionAdapter.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/TransactionViewModel.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedAdapter.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedScreen.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedScreenView.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedTabView.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/map/TripMapScreen.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/map/TripMapScreenView.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/help/HelpScreen.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/help/HelpScreenView.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryAdapter.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryScreen.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryScreenView.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryViewModel.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/home/CardStream.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/home/HomeScreen.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/home/HomeScreenView.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeysAdapter.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeysScreen.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeysScreenView.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/add/AddKeyScreen.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/add/AddKeyScreenView.kt delete mode 100644 farebot-app/src/main/java/com/codebutler/farebot/app/feature/main/MainActivity.kt delete mode 100644 farebot-app/src/main/res/layout/activity_main.xml delete mode 100644 farebot-app/src/main/res/layout/item_card_advanced.xml delete mode 100644 farebot-app/src/main/res/layout/item_history.xml delete mode 100644 farebot-app/src/main/res/layout/item_key.xml delete mode 100644 farebot-app/src/main/res/layout/item_supported_card.xml delete mode 100644 farebot-app/src/main/res/layout/item_transaction.xml delete mode 100644 farebot-app/src/main/res/layout/item_transaction_refill.xml delete mode 100644 farebot-app/src/main/res/layout/item_transaction_subscription.xml delete mode 100644 farebot-app/src/main/res/layout/item_transaction_trip.xml delete mode 100644 farebot-app/src/main/res/layout/screen_card.xml delete mode 100644 farebot-app/src/main/res/layout/screen_card_advanced.xml delete mode 100644 farebot-app/src/main/res/layout/screen_help.xml delete mode 100644 farebot-app/src/main/res/layout/screen_history.xml delete mode 100644 farebot-app/src/main/res/layout/screen_home.xml delete mode 100644 farebot-app/src/main/res/layout/screen_keys.xml delete mode 100644 farebot-app/src/main/res/layout/screen_keys_add.xml delete mode 100644 farebot-app/src/main/res/layout/screen_trip_map.xml delete mode 100644 farebot-app/src/main/res/layout/tab_card_advanced.xml delete mode 100644 farebot-app/src/main/res/menu/action_history.xml delete mode 100644 farebot-app/src/main/res/menu/action_keys.xml delete mode 100644 farebot-app/src/main/res/menu/screen_card.xml delete mode 100644 farebot-app/src/main/res/menu/screen_history.xml delete mode 100644 farebot-app/src/main/res/menu/screen_keys.xml delete mode 100644 farebot-app/src/main/res/menu/screen_main.xml create mode 100644 farebot-base/src/androidMain/kotlin/com/codebutler/farebot/base/mdst/ResourceAccessor.kt create mode 100644 farebot-base/src/androidMain/kotlin/com/codebutler/farebot/base/util/BundledDatabaseDriverFactory.kt create mode 100644 farebot-base/src/androidMain/kotlin/com/codebutler/farebot/base/util/SystemLocale.kt create mode 100644 farebot-base/src/androidUnitTest/kotlin/com/codebutler/farebot/base/mdst/TestFileLoader.kt create mode 100644 farebot-base/src/commonMain/composeResources/files/adelaide.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/amiibo.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/cadiz.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/chc_metrocard.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/clipper.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/compass.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/easycard.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/ezlink.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/gautrain.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/gironde.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/hafilat.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/kmt.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/lax_tap.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/lisboa_viva.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/mobib.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/navigo.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/opus.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/orca.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/orca_brt.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/orca_streetcar.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/oura.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/ovc.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/passpass.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/podorozhnik.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/ravkav.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/ricaricami.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/rkf.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/seq_go.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/shenzhen.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/smartrider.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/suica_bus.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/suica_rail.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/tfi_leap.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/tisseo.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/touchngo.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/troika.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/waltti_region.mdst create mode 100644 farebot-base/src/commonMain/composeResources/files/yargor.mdst create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/mdst/MdstData.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/mdst/MdstStationLookup.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/mdst/MdstStationTableReader.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/mdst/ResourceAccessor.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/FareBotUiTree.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/HeaderListItem.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/ListItem.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/ListItemInterface.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/ui/ListItemRecursive.kt rename farebot-base/src/{main/java => commonMain/kotlin}/com/codebutler/farebot/base/ui/UiTreeBuilder.kt (57%) create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/util/BundledDatabaseDriverFactory.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/util/ByteArrayExt.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/util/ByteUtils.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/util/CurrencyFormatter.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/util/DateFormatting.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/util/DefaultStringResource.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/util/HashUtils.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/util/Luhn.kt rename farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeyViewModel.kt => farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/util/Md5.kt (64%) create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/util/NumberUtils.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/util/StringResource.kt create mode 100644 farebot-base/src/commonMain/kotlin/com/codebutler/farebot/base/util/SystemLocale.kt create mode 100644 farebot-base/src/commonTest/kotlin/com/codebutler/farebot/base/mdst/MdstStationTableReaderTest.kt create mode 100644 farebot-base/src/commonTest/kotlin/com/codebutler/farebot/base/mdst/TestFileLoader.kt create mode 100644 farebot-base/src/commonTest/kotlin/com/codebutler/farebot/base/util/CrcTest.kt create mode 100644 farebot-base/src/commonTest/kotlin/com/codebutler/farebot/base/util/DateTimeTest.kt create mode 100644 farebot-base/src/commonTest/kotlin/com/codebutler/farebot/base/util/KeyHashTest.kt create mode 100644 farebot-base/src/commonTest/kotlin/com/codebutler/farebot/base/util/LuhnTest.kt create mode 100644 farebot-base/src/commonTest/kotlin/com/codebutler/farebot/base/util/NumberUtilsTest.kt create mode 100644 farebot-base/src/iosMain/kotlin/com/codebutler/farebot/base/mdst/ResourceAccessor.kt create mode 100644 farebot-base/src/iosMain/kotlin/com/codebutler/farebot/base/util/BundledDatabaseDriverFactory.kt create mode 100644 farebot-base/src/iosMain/kotlin/com/codebutler/farebot/base/util/SystemLocale.kt create mode 100644 farebot-base/src/iosTest/kotlin/com/codebutler/farebot/base/mdst/TestFileLoader.kt delete mode 100644 farebot-base/src/main/AndroidManifest.xml delete mode 100644 farebot-base/src/main/java/com/codebutler/farebot/base/ui/FareBotUiTree.java delete mode 100644 farebot-base/src/main/java/com/codebutler/farebot/base/util/ArrayUtils.java delete mode 100644 farebot-base/src/main/java/com/codebutler/farebot/base/util/ByteArray.java delete mode 100644 farebot-base/src/main/java/com/codebutler/farebot/base/util/ByteUtils.java delete mode 100644 farebot-base/src/main/java/com/codebutler/farebot/base/util/Charsets.java delete mode 100644 farebot-base/src/main/java/com/codebutler/farebot/base/util/DBUtil.java delete mode 100644 farebot-base/src/main/java/com/codebutler/farebot/base/util/IOUtils.java delete mode 100644 farebot-base/src/main/java/com/codebutler/farebot/base/util/Luhn.java create mode 100644 farebot-card/src/androidMain/kotlin/com/codebutler/farebot/card/TagReader.kt rename farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/EpochDateTypeAdapter.kt => farebot-card/src/androidMain/kotlin/com/codebutler/farebot/card/nfc/AndroidCardTransceiver.kt (57%) create mode 100644 farebot-card/src/androidMain/kotlin/com/codebutler/farebot/card/nfc/AndroidClassicTechnology.kt create mode 100644 farebot-card/src/androidMain/kotlin/com/codebutler/farebot/card/nfc/AndroidNfcFTechnology.kt rename farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/GsonCardSerializer.kt => farebot-card/src/androidMain/kotlin/com/codebutler/farebot/card/nfc/AndroidUltralightTechnology.kt (53%) rename farebot-card/src/{main/java/com/codebutler/farebot/card/RawCard.java => commonMain/kotlin/com/codebutler/farebot/card/Card.kt} (62%) rename farebot-card/src/{main/java/com/codebutler/farebot/card/CardType.java => commonMain/kotlin/com/codebutler/farebot/card/CardType.kt} (60%) rename farebot-card/src/{main/java/com/codebutler/farebot/card/serialize/CardSerializer.java => commonMain/kotlin/com/codebutler/farebot/card/RawCard.kt} (72%) rename farebot-app/src/main/java/com/codebutler/farebot/app/core/activity/ActivityResult.kt => farebot-card/src/commonMain/kotlin/com/codebutler/farebot/card/nfc/CardTransceiver.kt (75%) rename farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/ByteArrayGsonTypeAdapter.kt => farebot-card/src/commonMain/kotlin/com/codebutler/farebot/card/nfc/ClassicTechnology.kt (52%) rename farebot-app/src/main/java/com/codebutler/farebot/app/core/inject/ActivityScope.kt => farebot-card/src/commonMain/kotlin/com/codebutler/farebot/card/nfc/NfcFTechnology.kt (79%) rename farebot-app/src/main/java/com/codebutler/farebot/app/core/inject/ScreenScope.kt => farebot-card/src/commonMain/kotlin/com/codebutler/farebot/card/nfc/NfcTechnology.kt (78%) rename farebot-app/src/main/java/com/codebutler/farebot/app/core/ui/ActionBarOptions.kt => farebot-card/src/commonMain/kotlin/com/codebutler/farebot/card/nfc/UltralightTechnology.kt (66%) rename farebot-app/src/main/java/com/codebutler/farebot/app/core/ui/ScreenLifecycleEvent.kt => farebot-card/src/commonMain/kotlin/com/codebutler/farebot/card/serialize/CardSerializer.kt (72%) rename farebot-card/src/{main/java/com/codebutler/farebot/key/CardKeys.java => commonMain/kotlin/com/codebutler/farebot/key/CardKeys.kt} (80%) create mode 100644 farebot-card/src/iosMain/kotlin/com/codebutler/farebot/card/nfc/IosCardTransceiver.kt create mode 100644 farebot-card/src/iosMain/kotlin/com/codebutler/farebot/card/nfc/IosNfcFTechnology.kt create mode 100644 farebot-card/src/iosMain/kotlin/com/codebutler/farebot/card/nfc/IosUltralightTechnology.kt create mode 100644 farebot-card/src/iosMain/kotlin/com/codebutler/farebot/card/nfc/NfcDataConversions.kt delete mode 100644 farebot-card/src/main/AndroidManifest.xml delete mode 100644 farebot-card/src/main/java/com/codebutler/farebot/card/Card.java delete mode 100644 farebot-card/src/main/java/com/codebutler/farebot/card/TagReader.java create mode 100644 farebot-transit/src/commonMain/composeResources/values/strings.xml create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/CardInfo.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/CardInfoRegistry.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/ObfuscatedTrip.kt rename farebot-transit/src/{main/java/com/codebutler/farebot/transit/Refill.java => commonMain/kotlin/com/codebutler/farebot/transit/Refill.kt} (56%) create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/RefillTrip.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/Station.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/Subscription.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/Transaction.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/TransactionTrip.kt rename farebot-app/src/main/java/com/codebutler/farebot/app/core/activity/RequestPermissionsResult.kt => farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/TransitBalance.kt (53%) create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/TransitCurrency.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/TransitFactory.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/TransitIdentity.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/TransitInfo.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/TransitRegion.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/Trip.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/TripObfuscator.kt create mode 100644 farebot-transit/src/commonMain/kotlin/com/codebutler/farebot/transit/UnknownTransitInfo.kt create mode 100644 farebot-transit/src/commonTest/kotlin/com/codebutler/farebot/transit/CardInfoRegistryTest.kt create mode 100644 farebot-transit/src/commonTest/kotlin/com/codebutler/farebot/transit/TransitCurrencyTest.kt create mode 100644 farebot-transit/src/commonTest/kotlin/com/codebutler/farebot/transit/TransitSerializationTest.kt create mode 100644 farebot-transit/src/commonTest/kotlin/com/codebutler/farebot/transit/TripObfuscatorTest.kt delete mode 100644 farebot-transit/src/main/AndroidManifest.xml delete mode 100644 farebot-transit/src/main/java/com/codebutler/farebot/transit/RefillTrip.java delete mode 100644 farebot-transit/src/main/java/com/codebutler/farebot/transit/Station.java delete mode 100644 farebot-transit/src/main/java/com/codebutler/farebot/transit/Subscription.java delete mode 100644 farebot-transit/src/main/java/com/codebutler/farebot/transit/TransitFactory.java delete mode 100644 farebot-transit/src/main/java/com/codebutler/farebot/transit/TransitIdentity.java delete mode 100644 farebot-transit/src/main/java/com/codebutler/farebot/transit/TransitInfo.java delete mode 100644 farebot-transit/src/main/java/com/codebutler/farebot/transit/Trip.java diff --git a/farebot-app/src/main/AndroidManifest.xml b/farebot-android/src/main/AndroidManifest.xml similarity index 86% rename from farebot-app/src/main/AndroidManifest.xml rename to farebot-android/src/main/AndroidManifest.xml index a214f5514..856d1b6ce 100644 --- a/farebot-app/src/main/AndroidManifest.xml +++ b/farebot-android/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ +> @@ -56,11 +56,23 @@ android:name="com.codebutler.farebot.app.feature.main.MainActivity" android:configChanges="keyboardHidden|orientation" android:screenOrientation="portrait" + android:exported="true" android:theme="@style/FareBot.Theme.Main"> + + + + + + + + + + + @@ -90,10 +103,6 @@ android:name="com.google.android.maps.v2.API_KEY" android:value="AIzaSyBUUm1_1cyaCLkIvcE60gF4xO3pyb6SyP4"/> - - diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/analytics/Analytics.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/analytics/Analytics.kt similarity index 84% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/analytics/Analytics.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/analytics/Analytics.kt index b3831888e..ebcdf364b 100644 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/analytics/Analytics.kt +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/analytics/Analytics.kt @@ -22,8 +22,7 @@ package com.codebutler.farebot.app.core.analytics -import com.crashlytics.android.answers.Answers -import com.crashlytics.android.answers.CustomEvent +import android.util.Log enum class AnalyticsEventName(val value: String) { SCAN_CARD("Scan Card"), @@ -34,6 +33,5 @@ enum class AnalyticsEventName(val value: String) { } fun logAnalyticsEvent(name: AnalyticsEventName, type: String) { - Answers.getInstance().logCustom(CustomEvent(name.value) - .putCustomAttribute("Type", type)) + Log.d("Analytics", "${name.value}: $type") } diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplication.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplication.kt similarity index 51% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplication.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplication.kt index b00848881..3d34e8c48 100644 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplication.kt +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplication.kt @@ -23,26 +23,15 @@ package com.codebutler.farebot.app.core.app import android.app.Application -import android.content.SharedPreferences import android.os.StrictMode -import com.codebutler.farebot.BuildConfig -import com.crashlytics.android.Crashlytics -import com.crashlytics.android.answers.Answers -import io.fabric.sdk.android.Fabric -import java.util.Date -import javax.inject.Inject +import com.codebutler.farebot.app.core.di.androidModule +import com.codebutler.farebot.shared.di.sharedModule +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin class FareBotApplication : Application() { - companion object { - val PREF_LAST_READ_ID = "last_read_id" - val PREF_LAST_READ_AT = "last_read_at" - } - - lateinit var component: FareBotApplicationComponent - - @Inject lateinit var sharedPreferences: SharedPreferences - override fun onCreate() { super.onCreate() @@ -51,24 +40,10 @@ class FareBotApplication : Application() { .penaltyLog() .build()) - component = DaggerFareBotApplicationComponent.builder() - .application(this) - .module(FareBotApplicationModule()) - .build() - - component.inject(this) - - if (!BuildConfig.DEBUG) { - Fabric.with(this, Answers(), Crashlytics()) - } else { - Fabric.with(this, Answers()) + startKoin { + androidLogger() + androidContext(this@FareBotApplication) + modules(sharedModule, androidModule) } } - - fun updateTimestamp(tagIdString: String?) { - val prefs = sharedPreferences.edit() - prefs.putString(FareBotApplication.PREF_LAST_READ_ID, tagIdString) - prefs.putLong(FareBotApplication.PREF_LAST_READ_AT, Date().time) - prefs.apply() - } } diff --git a/farebot-android/src/main/java/com/codebutler/farebot/app/core/di/AndroidModule.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/di/AndroidModule.kt new file mode 100644 index 000000000..71ff17450 --- /dev/null +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/di/AndroidModule.kt @@ -0,0 +1,70 @@ +package com.codebutler.farebot.app.core.di + +import android.content.SharedPreferences +import app.cash.sqldelight.driver.android.AndroidSqliteDriver +import com.codebutler.farebot.app.core.nfc.NfcStream +import com.codebutler.farebot.app.core.nfc.TagReaderFactory +import com.codebutler.farebot.app.core.serialize.CardKeysSerializer +import com.codebutler.farebot.app.core.serialize.KotlinxCardKeysSerializer +import com.codebutler.farebot.shared.serialize.FareBotSerializersModule +import com.codebutler.farebot.shared.serialize.KotlinxCardSerializer +import com.codebutler.farebot.app.core.transit.createAndroidTransitFactoryRegistry +import com.codebutler.farebot.app.feature.home.AndroidCardScanner +import com.codebutler.farebot.base.util.DefaultStringResource +import com.codebutler.farebot.base.util.StringResource +import com.codebutler.farebot.card.serialize.CardSerializer +import com.codebutler.farebot.persist.CardKeysPersister +import com.codebutler.farebot.persist.CardPersister +import com.codebutler.farebot.persist.db.DbCardKeysPersister +import com.codebutler.farebot.persist.db.DbCardPersister +import com.codebutler.farebot.persist.db.FareBotDb +import com.codebutler.farebot.shared.nfc.CardScanner +import com.codebutler.farebot.shared.settings.AppSettings +import com.codebutler.farebot.shared.transit.TransitFactoryRegistry +import kotlinx.serialization.json.Json +import org.koin.android.ext.koin.androidContext +import org.koin.dsl.module + +val androidModule = module { + single { androidContext().getSharedPreferences(androidContext().packageName + "_preferences", android.content.Context.MODE_PRIVATE) } + + single { AppSettings(androidContext()) } + + single { + Json { + serializersModule = FareBotSerializersModule + ignoreUnknownKeys = true + encodeDefaults = true + } + } + + single { KotlinxCardSerializer(get()) } + + single { KotlinxCardKeysSerializer(get()) } + + single { + val driver = AndroidSqliteDriver(FareBotDb.Schema, androidContext(), "farebot.db") + FareBotDb(driver) + } + + single { DbCardPersister(get()) } + + single { DbCardKeysPersister(get()) } + + single { NfcStream() } + + single { TagReaderFactory() } + + single { createAndroidTransitFactoryRegistry(androidContext()) } + + single { DefaultStringResource() } + + single { + AndroidCardScanner( + nfcStream = get(), + tagReaderFactory = get(), + cardKeysPersister = get(), + cardKeysSerializer = get(), + ) + } +} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/Array.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/kotlin/Array.kt similarity index 100% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/Array.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/kotlin/Array.kt diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/Color.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/kotlin/Color.kt similarity index 100% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/Color.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/kotlin/Color.kt diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/Date.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/kotlin/Date.kt similarity index 100% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/Date.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/kotlin/Date.kt diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/CardTypeGsonTypeAdapter.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/nfc/NfcStream.kt similarity index 61% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/CardTypeGsonTypeAdapter.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/nfc/NfcStream.kt index 024915537..3e6d227ff 100644 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/CardTypeGsonTypeAdapter.kt +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/nfc/NfcStream.kt @@ -1,5 +1,5 @@ /* - * CardTypeGsonTypeAdapter.kt + * NfcStream.kt * * This file is part of FareBot. * Learn more at: https://codebutler.github.io/farebot/ @@ -20,20 +20,22 @@ * along with this program. If not, see . */ -package com.codebutler.farebot.app.core.serialize.gson +package com.codebutler.farebot.app.core.nfc -import com.codebutler.farebot.card.CardType -import com.google.gson.TypeAdapter -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonWriter +import android.nfc.Tag +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +/** + * A singleton holder for NFC tag events. The Activity manages the NFC adapter lifecycle + * and emits tags here. ViewModels and other components observe this flow. + */ +class NfcStream { -class CardTypeGsonTypeAdapter : TypeAdapter() { + private val _tags = MutableSharedFlow(replay = 1) - override fun write(out: JsonWriter, value: CardType) { - out.value(value.name) - } + fun observe(): Flow = _tags - override fun read(`in`: JsonReader): CardType { - return CardType.valueOf(`in`.nextString()) + fun emitTag(tag: Tag) { + _tags.tryEmit(tag) } } diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/nfc/TagReaderFactory.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/nfc/TagReaderFactory.kt similarity index 100% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/nfc/TagReaderFactory.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/nfc/TagReaderFactory.kt diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/nfc/UnsupportedTagException.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/nfc/UnsupportedTagException.kt similarity index 100% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/nfc/UnsupportedTagException.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/nfc/UnsupportedTagException.kt diff --git a/farebot-android/src/main/java/com/codebutler/farebot/app/core/platform/AndroidPlatformActions.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/platform/AndroidPlatformActions.kt new file mode 100644 index 000000000..4974cb8e9 --- /dev/null +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/platform/AndroidPlatformActions.kt @@ -0,0 +1,173 @@ +package com.codebutler.farebot.app.core.platform + +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.Settings +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import com.codebutler.farebot.app.feature.bg.BackgroundTagActivity +import com.codebutler.farebot.shared.platform.PlatformActions + +class AndroidPlatformActions( + private val context: Context, +) : PlatformActions { + + private var filePickerCallback: ((String?) -> Unit)? = null + private var filePickerLauncher: ActivityResultLauncher? = null + private var bytesPickerCallback: ((ByteArray?) -> Unit)? = null + private var bytesPickerLauncher: ActivityResultLauncher? = null + + fun registerFilePickerLauncher(activity: ComponentActivity) { + filePickerLauncher = activity.registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + val callback = filePickerCallback + filePickerCallback = null + if (result.resultCode == Activity.RESULT_OK) { + val uri = result.data?.data + if (uri != null) { + val text = context.contentResolver.openInputStream(uri) + ?.bufferedReader() + ?.use { it.readText() } + callback?.invoke(text) + } else { + callback?.invoke(null) + } + } else { + callback?.invoke(null) + } + } + bytesPickerLauncher = activity.registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + val callback = bytesPickerCallback + bytesPickerCallback = null + if (result.resultCode == Activity.RESULT_OK) { + val uri = result.data?.data + if (uri != null) { + val bytes = context.contentResolver.openInputStream(uri) + ?.use { it.readBytes() } + callback?.invoke(bytes) + } else { + callback?.invoke(null) + } + } else { + callback?.invoke(null) + } + } + } + + override fun openUrl(url: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + + override fun openNfcSettings() { + val intent = Intent(Settings.ACTION_WIRELESS_SETTINGS) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + + override fun copyToClipboard(text: String) { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText("FareBot", text)) + } + + override fun getClipboardText(): String? { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + return clipboard.primaryClip?.getItemAt(0)?.text?.toString() + } + + override fun shareText(text: String) { + val intent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, text) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(Intent.createChooser(intent, "Share").apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }) + } + + override fun showToast(message: String) { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + + override fun pickFileForImport(onResult: (String?) -> Unit) { + val launcher = filePickerLauncher + if (launcher == null) { + onResult(null) + return + } + filePickerCallback = onResult + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/json" + putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/json", "text/plain", "text/*")) + } + launcher.launch(intent) + } + + override fun updateAppTimestamp() { + // Update last-used timestamp for the app + val prefs = context.getSharedPreferences("farebot", Context.MODE_PRIVATE) + prefs.edit().putLong("last_used", System.currentTimeMillis()).apply() + } + + override fun pickFileForBytes(onResult: (ByteArray?) -> Unit) { + val launcher = bytesPickerLauncher + if (launcher == null) { + onResult(null) + return + } + bytesPickerCallback = onResult + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + } + launcher.launch(intent) + } + + override fun saveFileForExport(content: String, defaultFileName: String) { + val intent = Intent(Intent.ACTION_SEND).apply { + type = "application/json" + putExtra(Intent.EXTRA_TEXT, content) + putExtra(Intent.EXTRA_SUBJECT, defaultFileName) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(Intent.createChooser(intent, "Save").apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }) + } + + override fun supportsLaunchFromBackground(): Boolean = true + + override fun isLaunchFromBackgroundEnabled(): Boolean { + val componentName = ComponentName(context, BackgroundTagActivity::class.java) + val state = context.packageManager.getComponentEnabledSetting(componentName) + return state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } + + override fun setLaunchFromBackgroundEnabled(enabled: Boolean) { + val componentName = ComponentName(context, BackgroundTagActivity::class.java) + val newState = if (enabled) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } else { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + context.packageManager.setComponentEnabledSetting( + componentName, + newState, + PackageManager.DONT_KILL_APP, + ) + } +} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/RawSampleCard.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/RawSampleCard.kt similarity index 84% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/RawSampleCard.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/RawSampleCard.kt index 1008ee58d..1f5fa74a4 100644 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/RawSampleCard.kt +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/RawSampleCard.kt @@ -22,19 +22,19 @@ package com.codebutler.farebot.app.core.sample -import com.codebutler.farebot.base.util.ByteArray import com.codebutler.farebot.card.CardType import com.codebutler.farebot.card.RawCard -import java.util.Date +import kotlin.time.Clock +import kotlin.time.Instant class RawSampleCard : RawCard { override fun cardType(): CardType = CardType.Sample - override fun tagId(): ByteArray = ByteArray.create(byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 0)) + override fun tagId(): ByteArray = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) - override fun scannedAt(): Date = Date() + override fun scannedAt(): Instant = Clock.System.now() override fun isUnauthorized(): Boolean = false diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleCard.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleCard.kt similarity index 81% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleCard.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleCard.kt index 8fce6c0bf..40c86b2fe 100644 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleCard.kt +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleCard.kt @@ -22,23 +22,22 @@ package com.codebutler.farebot.app.core.sample -import android.content.Context import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.ui.FareBotUiTree -import com.codebutler.farebot.base.util.ByteArray +import com.codebutler.farebot.base.util.StringResource import com.codebutler.farebot.card.Card import com.codebutler.farebot.card.CardType -import java.util.Date +import kotlin.time.Instant class SampleCard(private val rawCard: RawSampleCard) : Card() { - override fun getCardType(): CardType = rawCard.cardType() + override val cardType: CardType = rawCard.cardType() - override fun getTagId(): ByteArray = rawCard.tagId() + override val tagId: ByteArray = rawCard.tagId() - override fun getScannedAt(): Date = rawCard.scannedAt() + override val scannedAt: Instant = rawCard.scannedAt() - override fun getAdvancedUi(context: Context): FareBotUiTree = uiTree(context) { + override fun getAdvancedUi(stringResource: StringResource): FareBotUiTree = uiTree(stringResource) { item { title = "Sample Transit Section 1" item { diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleRefill.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleRefill.kt similarity index 78% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleRefill.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleRefill.kt index 078862bfa..9662a7139 100644 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleRefill.kt +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleRefill.kt @@ -22,7 +22,7 @@ package com.codebutler.farebot.app.core.sample -import android.content.res.Resources +import com.codebutler.farebot.base.util.StringResource import com.codebutler.farebot.transit.Refill import java.util.Date @@ -30,11 +30,11 @@ class SampleRefill(private val date: Date) : Refill() { override fun getTimestamp(): Long = date.time / 1000 - override fun getAgencyName(resources: Resources): String = "Agency" + override fun getAgencyName(stringResource: StringResource): String = "Agency" - override fun getShortAgencyName(resources: Resources): String = "Agency" + override fun getShortAgencyName(stringResource: StringResource): String = "Agency" override fun getAmount(): Long = 40L - override fun getAmountString(resources: Resources): String = "$40.00" + override fun getAmountString(stringResource: StringResource): String = "$40.00" } diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleSubscription.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleSubscription.kt similarity index 62% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleSubscription.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleSubscription.kt index 8dd8dd2b9..64e80015e 100644 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleSubscription.kt +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleSubscription.kt @@ -22,26 +22,25 @@ package com.codebutler.farebot.app.core.sample -import android.content.res.Resources -import com.codebutler.farebot.app.core.kotlin.date import com.codebutler.farebot.transit.Subscription -import java.util.Date +import kotlin.time.Instant +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn class SampleSubscription : Subscription() { - override fun getId(): Int = 1 + override val id: Int = 1 - override fun getValidFrom(): Date = date(2017, 6) + override val validFrom: Instant get() = LocalDate(2017, 6, 1).atStartOfDayIn(TimeZone.UTC) - override fun getValidTo(): Date = date(2017, 7) + override val validTo: Instant get() = LocalDate(2017, 7, 1).atStartOfDayIn(TimeZone.UTC) - override fun getAgencyName(resources: Resources): String = "Municipal Robot Railway" + override val agencyName: String get() = "Municipal Robot Railway" - override fun getShortAgencyName(resources: Resources): String = "Muni" + override val shortAgencyName: String get() = "Muni" - override fun getMachineId(): Int = 1 + override val machineId: Int = 1 - override fun getSubscriptionName(resources: Resources): String = "Monthly Pass" - - override fun getActivation(): String = "" + override val subscriptionName: String get() = "Monthly Pass" } diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitFactory.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitFactory.kt similarity index 96% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitFactory.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitFactory.kt index 96b4f6575..38e0f9de1 100644 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitFactory.kt +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitFactory.kt @@ -22,6 +22,7 @@ package com.codebutler.farebot.app.core.sample +import com.codebutler.farebot.base.util.hex import com.codebutler.farebot.transit.TransitFactory import com.codebutler.farebot.transit.TransitIdentity diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitInfo.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitInfo.kt similarity index 74% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitInfo.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitInfo.kt index 2d1c4944a..c4679ac3f 100644 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitInfo.kt +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleTransitInfo.kt @@ -22,39 +22,35 @@ package com.codebutler.farebot.app.core.sample -import android.content.Context -import android.content.res.Resources import com.codebutler.farebot.app.core.kotlin.date import com.codebutler.farebot.base.ui.uiTree import com.codebutler.farebot.base.ui.FareBotUiTree -import com.codebutler.farebot.transit.Refill +import com.codebutler.farebot.base.util.StringResource import com.codebutler.farebot.transit.Subscription +import com.codebutler.farebot.transit.TransitBalance +import com.codebutler.farebot.transit.TransitCurrency import com.codebutler.farebot.transit.TransitInfo import com.codebutler.farebot.transit.Trip class SampleTransitInfo : TransitInfo() { - override fun getBalanceString(resources: Resources): String = "$42.50" + override val balance: TransitBalance = TransitBalance(balance = TransitCurrency.USD(4250)) - override fun getSerialNumber(): String? = "1234567890" + override val serialNumber: String? = "1234567890" - override fun getTrips(): List = listOf( + override val trips: List = listOf( SampleTrip(date(2017, 6, 4, 19, 0)), SampleTrip(date(2017, 6, 5, 8, 0)), SampleTrip(date(2017, 6, 5, 16, 9)) ) - override fun getRefills(): List = listOf( - SampleRefill(date(2017, 6, 5, 16, 4)) - ) - - override fun getSubscriptions(): List = listOf( + override val subscriptions: List = listOf( SampleSubscription() ) - override fun getCardName(resources: Resources): String = "Sample Transit" + override val cardName: String = "Sample Transit" - override fun getAdvancedUi(context: Context): FareBotUiTree = uiTree(context) { + override fun getAdvancedUi(stringResource: StringResource): FareBotUiTree = uiTree(stringResource) { item { title = "Sample Card Section 1" item { diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleTrip.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleTrip.kt similarity index 53% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleTrip.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleTrip.kt index 5e9c6f2f4..49c5f9030 100644 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/sample/SampleTrip.kt +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/sample/SampleTrip.kt @@ -22,38 +22,29 @@ package com.codebutler.farebot.app.core.sample -import android.content.res.Resources import com.codebutler.farebot.transit.Station +import com.codebutler.farebot.transit.TransitCurrency import com.codebutler.farebot.transit.Trip +import kotlin.time.Instant import java.util.Date class SampleTrip(private val date: Date) : Trip() { - override fun getTimestamp(): Long = date.time / 1000 + override val startTimestamp: Instant get() = Instant.fromEpochMilliseconds(date.time) - override fun getExitTimestamp(): Long = date.time / 1000 + override val endTimestamp: Instant get() = Instant.fromEpochMilliseconds(date.time) - override fun getRouteName(resources: Resources): String? = "Route Name" + override val routeName: String get() = "Route Name" - override fun getAgencyName(resources: Resources): String? = "Agency" + override val agencyName: String get() = "Agency" - override fun getShortAgencyName(resources: Resources): String? = "Agency" + override val shortAgencyName: String get() = "Agency" - override fun getBalanceString(): String? = "$42.000" + override val fare: TransitCurrency get() = TransitCurrency.USD(420) - override fun getStartStationName(resources: Resources): String? = "Start Station" + override val startStation: Station get() = Station.create("Name", "Name", "", "") - override fun getStartStation(): Station? = Station.create("Name", "Name", "", "") + override val endStation: Station get() = Station.create("Name", "Name", "", "") - override fun hasFare(): Boolean = true - - override fun getFareString(resources: Resources): String? = "$4.20" - - override fun getEndStationName(resources: Resources): String? = "End Station" - - override fun getEndStation(): Station? = Station.create("Name", "Name", "", "") - - override fun getMode(): Mode? = Mode.METRO - - override fun hasTime(): Boolean = true + override val mode: Mode get() = Mode.METRO } diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/CardKeysSerializer.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/serialize/CardKeysSerializer.kt similarity index 100% rename from farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/CardKeysSerializer.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/core/serialize/CardKeysSerializer.kt diff --git a/farebot-android/src/main/java/com/codebutler/farebot/app/core/serialize/KotlinxCardKeysSerializer.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/serialize/KotlinxCardKeysSerializer.kt new file mode 100644 index 000000000..d172f5bab --- /dev/null +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/serialize/KotlinxCardKeysSerializer.kt @@ -0,0 +1,55 @@ +/* + * KotlinxCardKeysSerializer.kt + * + * This file is part of FareBot. + * Learn more at: https://codebutler.github.io/farebot/ + * + * Copyright (C) 2017 Eric Butler + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.codebutler.farebot.app.core.serialize + +import com.codebutler.farebot.card.CardType +import com.codebutler.farebot.card.classic.key.ClassicCardKeys +import com.codebutler.farebot.key.CardKeys +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +class KotlinxCardKeysSerializer(private val json: Json) : CardKeysSerializer { + + override fun serialize(cardKeys: CardKeys): String { + return when (cardKeys.cardType()) { + CardType.MifareClassic -> json.encodeToString( + ClassicCardKeys.serializer(), + cardKeys as ClassicCardKeys + ) + else -> throw IllegalArgumentException("Unknown card keys type: ${cardKeys.cardType()}") + } + } + + override fun deserialize(data: String): CardKeys { + val jsonObject = json.decodeFromString(JsonObject.serializer(), data) + val cardTypeName = jsonObject["cardType"]?.jsonPrimitive?.content + ?: throw IllegalArgumentException("Missing cardType field") + val cardType = CardType.valueOf(cardTypeName) + return when (cardType) { + CardType.MifareClassic -> json.decodeFromString(ClassicCardKeys.serializer(), data) + else -> throw IllegalArgumentException("Unknown card keys type: $cardType") + } + } +} diff --git a/farebot-android/src/main/java/com/codebutler/farebot/app/core/transit/TransitFactoryRegistry.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/transit/TransitFactoryRegistry.kt new file mode 100644 index 000000000..a99727e0e --- /dev/null +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/transit/TransitFactoryRegistry.kt @@ -0,0 +1,243 @@ +/* + * TransitFactoryRegistry.kt + * + * This file is part of FareBot. + * Learn more at: https://codebutler.github.io/farebot/ + * + * Copyright (C) 2017 Eric Butler + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.codebutler.farebot.app.core.transit + +import android.content.Context +import com.codebutler.farebot.shared.sample.SampleTransitFactory +import com.codebutler.farebot.app.core.util.AndroidStringResource +import com.codebutler.farebot.card.CardType +import com.codebutler.farebot.shared.transit.TransitFactoryRegistry +import com.codebutler.farebot.transit.bilhete_unico.BilheteUnicoSPTransitFactory +import com.codebutler.farebot.transit.clipper.ClipperTransitFactory +import com.codebutler.farebot.transit.easycard.EasyCardTransitFactory +import com.codebutler.farebot.transit.edy.EdyTransitFactory +import com.codebutler.farebot.transit.ezlink.EZLinkTransitFactory +import com.codebutler.farebot.transit.hsl.HSLTransitFactory +import com.codebutler.farebot.transit.manly_fast_ferry.ManlyFastFerryTransitFactory +import com.codebutler.farebot.transit.myki.MykiTransitFactory +import com.codebutler.farebot.transit.octopus.OctopusTransitFactory +import com.codebutler.farebot.transit.opal.OpalTransitFactory +import com.codebutler.farebot.transit.orca.OrcaTransitFactory +import com.codebutler.farebot.transit.ovc.OVChipTransitFactory +import com.codebutler.farebot.transit.seq_go.SeqGoTransitFactory +import com.codebutler.farebot.transit.suica.SuicaTransitFactory +import com.codebutler.farebot.transit.kmt.KMTTransitFactory +import com.codebutler.farebot.transit.mrtj.MRTJTransitFactory +import com.codebutler.farebot.transit.clipper.ClipperUltralightTransitFactory +import com.codebutler.farebot.transit.ultralight.TroikaUltralightTransitFactory +import com.codebutler.farebot.transit.ultralight.VeneziaUltralightTransitFactory +import com.codebutler.farebot.transit.ultralight.PisaUltralightTransitFactory +import com.codebutler.farebot.transit.ultralight.OVChipUltralightTransitFactory +import com.codebutler.farebot.transit.ultralight.HSLUltralightTransitFactory +import com.codebutler.farebot.transit.ultralight.MRTUltralightTransitFactory +import com.codebutler.farebot.transit.ultralight.AmiiboTransitFactory +import com.codebutler.farebot.transit.ultralight.BlankUltralightTransitFactory +import com.codebutler.farebot.transit.ultralight.LockedUltralightTransitFactory +import com.codebutler.farebot.transit.vicinity.BlankVicinityTransitFactory +import com.codebutler.farebot.transit.vicinity.UnknownVicinityTransitFactory +import com.codebutler.farebot.transit.calypso.opus.OpusTransitFactory +import com.codebutler.farebot.transit.calypso.ravkav.RavKavTransitFactory +import com.codebutler.farebot.transit.calypso.mobib.MobibTransitInfo +import com.codebutler.farebot.transit.calypso.venezia.VeneziaTransitFactory +import com.codebutler.farebot.transit.calypso.pisa.PisaTransitFactory +import com.codebutler.farebot.transit.calypso.lisboaviva.LisboaVivaTransitInfo +import com.codebutler.farebot.transit.calypso.emv.EmvTransitFactory +import com.codebutler.farebot.transit.calypso.intercode.IntercodeTransitFactory +import com.codebutler.farebot.transit.snapper.SnapperTransitFactory +import com.codebutler.farebot.transit.tmoney.TMoneyTransitFactory +import com.codebutler.farebot.transit.nextfareul.NextfareUnknownUltralightTransitInfo +import com.codebutler.farebot.transit.ventra.VentraUltralightTransitInfo +import com.codebutler.farebot.transit.yvr_compass.CompassUltralightTransitInfo +import com.codebutler.farebot.transit.msp_goto.MspGotoTransitFactory +import com.codebutler.farebot.transit.waikato.WaikatoCardTransitFactory +import com.codebutler.farebot.transit.troika.TroikaTransitFactory +import com.codebutler.farebot.transit.oyster.OysterTransitFactory +import com.codebutler.farebot.transit.charlie.CharlieCardTransitFactory +import com.codebutler.farebot.transit.gautrain.GautrainTransitFactory +import com.codebutler.farebot.transit.smartrider.SmartRiderTransitFactory +import com.codebutler.farebot.transit.podorozhnik.PodorozhnikTransitFactory +import com.codebutler.farebot.transit.touchngo.TouchnGoTransitFactory +import com.codebutler.farebot.transit.tfi_leap.LeapTransitFactory +import com.codebutler.farebot.transit.lax_tap.LaxTapTransitFactory +import com.codebutler.farebot.transit.ricaricami.RicaricaMiTransitFactory +import com.codebutler.farebot.transit.yargor.YarGorTransitFactory +import com.codebutler.farebot.transit.chc_metrocard.ChcMetrocardTransitFactory +import com.codebutler.farebot.transit.bip.BipTransitFactory +import com.codebutler.farebot.transit.bonobus.BonobusTransitFactory +import com.codebutler.farebot.transit.cifial.CifialTransitFactory +import com.codebutler.farebot.transit.kazan.KazanTransitFactory +import com.codebutler.farebot.transit.kiev.KievTransitFactory +import com.codebutler.farebot.transit.komuterlink.KomuterLinkTransitFactory +import com.codebutler.farebot.transit.metromoney.MetroMoneyTransitFactory +import com.codebutler.farebot.transit.metroq.MetroQTransitFactory +import com.codebutler.farebot.transit.otago.OtagoGoCardTransitFactory +import com.codebutler.farebot.transit.pilet.KievDigitalTransitFactory +import com.codebutler.farebot.transit.pilet.TartuTransitFactory +import com.codebutler.farebot.transit.selecta.SelectaFranceTransitFactory +import com.codebutler.farebot.transit.umarsh.UmarshTransitFactory +import com.codebutler.farebot.transit.warsaw.WarsawTransitFactory +import com.codebutler.farebot.transit.zolotayakorona.ZolotayaKoronaTransitFactory +import com.codebutler.farebot.transit.hafilat.HafilatTransitFactory +import com.codebutler.farebot.transit.intercard.IntercardTransitFactory +import com.codebutler.farebot.transit.magnacarta.MagnaCartaTransitFactory +import com.codebutler.farebot.transit.tampere.TampereTransitFactory +import com.codebutler.farebot.transit.serialonly.AtHopTransitFactory +import com.codebutler.farebot.transit.serialonly.HoloTransitFactory +import com.codebutler.farebot.transit.serialonly.IstanbulKartTransitFactory +import com.codebutler.farebot.transit.serialonly.NolTransitFactory +import com.codebutler.farebot.transit.serialonly.NorticTransitFactory +import com.codebutler.farebot.transit.serialonly.PrestoTransitFactory +import com.codebutler.farebot.transit.serialonly.TrimetHopTransitFactory +import com.codebutler.farebot.transit.serialonly.StrelkaTransitFactory +import com.codebutler.farebot.transit.serialonly.SunCardTransitFactory +import com.codebutler.farebot.transit.krocap.KROCAPTransitFactory +import com.codebutler.farebot.transit.ndef.NdefClassicTransitFactory +import com.codebutler.farebot.transit.ndef.NdefFelicaTransitFactory +import com.codebutler.farebot.transit.ndef.NdefUltralightTransitFactory +import com.codebutler.farebot.transit.ndef.NdefVicinityTransitFactory +import com.codebutler.farebot.transit.unknown.BlankClassicTransitFactory +import com.codebutler.farebot.transit.unknown.BlankDesfireTransitFactory +import com.codebutler.farebot.transit.unknown.UnauthorizedClassicTransitFactory +import com.codebutler.farebot.transit.unknown.UnauthorizedDesfireTransitFactory +import com.codebutler.farebot.transit.china.ChinaTransitRegistry + +fun createAndroidTransitFactoryRegistry(context: Context): TransitFactoryRegistry { + // Register China transit factories + ChinaTransitRegistry.registerAll() + val registry = TransitFactoryRegistry() + val stringResource = AndroidStringResource() + + // FeliCa factories + registry.registerFactory(CardType.FeliCa, SuicaTransitFactory(stringResource)) + registry.registerFactory(CardType.FeliCa, EdyTransitFactory(stringResource)) + registry.registerFactory(CardType.FeliCa, OctopusTransitFactory()) + registry.registerFactory(CardType.FeliCa, KMTTransitFactory()) + registry.registerFactory(CardType.FeliCa, MRTJTransitFactory()) + registry.registerFactory(CardType.FeliCa, NdefFelicaTransitFactory()) + + // DESFire factories + registry.registerFactory(CardType.MifareDesfire, OrcaTransitFactory(stringResource)) + registry.registerFactory(CardType.MifareDesfire, ClipperTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, HSLTransitFactory(stringResource)) + registry.registerFactory(CardType.MifareDesfire, OpalTransitFactory(stringResource)) + registry.registerFactory(CardType.MifareDesfire, MykiTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, LeapTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, HafilatTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, IntercardTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, MagnaCartaTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, TampereTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, AtHopTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, HoloTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, IstanbulKartTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, NolTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, NorticTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, PrestoTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, TrimetHopTransitFactory()) + // DESFire catch-all handlers (must be LAST for DESFire) + registry.registerFactory(CardType.MifareDesfire, BlankDesfireTransitFactory()) + registry.registerFactory(CardType.MifareDesfire, UnauthorizedDesfireTransitFactory()) + + // Classic factories + registry.registerFactory(CardType.MifareClassic, OVChipTransitFactory(stringResource)) + registry.registerFactory(CardType.MifareClassic, BilheteUnicoSPTransitFactory()) + registry.registerFactory(CardType.MifareClassic, ManlyFastFerryTransitFactory()) + registry.registerFactory(CardType.MifareClassic, SeqGoTransitFactory()) + registry.registerFactory(CardType.MifareClassic, EasyCardTransitFactory(stringResource)) + registry.registerFactory(CardType.MifareClassic, TroikaTransitFactory()) + registry.registerFactory(CardType.MifareClassic, OysterTransitFactory()) + registry.registerFactory(CardType.MifareClassic, CharlieCardTransitFactory()) + registry.registerFactory(CardType.MifareClassic, GautrainTransitFactory()) + registry.registerFactory(CardType.MifareClassic, SmartRiderTransitFactory(stringResource)) + registry.registerFactory(CardType.MifareClassic, PodorozhnikTransitFactory(stringResource)) + registry.registerFactory(CardType.MifareClassic, TouchnGoTransitFactory()) + registry.registerFactory(CardType.MifareClassic, LaxTapTransitFactory()) + registry.registerFactory(CardType.MifareClassic, RicaricaMiTransitFactory()) + registry.registerFactory(CardType.MifareClassic, YarGorTransitFactory()) + registry.registerFactory(CardType.MifareClassic, ChcMetrocardTransitFactory()) + registry.registerFactory(CardType.MifareClassic, KomuterLinkTransitFactory()) + registry.registerFactory(CardType.MifareClassic, BonobusTransitFactory()) + registry.registerFactory(CardType.MifareClassic, CifialTransitFactory()) + registry.registerFactory(CardType.MifareClassic, KazanTransitFactory()) + registry.registerFactory(CardType.MifareClassic, KievTransitFactory()) + registry.registerFactory(CardType.MifareClassic, KievDigitalTransitFactory()) + registry.registerFactory(CardType.MifareClassic, TartuTransitFactory()) + registry.registerFactory(CardType.MifareClassic, MetroMoneyTransitFactory()) + registry.registerFactory(CardType.MifareClassic, MetroQTransitFactory()) + registry.registerFactory(CardType.MifareClassic, OtagoGoCardTransitFactory()) + registry.registerFactory(CardType.MifareClassic, SelectaFranceTransitFactory()) + registry.registerFactory(CardType.MifareClassic, UmarshTransitFactory()) + registry.registerFactory(CardType.MifareClassic, WarsawTransitFactory()) + registry.registerFactory(CardType.MifareClassic, ZolotayaKoronaTransitFactory()) + registry.registerFactory(CardType.MifareClassic, BipTransitFactory()) + registry.registerFactory(CardType.MifareClassic, MspGotoTransitFactory()) + registry.registerFactory(CardType.MifareClassic, WaikatoCardTransitFactory()) + registry.registerFactory(CardType.MifareClassic, StrelkaTransitFactory()) + registry.registerFactory(CardType.MifareClassic, SunCardTransitFactory()) + registry.registerFactory(CardType.MifareClassic, NdefClassicTransitFactory()) + // Classic catch-all handlers (must be LAST for Classic) + registry.registerFactory(CardType.MifareClassic, BlankClassicTransitFactory()) + registry.registerFactory(CardType.MifareClassic, UnauthorizedClassicTransitFactory()) + + // ISO7816 / Calypso factories + registry.registerFactory(CardType.ISO7816, OpusTransitFactory(stringResource)) + registry.registerFactory(CardType.ISO7816, RavKavTransitFactory(stringResource)) + registry.registerFactory(CardType.ISO7816, MobibTransitInfo.Factory(stringResource)) + registry.registerFactory(CardType.ISO7816, VeneziaTransitFactory(stringResource)) + registry.registerFactory(CardType.ISO7816, PisaTransitFactory(stringResource)) + registry.registerFactory(CardType.ISO7816, LisboaVivaTransitInfo.Factory(stringResource)) + registry.registerFactory(CardType.ISO7816, IntercodeTransitFactory(stringResource)) + registry.registerFactory(CardType.ISO7816, SnapperTransitFactory()) + registry.registerFactory(CardType.ISO7816, TMoneyTransitFactory()) + registry.registerFactory(CardType.ISO7816, KROCAPTransitFactory()) + + // EMV contactless payment cards + registry.registerFactory(CardType.ISO7816, EmvTransitFactory) + + // CEPAS factories + registry.registerFactory(CardType.CEPAS, EZLinkTransitFactory(stringResource)) + + // Ultralight factories (order matters - specific checks first, catch-alls last) + registry.registerFactory(CardType.MifareUltralight, TroikaUltralightTransitFactory()) + registry.registerFactory(CardType.MifareUltralight, ClipperUltralightTransitFactory()) + registry.registerFactory(CardType.MifareUltralight, OVChipUltralightTransitFactory()) + registry.registerFactory(CardType.MifareUltralight, MRTUltralightTransitFactory()) + registry.registerFactory(CardType.MifareUltralight, VeneziaUltralightTransitFactory()) + registry.registerFactory(CardType.MifareUltralight, PisaUltralightTransitFactory()) + registry.registerFactory(CardType.MifareUltralight, AmiiboTransitFactory()) + registry.registerFactory(CardType.MifareUltralight, HSLUltralightTransitFactory()) + registry.registerFactory(CardType.MifareUltralight, VentraUltralightTransitInfo.FACTORY) + registry.registerFactory(CardType.MifareUltralight, CompassUltralightTransitInfo.FACTORY) + registry.registerFactory(CardType.MifareUltralight, NextfareUnknownUltralightTransitInfo.FACTORY) + registry.registerFactory(CardType.MifareUltralight, NdefUltralightTransitFactory()) + registry.registerFactory(CardType.MifareUltralight, BlankUltralightTransitFactory()) + registry.registerFactory(CardType.MifareUltralight, LockedUltralightTransitFactory()) + + // Vicinity / NFC-V factories + registry.registerFactory(CardType.Vicinity, NdefVicinityTransitFactory()) + registry.registerFactory(CardType.Vicinity, BlankVicinityTransitFactory()) + registry.registerFactory(CardType.Vicinity, UnknownVicinityTransitFactory()) + + registry.registerFactory(CardType.Sample, SampleTransitFactory()) + + return registry +} diff --git a/farebot-android/src/main/java/com/codebutler/farebot/app/core/util/AndroidStringResource.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/core/util/AndroidStringResource.kt new file mode 100644 index 000000000..bc499d815 --- /dev/null +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/core/util/AndroidStringResource.kt @@ -0,0 +1,14 @@ +package com.codebutler.farebot.app.core.util + +import com.codebutler.farebot.base.util.StringResource +import kotlinx.coroutines.runBlocking +import org.jetbrains.compose.resources.StringResource as ComposeStringResource +import org.jetbrains.compose.resources.getString as composeGetString + +class AndroidStringResource : StringResource { + override fun getString(resource: ComposeStringResource): String = + runBlocking { composeGetString(resource) } + + override fun getString(resource: ComposeStringResource, vararg formatArgs: Any): String = + runBlocking { composeGetString(resource, *formatArgs) } +} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/bg/BackgroundTagActivity.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/feature/bg/BackgroundTagActivity.kt similarity index 97% rename from farebot-app/src/main/java/com/codebutler/farebot/app/feature/bg/BackgroundTagActivity.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/feature/bg/BackgroundTagActivity.kt index dceb0f92d..63fb764e1 100644 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/bg/BackgroundTagActivity.kt +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/feature/bg/BackgroundTagActivity.kt @@ -33,7 +33,7 @@ class BackgroundTagActivity : Activity() { startActivity(Intent(this, MainActivity::class.java).apply { action = intent.action - putExtras(intent.extras) + putExtras(intent.extras!!) }) finish() diff --git a/farebot-android/src/main/java/com/codebutler/farebot/app/feature/home/AndroidCardScanner.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/feature/home/AndroidCardScanner.kt new file mode 100644 index 000000000..9a4e719dd --- /dev/null +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/feature/home/AndroidCardScanner.kt @@ -0,0 +1,94 @@ +package com.codebutler.farebot.app.feature.home + +import com.codebutler.farebot.app.core.nfc.NfcStream +import com.codebutler.farebot.app.core.nfc.TagReaderFactory +import com.codebutler.farebot.app.core.serialize.CardKeysSerializer +import com.codebutler.farebot.base.util.ByteUtils +import com.codebutler.farebot.card.RawCard +import com.codebutler.farebot.persist.CardKeysPersister +import com.codebutler.farebot.shared.nfc.CardScanner +import com.codebutler.farebot.shared.nfc.ScannedTag +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +/** + * Android implementation of [CardScanner] that wraps [NfcStream] and [TagReaderFactory]. + * + * Uses passive scanning via Android NFC foreground dispatch. Tags arrive through + * [NfcStream] when the Activity has NFC foreground dispatch enabled. + */ +class AndroidCardScanner( + private val nfcStream: NfcStream, + private val tagReaderFactory: TagReaderFactory, + private val cardKeysPersister: CardKeysPersister, + private val cardKeysSerializer: CardKeysSerializer, +) : CardScanner { + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + + private val _scannedCards = MutableSharedFlow>() + override val scannedCards: SharedFlow> = _scannedCards.asSharedFlow() + + private val _scanErrors = MutableSharedFlow() + override val scanErrors: SharedFlow = _scanErrors.asSharedFlow() + + private val _scannedTags = MutableSharedFlow() + override val scannedTags: SharedFlow = _scannedTags.asSharedFlow() + + private val _isScanning = MutableStateFlow(false) + override val isScanning: StateFlow = _isScanning.asStateFlow() + + private var isObserving = false + + fun startObservingTags() { + if (isObserving) return + isObserving = true + + scope.launch { + nfcStream.observe().collect { tag -> + val techList = tag.techList?.toList() ?: emptyList() + _scannedTags.emit(ScannedTag(id = tag.id, techList = techList)) + + _isScanning.value = true + try { + val cardKeys = getCardKeys(ByteUtils.getHexString(tag.id)) + val rawCard = tagReaderFactory.getTagReader(tag.id, tag, cardKeys).readTag() + if (rawCard.isUnauthorized()) { + throw CardUnauthorizedException() + } + _scannedCards.emit(rawCard) + } catch (error: Throwable) { + _scanErrors.emit(error) + } finally { + _isScanning.value = false + } + } + } + } + + override fun startActiveScan() { + // No-op on Android - uses passive NFC foreground dispatch + } + + override fun stopActiveScan() { + // No-op on Android + } + + private fun getCardKeys(tagId: String): com.codebutler.farebot.key.CardKeys? { + val savedKey = cardKeysPersister.getForTagId(tagId) ?: return null + return cardKeysSerializer.deserialize(savedKey.keyData) + } + + class CardUnauthorizedException : Throwable() { + override val message: String + get() = "Unauthorized" + } +} diff --git a/farebot-android/src/main/java/com/codebutler/farebot/app/feature/main/MainActivity.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/feature/main/MainActivity.kt new file mode 100644 index 000000000..897f40068 --- /dev/null +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/feature/main/MainActivity.kt @@ -0,0 +1,157 @@ +/* + * MainActivity.kt + * + * This file is part of FareBot. + * Learn more at: https://codebutler.github.io/farebot/ + * + * Copyright (C) 2017 Eric Butler + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.codebutler.farebot.app.feature.main + +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.Uri +import android.nfc.NfcAdapter +import android.nfc.Tag +import android.nfc.tech.IsoDep +import android.nfc.tech.MifareClassic +import android.nfc.tech.MifareUltralight +import android.nfc.tech.NfcF +import android.os.Build +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import com.codebutler.farebot.app.core.nfc.NfcStream +import com.codebutler.farebot.app.core.platform.AndroidPlatformActions +import com.codebutler.farebot.app.feature.home.AndroidCardScanner +import com.codebutler.farebot.app.feature.prefs.FareBotPreferenceActivity +import com.codebutler.farebot.card.CardType +import com.codebutler.farebot.shared.FareBotApp +import com.codebutler.farebot.shared.nfc.CardScanner +import com.codebutler.farebot.shared.serialize.CardImporter +import com.codebutler.farebot.shared.ui.screen.ALL_SUPPORTED_CARDS +import org.koin.android.ext.android.inject + +class MainActivity : ComponentActivity() { + + companion object { + private const val ACTION_TAG = "com.codebutler.farebot.ACTION_TAG" + private const val INTENT_EXTRA_TAG = "android.nfc.extra.TAG" + + private val TECH_LISTS = arrayOf( + arrayOf(IsoDep::class.java.name), + arrayOf(MifareClassic::class.java.name), + arrayOf(MifareUltralight::class.java.name), + arrayOf(NfcF::class.java.name)) + + private val SUPPORTED_CARDS = ALL_SUPPORTED_CARDS + } + + private val nfcStream: NfcStream by inject() + private val cardScanner: CardScanner by inject() + private val cardImporter: CardImporter by inject() + + private var nfcReceiver: BroadcastReceiver? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Start observing NFC tags + (cardScanner as? AndroidCardScanner)?.startObservingTags() + + // Handle initial tag from launch intent + if (savedInstanceState == null) { + @Suppress("DEPRECATION") + intent.getParcelableExtra(INTENT_EXTRA_TAG)?.let { tag -> + nfcStream.emitTag(tag) + } + handleFileIntent(intent) + } + + // Register broadcast receiver for NFC tags + nfcReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + @Suppress("DEPRECATION") + val tag = intent.getParcelableExtra(INTENT_EXTRA_TAG) + if (tag != null) { + nfcStream.emitTag(tag) + } + } + } + registerReceiver(nfcReceiver, IntentFilter(ACTION_TAG)) + + val supportsMifareClassic = packageManager.hasSystemFeature("com.nxp.mifare") + val platformActions = AndroidPlatformActions(this) + platformActions.registerFilePickerLauncher(this) + + setContent { + FareBotApp( + platformActions = platformActions, + supportedCards = SUPPORTED_CARDS, + isMifareClassicSupported = supportsMifareClassic, + onNavigateToPrefs = { + startActivity(FareBotPreferenceActivity.newIntent(this@MainActivity)) + }, + ) + } + } + + override fun onResume() { + super.onResume() + val intent = Intent(ACTION_TAG) + intent.`package` = packageName + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_MUTABLE + } else { + 0 + } + val pendingIntent = PendingIntent.getBroadcast(this, 0, intent, flags) + val nfcAdapter = NfcAdapter.getDefaultAdapter(this) + nfcAdapter?.enableForegroundDispatch(this, pendingIntent, null, TECH_LISTS) + } + + override fun onPause() { + super.onPause() + val nfcAdapter = NfcAdapter.getDefaultAdapter(this) + nfcAdapter?.disableForegroundDispatch(this) + } + + override fun onDestroy() { + super.onDestroy() + nfcReceiver?.let { unregisterReceiver(it) } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + handleFileIntent(intent) + } + + @Suppress("DEPRECATION") + private fun handleFileIntent(intent: Intent) { + val uri = intent.data + ?: intent.getParcelableExtra(Intent.EXTRA_STREAM) + ?: return + val text = contentResolver.openInputStream(uri)?.bufferedReader()?.use { it.readText() } + if (text != null) { + cardImporter.submitImport(text) + } + } +} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/prefs/FareBotPreferenceActivity.kt b/farebot-android/src/main/java/com/codebutler/farebot/app/feature/prefs/FareBotPreferenceActivity.kt similarity index 94% rename from farebot-app/src/main/java/com/codebutler/farebot/app/feature/prefs/FareBotPreferenceActivity.kt rename to farebot-android/src/main/java/com/codebutler/farebot/app/feature/prefs/FareBotPreferenceActivity.kt index 440af0e0b..e41dd7a5f 100644 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/prefs/FareBotPreferenceActivity.kt +++ b/farebot-android/src/main/java/com/codebutler/farebot/app/feature/prefs/FareBotPreferenceActivity.kt @@ -20,6 +20,8 @@ * along with this program. If not, see . */ +@file:Suppress("DEPRECATION") + package com.codebutler.farebot.app.feature.prefs import android.content.ComponentName @@ -36,7 +38,7 @@ import android.view.MenuItem import com.codebutler.farebot.R import com.codebutler.farebot.app.feature.bg.BackgroundTagActivity -@Suppress("DEPRECATION") +@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") class FareBotPreferenceActivity : PreferenceActivity(), Preference.OnPreferenceChangeListener { companion object { @@ -45,6 +47,7 @@ class FareBotPreferenceActivity : PreferenceActivity(), Preference.OnPreferenceC private lateinit var preferenceLaunchFromBackground: CheckBoxPreference + @Deprecated("Deprecated in Java") public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) addPreferencesFromResource(R.xml.prefs) @@ -56,6 +59,7 @@ class FareBotPreferenceActivity : PreferenceActivity(), Preference.OnPreferenceC preferenceLaunchFromBackground.onPreferenceChangeListener = this } + @Deprecated("Deprecated in Java") override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean { if (preference === preferenceLaunchFromBackground) { launchFromBgEnabled = newValue as Boolean @@ -64,6 +68,7 @@ class FareBotPreferenceActivity : PreferenceActivity(), Preference.OnPreferenceC return false } + @Deprecated("Deprecated in Java") override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { finish() diff --git a/farebot-app/src/main/res/drawable-hdpi/bilheteunicosp_card.png b/farebot-android/src/main/res/drawable-hdpi/bilheteunicosp_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/bilheteunicosp_card.png rename to farebot-android/src/main/res/drawable-hdpi/bilheteunicosp_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/clipper_card.png b/farebot-android/src/main/res/drawable-hdpi/clipper_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/clipper_card.png rename to farebot-android/src/main/res/drawable-hdpi/clipper_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/edy_card.png b/farebot-android/src/main/res/drawable-hdpi/edy_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/edy_card.png rename to farebot-android/src/main/res/drawable-hdpi/edy_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/ezlink_card.png b/farebot-android/src/main/res/drawable-hdpi/ezlink_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/ezlink_card.png rename to farebot-android/src/main/res/drawable-hdpi/ezlink_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/hsl_card.png b/farebot-android/src/main/res/drawable-hdpi/hsl_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/hsl_card.png rename to farebot-android/src/main/res/drawable-hdpi/hsl_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/icoca_card.png b/farebot-android/src/main/res/drawable-hdpi/icoca_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/icoca_card.png rename to farebot-android/src/main/res/drawable-hdpi/icoca_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/kmt_card.png b/farebot-android/src/main/res/drawable-hdpi/kmt_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/kmt_card.png rename to farebot-android/src/main/res/drawable-hdpi/kmt_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/manly_fast_ferry_card.png b/farebot-android/src/main/res/drawable-hdpi/manly_fast_ferry_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/manly_fast_ferry_card.png rename to farebot-android/src/main/res/drawable-hdpi/manly_fast_ferry_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/marker_end.png b/farebot-android/src/main/res/drawable-hdpi/marker_end.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/marker_end.png rename to farebot-android/src/main/res/drawable-hdpi/marker_end.png diff --git a/farebot-app/src/main/res/drawable-hdpi/marker_start.png b/farebot-android/src/main/res/drawable-hdpi/marker_start.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/marker_start.png rename to farebot-android/src/main/res/drawable-hdpi/marker_start.png diff --git a/farebot-app/src/main/res/drawable-hdpi/myki_card.png b/farebot-android/src/main/res/drawable-hdpi/myki_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/myki_card.png rename to farebot-android/src/main/res/drawable-hdpi/myki_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/nets_card.png b/farebot-android/src/main/res/drawable-hdpi/nets_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/nets_card.png rename to farebot-android/src/main/res/drawable-hdpi/nets_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/octopus_card.png b/farebot-android/src/main/res/drawable-hdpi/octopus_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/octopus_card.png rename to farebot-android/src/main/res/drawable-hdpi/octopus_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/opal_card.png b/farebot-android/src/main/res/drawable-hdpi/opal_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/opal_card.png rename to farebot-android/src/main/res/drawable-hdpi/opal_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/orca_card.png b/farebot-android/src/main/res/drawable-hdpi/orca_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/orca_card.png rename to farebot-android/src/main/res/drawable-hdpi/orca_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/ovchip_card.png b/farebot-android/src/main/res/drawable-hdpi/ovchip_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/ovchip_card.png rename to farebot-android/src/main/res/drawable-hdpi/ovchip_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/pasmo_card.png b/farebot-android/src/main/res/drawable-hdpi/pasmo_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/pasmo_card.png rename to farebot-android/src/main/res/drawable-hdpi/pasmo_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/seqgo_card.png b/farebot-android/src/main/res/drawable-hdpi/seqgo_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/seqgo_card.png rename to farebot-android/src/main/res/drawable-hdpi/seqgo_card.png diff --git a/farebot-app/src/main/res/drawable-hdpi/suica_card.png b/farebot-android/src/main/res/drawable-hdpi/suica_card.png similarity index 100% rename from farebot-app/src/main/res/drawable-hdpi/suica_card.png rename to farebot-android/src/main/res/drawable-hdpi/suica_card.png diff --git a/farebot-app/src/main/res/drawable-xhdpi/easycard.png b/farebot-android/src/main/res/drawable-xhdpi/easycard.png similarity index 100% rename from farebot-app/src/main/res/drawable-xhdpi/easycard.png rename to farebot-android/src/main/res/drawable-xhdpi/easycard.png diff --git a/farebot-app/src/main/res/drawable/fg_item_selectable.xml b/farebot-android/src/main/res/drawable/fg_item_selectable.xml similarity index 100% rename from farebot-app/src/main/res/drawable/fg_item_selectable.xml rename to farebot-android/src/main/res/drawable/fg_item_selectable.xml diff --git a/farebot-app/src/main/res/drawable/ic_add_black_24dp.xml b/farebot-android/src/main/res/drawable/ic_add_black_24dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_add_black_24dp.xml rename to farebot-android/src/main/res/drawable/ic_add_black_24dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_delete_black_24dp.xml b/farebot-android/src/main/res/drawable/ic_delete_black_24dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_delete_black_24dp.xml rename to farebot-android/src/main/res/drawable/ic_delete_black_24dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_help_outline_grey_24dp.xml b/farebot-android/src/main/res/drawable/ic_help_outline_grey_24dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_help_outline_grey_24dp.xml rename to farebot-android/src/main/res/drawable/ic_help_outline_grey_24dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_history_grey_24dp.xml b/farebot-android/src/main/res/drawable/ic_history_grey_24dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_history_grey_24dp.xml rename to farebot-android/src/main/res/drawable/ic_history_grey_24dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_lock_black_24dp.xml b/farebot-android/src/main/res/drawable/ic_lock_black_24dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_lock_black_24dp.xml rename to farebot-android/src/main/res/drawable/ic_lock_black_24dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_transaction_banned_32dp.xml b/farebot-android/src/main/res/drawable/ic_transaction_banned_32dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_transaction_banned_32dp.xml rename to farebot-android/src/main/res/drawable/ic_transaction_banned_32dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_transaction_bus_32dp.xml b/farebot-android/src/main/res/drawable/ic_transaction_bus_32dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_transaction_bus_32dp.xml rename to farebot-android/src/main/res/drawable/ic_transaction_bus_32dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_transaction_ferry_32dp.xml b/farebot-android/src/main/res/drawable/ic_transaction_ferry_32dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_transaction_ferry_32dp.xml rename to farebot-android/src/main/res/drawable/ic_transaction_ferry_32dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_transaction_handheld_32dp.xml b/farebot-android/src/main/res/drawable/ic_transaction_handheld_32dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_transaction_handheld_32dp.xml rename to farebot-android/src/main/res/drawable/ic_transaction_handheld_32dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_transaction_metro_32dp.xml b/farebot-android/src/main/res/drawable/ic_transaction_metro_32dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_transaction_metro_32dp.xml rename to farebot-android/src/main/res/drawable/ic_transaction_metro_32dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_transaction_pos_32dp.xml b/farebot-android/src/main/res/drawable/ic_transaction_pos_32dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_transaction_pos_32dp.xml rename to farebot-android/src/main/res/drawable/ic_transaction_pos_32dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_transaction_train_32dp.xml b/farebot-android/src/main/res/drawable/ic_transaction_train_32dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_transaction_train_32dp.xml rename to farebot-android/src/main/res/drawable/ic_transaction_train_32dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_transaction_tram_32dp.xml b/farebot-android/src/main/res/drawable/ic_transaction_tram_32dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_transaction_tram_32dp.xml rename to farebot-android/src/main/res/drawable/ic_transaction_tram_32dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_transaction_tvm_32dp.xml b/farebot-android/src/main/res/drawable/ic_transaction_tvm_32dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_transaction_tvm_32dp.xml rename to farebot-android/src/main/res/drawable/ic_transaction_tvm_32dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_transaction_unknown_32dp.xml b/farebot-android/src/main/res/drawable/ic_transaction_unknown_32dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_transaction_unknown_32dp.xml rename to farebot-android/src/main/res/drawable/ic_transaction_unknown_32dp.xml diff --git a/farebot-app/src/main/res/drawable/ic_transaction_vend_32dp.xml b/farebot-android/src/main/res/drawable/ic_transaction_vend_32dp.xml similarity index 100% rename from farebot-app/src/main/res/drawable/ic_transaction_vend_32dp.xml rename to farebot-android/src/main/res/drawable/ic_transaction_vend_32dp.xml diff --git a/farebot-app/src/main/res/drawable/img_home_splash.xml b/farebot-android/src/main/res/drawable/img_home_splash.xml similarity index 100% rename from farebot-app/src/main/res/drawable/img_home_splash.xml rename to farebot-android/src/main/res/drawable/img_home_splash.xml diff --git a/farebot-app/src/main/res/mipmap-xhdpi/ic_launcher.png b/farebot-android/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from farebot-app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to farebot-android/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/farebot-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/farebot-android/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from farebot-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to farebot-android/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/farebot-app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/farebot-android/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from farebot-app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to farebot-android/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/farebot-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/farebot-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from farebot-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to farebot-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/farebot-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/farebot-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from farebot-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to farebot-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/farebot-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/farebot-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from farebot-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to farebot-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/farebot-app/src/main/res/values-fi/strings.xml b/farebot-android/src/main/res/values-fi/strings.xml similarity index 100% rename from farebot-app/src/main/res/values-fi/strings.xml rename to farebot-android/src/main/res/values-fi/strings.xml diff --git a/farebot-app/src/main/res/values-fr/strings-location.xml b/farebot-android/src/main/res/values-fr/strings-location.xml similarity index 100% rename from farebot-app/src/main/res/values-fr/strings-location.xml rename to farebot-android/src/main/res/values-fr/strings-location.xml diff --git a/farebot-app/src/main/res/values-fr/strings.xml b/farebot-android/src/main/res/values-fr/strings.xml similarity index 100% rename from farebot-app/src/main/res/values-fr/strings.xml rename to farebot-android/src/main/res/values-fr/strings.xml diff --git a/farebot-app/src/main/res/values-iw/strings.xml b/farebot-android/src/main/res/values-iw/strings.xml similarity index 100% rename from farebot-app/src/main/res/values-iw/strings.xml rename to farebot-android/src/main/res/values-iw/strings.xml diff --git a/farebot-app/src/main/res/values-ja/strings-location.xml b/farebot-android/src/main/res/values-ja/strings-location.xml similarity index 100% rename from farebot-app/src/main/res/values-ja/strings-location.xml rename to farebot-android/src/main/res/values-ja/strings-location.xml diff --git a/farebot-app/src/main/res/values-ja/strings.xml b/farebot-android/src/main/res/values-ja/strings.xml similarity index 100% rename from farebot-app/src/main/res/values-ja/strings.xml rename to farebot-android/src/main/res/values-ja/strings.xml diff --git a/farebot-app/src/main/res/values-nl/strings-location.xml b/farebot-android/src/main/res/values-nl/strings-location.xml similarity index 100% rename from farebot-app/src/main/res/values-nl/strings-location.xml rename to farebot-android/src/main/res/values-nl/strings-location.xml diff --git a/farebot-app/src/main/res/values-nl/strings.xml b/farebot-android/src/main/res/values-nl/strings.xml similarity index 100% rename from farebot-app/src/main/res/values-nl/strings.xml rename to farebot-android/src/main/res/values-nl/strings.xml diff --git a/farebot-app/src/main/res/values/colors.xml b/farebot-android/src/main/res/values/colors.xml similarity index 100% rename from farebot-app/src/main/res/values/colors.xml rename to farebot-android/src/main/res/values/colors.xml diff --git a/farebot-app/src/main/res/values/dimens.xml b/farebot-android/src/main/res/values/dimens.xml similarity index 100% rename from farebot-app/src/main/res/values/dimens.xml rename to farebot-android/src/main/res/values/dimens.xml diff --git a/farebot-app/src/main/res/values/plurals.xml b/farebot-android/src/main/res/values/plurals.xml similarity index 100% rename from farebot-app/src/main/res/values/plurals.xml rename to farebot-android/src/main/res/values/plurals.xml diff --git a/farebot-app/src/main/res/values/strings-location.xml b/farebot-android/src/main/res/values/strings-location.xml similarity index 100% rename from farebot-app/src/main/res/values/strings-location.xml rename to farebot-android/src/main/res/values/strings-location.xml diff --git a/farebot-app/src/main/res/values/strings.xml b/farebot-android/src/main/res/values/strings.xml similarity index 92% rename from farebot-app/src/main/res/values/strings.xml rename to farebot-android/src/main/res/values/strings.xml index a8d3a0694..ba2dfc557 100644 --- a/farebot-app/src/main/res/values/strings.xml +++ b/farebot-android/src/main/res/values/strings.xml @@ -70,4 +70,9 @@ Delete Key Data Import proxmark3 key dump + Error + NFC is disabled + NFC Settings + Scan a card to get started + No keys added yet. diff --git a/farebot-app/src/main/res/values/styles.xml b/farebot-android/src/main/res/values/styles.xml similarity index 100% rename from farebot-app/src/main/res/values/styles.xml rename to farebot-android/src/main/res/values/styles.xml diff --git a/farebot-app/src/main/res/values/themes.xml b/farebot-android/src/main/res/values/themes.xml similarity index 100% rename from farebot-app/src/main/res/values/themes.xml rename to farebot-android/src/main/res/values/themes.xml diff --git a/farebot-app/src/main/res/xml/filter_nfc.xml b/farebot-android/src/main/res/xml/filter_nfc.xml similarity index 100% rename from farebot-app/src/main/res/xml/filter_nfc.xml rename to farebot-android/src/main/res/xml/filter_nfc.xml diff --git a/farebot-app/src/main/res/xml/prefs.xml b/farebot-android/src/main/res/xml/prefs.xml similarity index 100% rename from farebot-app/src/main/res/xml/prefs.xml rename to farebot-android/src/main/res/xml/prefs.xml diff --git a/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/CardKeysPersister.kt b/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/CardKeysPersister.kt new file mode 100644 index 000000000..8aaa91d7d --- /dev/null +++ b/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/CardKeysPersister.kt @@ -0,0 +1,10 @@ +package com.codebutler.farebot.persist + +import com.codebutler.farebot.persist.db.model.SavedKey + +interface CardKeysPersister { + fun getSavedKeys(): List + fun getForTagId(tagId: String): SavedKey? + fun insert(savedKey: SavedKey): Long + fun delete(savedKey: SavedKey) +} diff --git a/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/CardPersister.kt b/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/CardPersister.kt new file mode 100644 index 000000000..41b535e27 --- /dev/null +++ b/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/CardPersister.kt @@ -0,0 +1,32 @@ +/* + * CardPersister.kt + * + * This file is part of FareBot. + * Learn more at: https://codebutler.github.io/farebot/ + * + * Copyright (C) 2016 Eric Butler + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.codebutler.farebot.persist + +import com.codebutler.farebot.persist.db.model.SavedCard + +interface CardPersister { + fun getCards(): List + fun getCard(id: Long): SavedCard? + fun insertCard(card: SavedCard): Long + fun deleteCard(card: SavedCard) +} diff --git a/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/DbCardKeysPersister.kt b/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/DbCardKeysPersister.kt new file mode 100644 index 000000000..22c34929e --- /dev/null +++ b/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/DbCardKeysPersister.kt @@ -0,0 +1,37 @@ +package com.codebutler.farebot.persist.db + +import com.codebutler.farebot.card.CardType +import com.codebutler.farebot.persist.CardKeysPersister +import com.codebutler.farebot.persist.db.model.SavedKey +import kotlin.time.Instant + +class DbCardKeysPersister(private val db: FareBotDb) : CardKeysPersister { + + override fun getSavedKeys(): List = + db.savedKeyQueries.selectAll().executeAsList().map { it.toSavedKey() } + + override fun getForTagId(tagId: String): SavedKey? = + db.savedKeyQueries.selectByCardId(tagId).executeAsOneOrNull()?.toSavedKey() + + override fun insert(savedKey: SavedKey): Long { + db.savedKeyQueries.insert( + card_id = savedKey.cardId, + card_type = savedKey.cardType.name, + key_data = savedKey.keyData, + created_at = savedKey.createdAt.toEpochMilliseconds() + ) + return db.savedKeyQueries.selectAll().executeAsList().firstOrNull()?.id ?: -1 + } + + override fun delete(savedKey: SavedKey) { + db.savedKeyQueries.deleteById(savedKey.id) + } +} + +private fun Keys.toSavedKey() = SavedKey( + id = id, + cardId = card_id, + cardType = CardType.valueOf(card_type), + keyData = key_data, + createdAt = Instant.fromEpochMilliseconds(created_at) +) diff --git a/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/DbCardPersister.kt b/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/DbCardPersister.kt new file mode 100644 index 000000000..9e7fd71c1 --- /dev/null +++ b/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/DbCardPersister.kt @@ -0,0 +1,37 @@ +package com.codebutler.farebot.persist.db + +import com.codebutler.farebot.card.CardType +import com.codebutler.farebot.persist.CardPersister +import com.codebutler.farebot.persist.db.model.SavedCard +import kotlin.time.Instant + +class DbCardPersister(private val db: FareBotDb) : CardPersister { + + override fun getCards(): List = + db.savedCardQueries.selectAll().executeAsList().map { it.toSavedCard() } + + override fun getCard(id: Long): SavedCard? = + db.savedCardQueries.selectById(id).executeAsOneOrNull()?.toSavedCard() + + override fun insertCard(card: SavedCard): Long { + db.savedCardQueries.insert( + type = card.type.name, + serial = card.serial, + data_ = card.data, + scanned_at = card.scannedAt.toEpochMilliseconds() + ) + return db.savedCardQueries.selectAll().executeAsList().firstOrNull()?.id ?: -1 + } + + override fun deleteCard(card: SavedCard) { + db.savedCardQueries.deleteById(card.id) + } +} + +private fun Cards.toSavedCard() = SavedCard( + id = id, + type = CardType.valueOf(type), + serial = serial, + data = data_, + scannedAt = Instant.fromEpochMilliseconds(scanned_at) +) diff --git a/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/model/SavedCard.kt b/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/model/SavedCard.kt new file mode 100644 index 000000000..cdc43ddd3 --- /dev/null +++ b/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/model/SavedCard.kt @@ -0,0 +1,13 @@ +package com.codebutler.farebot.persist.db.model + +import com.codebutler.farebot.card.CardType +import kotlin.time.Clock +import kotlin.time.Instant + +data class SavedCard( + val id: Long = 0, + val type: CardType, + val serial: String, + val data: String, + val scannedAt: Instant = Clock.System.now() +) diff --git a/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/model/SavedKey.kt b/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/model/SavedKey.kt new file mode 100644 index 000000000..f484aab49 --- /dev/null +++ b/farebot-app-persist/src/commonMain/kotlin/com/codebutler/farebot/persist/db/model/SavedKey.kt @@ -0,0 +1,13 @@ +package com.codebutler.farebot.persist.db.model + +import com.codebutler.farebot.card.CardType +import kotlin.time.Clock +import kotlin.time.Instant + +data class SavedKey( + val id: Long = 0, + val cardId: String, + val cardType: CardType, + val keyData: String, + val createdAt: Instant = Clock.System.now() +) diff --git a/farebot-app-persist/src/commonMain/sqldelight/com/codebutler/farebot/persist/db/SavedCard.sq b/farebot-app-persist/src/commonMain/sqldelight/com/codebutler/farebot/persist/db/SavedCard.sq new file mode 100644 index 000000000..8c27238b6 --- /dev/null +++ b/farebot-app-persist/src/commonMain/sqldelight/com/codebutler/farebot/persist/db/SavedCard.sq @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS cards ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + type TEXT NOT NULL, + serial TEXT NOT NULL, + data TEXT NOT NULL, + scanned_at INTEGER NOT NULL +); + +selectAll: +SELECT * FROM cards ORDER BY scanned_at DESC; + +selectById: +SELECT * FROM cards WHERE id = ?; + +insert: +INSERT INTO cards (type, serial, data, scanned_at) VALUES (?, ?, ?, ?); + +deleteById: +DELETE FROM cards WHERE id = ?; diff --git a/farebot-app-persist/src/commonMain/sqldelight/com/codebutler/farebot/persist/db/SavedKey.sq b/farebot-app-persist/src/commonMain/sqldelight/com/codebutler/farebot/persist/db/SavedKey.sq new file mode 100644 index 000000000..101c22a55 --- /dev/null +++ b/farebot-app-persist/src/commonMain/sqldelight/com/codebutler/farebot/persist/db/SavedKey.sq @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + card_id TEXT NOT NULL, + card_type TEXT NOT NULL, + key_data TEXT NOT NULL, + created_at INTEGER NOT NULL +); + +selectAll: +SELECT * FROM keys ORDER BY created_at DESC; + +selectByCardId: +SELECT * FROM keys WHERE card_id = ?; + +insert: +INSERT INTO keys (card_id, card_type, key_data, created_at) VALUES (?, ?, ?, ?); + +deleteById: +DELETE FROM keys WHERE id = ?; diff --git a/farebot-app-persist/src/main/AndroidManifest.xml b/farebot-app-persist/src/main/AndroidManifest.xml deleted file mode 100644 index c195e65c0..000000000 --- a/farebot-app-persist/src/main/AndroidManifest.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/CardKeysPersister.java b/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/CardKeysPersister.java deleted file mode 100644 index 61c6d60d4..000000000 --- a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/CardKeysPersister.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.codebutler.farebot.persist; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.codebutler.farebot.persist.db.model.SavedKey; - -import java.util.List; - -public interface CardKeysPersister { - - @NonNull - List getSavedKeys(); - - @Nullable - SavedKey getForTagId(@NonNull String tagId); - - long insert(@NonNull SavedKey savedKey); - - void delete(@NonNull SavedKey savedKey); -} diff --git a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/CardPersister.java b/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/CardPersister.java deleted file mode 100644 index b531707b6..000000000 --- a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/CardPersister.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * CardPersister.java - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2016 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.persist; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.codebutler.farebot.persist.db.model.SavedCard; - -import java.util.List; - -public interface CardPersister { - - @NonNull - List getCards(); - - @Nullable - SavedCard getCard(long id); - - long insertCard(@NonNull SavedCard card); - - void deleteCard(@NonNull SavedCard card); -} diff --git a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/DbCardKeysPersister.kt b/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/DbCardKeysPersister.kt deleted file mode 100644 index 13aa55cfc..000000000 --- a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/DbCardKeysPersister.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.codebutler.farebot.persist.db - -import com.codebutler.farebot.persist.CardKeysPersister -import com.codebutler.farebot.persist.db.model.SavedKey - -class DbCardKeysPersister(private val db: FareBotDb) : CardKeysPersister { - override fun getSavedKeys(): List = db.savedKeyDao().selectAll() - override fun getForTagId(tagId: String): SavedKey? = db.savedKeyDao().selectByCardId(tagId) - override fun insert(savedKey: SavedKey): Long = db.savedKeyDao().insert(savedKey) - override fun delete(savedKey: SavedKey) = db.savedKeyDao().delete(savedKey) -} diff --git a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/DbCardPersister.kt b/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/DbCardPersister.kt deleted file mode 100644 index a39934d91..000000000 --- a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/DbCardPersister.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.codebutler.farebot.persist.db - -import com.codebutler.farebot.persist.CardPersister -import com.codebutler.farebot.persist.db.model.SavedCard - -class DbCardPersister(private val db: FareBotDb) : CardPersister { - override fun getCards(): List = db.savedCardDao().selectAll() - override fun getCard(id: Long): SavedCard? = db.savedCardDao().selectById(id) - override fun insertCard(savedCard: SavedCard): Long = db.savedCardDao().insert(savedCard) - override fun deleteCard(savedCard: SavedCard) = db.savedCardDao().delete(savedCard) -} diff --git a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/FareBotDb.kt b/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/FareBotDb.kt deleted file mode 100644 index a2570a04f..000000000 --- a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/FareBotDb.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.codebutler.farebot.persist.db - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase -import com.codebutler.farebot.persist.db.dao.SavedCardDao -import com.codebutler.farebot.persist.db.dao.SavedKeyDao -import com.codebutler.farebot.persist.db.model.SavedCard -import com.codebutler.farebot.persist.db.model.SavedKey - -private const val DATABASE_NAME = "farebot.db" - -@Database(entities = [SavedCard::class, SavedKey::class], version = 3, exportSchema = true) -@TypeConverters(FareBotDbConverters::class) -abstract class FareBotDb : RoomDatabase() { - abstract fun savedCardDao(): SavedCardDao - abstract fun savedKeyDao(): SavedKeyDao - - companion object { - @Volatile private var instance: FareBotDb? = null - - fun getInstance(context: Context): FareBotDb = instance ?: synchronized(this) { - instance ?: buildDatabase(context).also { instance = it } - } - - private fun buildDatabase(context: Context): FareBotDb = - Room.databaseBuilder(context, FareBotDb::class.java, DATABASE_NAME) - .addMigrations(object : Migration(1, 2) { - override fun migrate(database: SupportSQLiteDatabase) { - // Migration from Sqldelight to Room. Nothing to change. - } - }) - .addMigrations(object : Migration(2, 3) { - override fun migrate(database: SupportSQLiteDatabase) { - // Re-create tables with new NOT NULL `id` column. - database.beginTransaction() - try { - database.execSQL(""" - CREATE TABLE `cards_new` ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - `type` TEXT NOT NULL, - `serial` TEXT NOT NULL, - `data` TEXT NOT NULL, - `scanned_at` INTEGER NOT NULL - ); - """.trimIndent()) - - database.execSQL(""" - CREATE TABLE `keys_new` ( - `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - `card_id` TEXT NOT NULL, - `card_type` TEXT NOT NULL, - `key_data` TEXT NOT NULL, - `created_at` INTEGER NOT NULL - ); - """.trimIndent()) - - database.execSQL(""" - INSERT INTO `cards_new` (type, serial, data, scanned_at) - SELECT type, serial, data, scanned_at FROM cards; - """.trimIndent()) - - database.execSQL(""" - INSERT INTO `keys_new` (card_id, card_type, key_data, created_at) - SELECT card_id, card_type, key_data, created_at FROM keys; - """.trimIndent()) - - database.execSQL("DROP TABLE `cards`;") - database.execSQL("DROP TABLE `keys`;") - - database.execSQL("ALTER TABLE `cards_new` RENAME TO `cards`;") - database.execSQL("ALTER TABLE `keys_new` RENAME TO `keys`;") - - database.setTransactionSuccessful() - } finally { - database.endTransaction() - } - } - }) - .build() - } -} diff --git a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/FareBotDbConverters.kt b/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/FareBotDbConverters.kt deleted file mode 100644 index ae2b1fbc5..000000000 --- a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/FareBotDbConverters.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.codebutler.farebot.persist.db - -import androidx.room.TypeConverter -import com.codebutler.farebot.card.CardType -import java.util.Date - -@Suppress("unused") -class FareBotDbConverters { - @TypeConverter - fun cardTypeToString(cardType: CardType) = cardType.name - - @TypeConverter - fun stringToCardType(cardTypeName: String) = CardType.valueOf(cardTypeName) - - @TypeConverter - fun dateToLong(date: Date) = date.time - - @TypeConverter - fun longToDate(dateValue: Long) = Date(dateValue) -} diff --git a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/dao/SavedCardDao.kt b/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/dao/SavedCardDao.kt deleted file mode 100644 index 6248c3ed9..000000000 --- a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/dao/SavedCardDao.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.codebutler.farebot.persist.db.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.Query -import com.codebutler.farebot.persist.db.model.SavedCard - -@Dao -interface SavedCardDao { - @Query("SELECT * FROM cards ORDER BY scanned_at DESC") - fun selectAll(): List - - @Query("SELECT * FROM cards WHERE id = :id") - fun selectById(id: Long): SavedCard? - - @Insert - fun insert(savedCard: SavedCard): Long - - @Delete - fun delete(savedCard: SavedCard) -} diff --git a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/dao/SavedKeyDao.kt b/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/dao/SavedKeyDao.kt deleted file mode 100644 index 5d0031fe0..000000000 --- a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/dao/SavedKeyDao.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.codebutler.farebot.persist.db.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.Query -import com.codebutler.farebot.persist.db.model.SavedKey - -@Dao -interface SavedKeyDao { - @Query("SELECT * FROM keys ORDER BY created_at DESC") - fun selectAll(): List - - @Query("SELECT * FROM keys WHERE card_id = :cardId") - fun selectByCardId(cardId: String): SavedKey? - - @Insert - fun insert(savedKey: SavedKey): Long - - @Delete - fun delete(savedKey: SavedKey) -} diff --git a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/model/SavedCard.kt b/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/model/SavedCard.kt deleted file mode 100644 index 4728a8c96..000000000 --- a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/model/SavedCard.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.codebutler.farebot.persist.db.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.codebutler.farebot.card.CardType -import java.util.Date - -@Entity(tableName = "cards") -data class SavedCard( - @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, - @ColumnInfo(name = "type") val type: CardType, - @ColumnInfo(name = "serial") val serial: String, - @ColumnInfo(name = "data") val data: String, - @ColumnInfo(name = "scanned_at") val scannedAt: Date = Date() -) diff --git a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/model/SavedKey.kt b/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/model/SavedKey.kt deleted file mode 100644 index 488b1fa13..000000000 --- a/farebot-app-persist/src/main/java/com/codebutler/farebot/persist/db/model/SavedKey.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.codebutler.farebot.persist.db.model - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import com.codebutler.farebot.card.CardType -import java.util.Date - -@Entity(tableName = "keys") -data class SavedKey( - @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, - @ColumnInfo(name = "card_id") val cardId: String, - @ColumnInfo(name = "card_type") val cardType: CardType, - @ColumnInfo(name = "key_data") val keyData: String, - @ColumnInfo(name = "created_at") val createdAt: Date = Date() -) diff --git a/farebot-app/build.gradle b/farebot-app/build.gradle deleted file mode 100644 index df5263f40..000000000 --- a/farebot-app/build.gradle +++ /dev/null @@ -1,135 +0,0 @@ -/* - * build.gradle - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -apply plugin: 'com.android.application' -apply plugin: 'io.fabric' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' -apply plugin: 'kotlin-kapt' - -dependencies { - implementation project(':farebot-base') - implementation project(':farebot-app-persist') - implementation project(':farebot-card') - implementation project(':farebot-card-cepas') - implementation project(':farebot-card-classic') - implementation project(':farebot-card-desfire') - implementation project(':farebot-card-desfire') - implementation project(':farebot-card-felica') - implementation project(':farebot-card-ultralight') - implementation project(':farebot-transit') - implementation project(':farebot-transit-bilhete') - implementation project(':farebot-transit-clipper') - implementation project(':farebot-transit-easycard') - implementation project(':farebot-transit-edy') - implementation project(':farebot-transit-kmt') - implementation project(':farebot-transit-ezlink') - implementation project(':farebot-transit-hsl') - implementation project(':farebot-transit-manly') - implementation project(':farebot-transit-myki') - implementation project(':farebot-transit-octopus') - implementation project(':farebot-transit-opal') - implementation project(':farebot-transit-orca') - implementation project(':farebot-transit-ovc') - implementation project(':farebot-transit-seqgo') - implementation project(':farebot-transit-stub') - implementation project(':farebot-transit-suica') - - implementation libs.autoDispose - implementation libs.autoDisposeAndroid - implementation libs.autoDisposeAndroidKotlin - implementation libs.autoDisposeKotlin - implementation libs.groupie - implementation libs.groupieDatabinding - implementation libs.gson - implementation libs.kotlinStdlib - implementation libs.magellan - implementation libs.playServicesMaps - implementation libs.rxBroadcast - implementation libs.rxJava2 - implementation libs.rxRelay2 - implementation libs.supportDesign - implementation libs.supportV4 - implementation libs.supportV7CardView - implementation libs.supportV7RecyclerView - - implementation(libs.crashlytics) { - transitive = true - } - - implementation libs.dagger - kapt libs.daggerCompiler -} - -static def askPassword() { - return 'security -q find-generic-password -w -g -l farebot-release'.execute().text.trim() -} - -gradle.taskGraph.whenReady { taskGraph -> - if(taskGraph.hasTask(':farebot-app:packageRelease')) { - def password = askPassword() - android.signingConfigs.release.storePassword = password - android.signingConfigs.release.keyPassword = password - } -} - -kapt { - useBuildCache = true - mapDiagnosticLocations = true -} - -android { - defaultConfig { - versionCode 29 - versionName '3.1.1' - multiDexEnabled true - } - - signingConfigs { - debug { - storeFile file('../debug.keystore') - } - release { - storeFile file('../release.keystore') - keyAlias 'ericbutler' - storePassword '' - keyPassword '' - } - } - - buildTypes { - debug { - ext.enableCrashlytics = false - } - release { - shrinkResources false - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), '../config/proguard/proguard-rules.pro' - signingConfig signingConfigs.release - } - } - - packagingOptions { - exclude 'META-INF/LICENSE.txt' - exclude 'META-INF/NOTICE.txt' - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/activity/ActivityOperations.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/activity/ActivityOperations.kt deleted file mode 100644 index a83d1e5f8..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/activity/ActivityOperations.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * ActivityOperations.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.activity - -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ActionMode -import android.view.MenuItem -import io.reactivex.Observable - -/** - * interface for screens to interact with parent activity. - */ -class ActivityOperations( - private val activity: AppCompatActivity, - val activityResult: Observable, - val menuItemClick: Observable, - val permissionResult: Observable -) { - - fun startActionMode(callback: ActionMode.Callback): ActionMode? { - return activity.startSupportActionMode(callback) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplicationComponent.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplicationComponent.kt deleted file mode 100644 index b5c22d43a..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplicationComponent.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * FareBotApplicationComponent.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.app - -import android.content.SharedPreferences -import com.codebutler.farebot.app.core.nfc.TagReaderFactory -import com.codebutler.farebot.app.core.serialize.CardKeysSerializer -import com.codebutler.farebot.app.core.transit.TransitFactoryRegistry -import com.codebutler.farebot.app.core.util.ExportHelper -import com.codebutler.farebot.card.serialize.CardSerializer -import com.codebutler.farebot.persist.CardKeysPersister -import com.codebutler.farebot.persist.CardPersister -import dagger.BindsInstance -import dagger.Component - -@Component(modules = arrayOf(FareBotApplicationModule::class)) -interface FareBotApplicationComponent { - - fun application(): FareBotApplication - - fun cardPersister(): CardPersister - - fun cardSerializer(): CardSerializer - - fun cardKeysPersister(): CardKeysPersister - - fun cardKeysSerializer(): CardKeysSerializer - - fun exportHelper(): ExportHelper - - fun sharedPreferences(): SharedPreferences - - fun tagReaderFactory(): TagReaderFactory - - fun transitFactoryRegistry(): TransitFactoryRegistry - - fun inject(application: FareBotApplication) - - @Component.Builder - interface Builder { - - fun module(module: FareBotApplicationModule): Builder - - @BindsInstance - fun application(application: FareBotApplication): Builder - - fun build(): FareBotApplicationComponent - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplicationModule.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplicationModule.kt deleted file mode 100644 index 225d6f336..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/app/FareBotApplicationModule.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * FareBotApplicationModule.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.app - -import android.content.SharedPreferences -import android.preference.PreferenceManager -import com.codebutler.farebot.app.core.nfc.TagReaderFactory -import com.codebutler.farebot.app.core.serialize.CardKeysSerializer -import com.codebutler.farebot.app.core.serialize.gson.ByteArrayGsonTypeAdapter -import com.codebutler.farebot.app.core.serialize.gson.CardKeysGsonTypeAdapterFactory -import com.codebutler.farebot.app.core.serialize.gson.CardTypeGsonTypeAdapter -import com.codebutler.farebot.app.core.serialize.gson.EpochDateTypeAdapter -import com.codebutler.farebot.app.core.serialize.gson.GsonCardKeysSerializer -import com.codebutler.farebot.app.core.serialize.gson.GsonCardSerializer -import com.codebutler.farebot.app.core.serialize.gson.RawCardGsonTypeAdapterFactory -import com.codebutler.farebot.app.core.transit.TransitFactoryRegistry -import com.codebutler.farebot.app.core.util.ExportHelper -import com.codebutler.farebot.base.util.ByteArray -import com.codebutler.farebot.card.CardType -import com.codebutler.farebot.card.cepas.CEPASTypeAdapterFactory -import com.codebutler.farebot.card.classic.ClassicTypeAdapterFactory -import com.codebutler.farebot.card.desfire.DesfireTypeAdapterFactory -import com.codebutler.farebot.card.felica.FelicaTypeAdapterFactory -import com.codebutler.farebot.card.serialize.CardSerializer -import com.codebutler.farebot.card.ultralight.UltralightTypeAdapterFactory -import com.codebutler.farebot.persist.CardKeysPersister -import com.codebutler.farebot.persist.CardPersister -import com.codebutler.farebot.persist.db.DbCardKeysPersister -import com.codebutler.farebot.persist.db.DbCardPersister -import com.codebutler.farebot.persist.db.FareBotDb -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import dagger.Module -import dagger.Provides -import java.util.Date - -@Module -class FareBotApplicationModule { - - @Provides - fun provideSharedPreferences(application: FareBotApplication): SharedPreferences = - PreferenceManager.getDefaultSharedPreferences(application) - - @Provides - fun provideGson(): Gson = GsonBuilder() - .registerTypeAdapter(Date::class.java, EpochDateTypeAdapter()) - .registerTypeAdapterFactory(CEPASTypeAdapterFactory.create()) - .registerTypeAdapterFactory(ClassicTypeAdapterFactory.create()) - .registerTypeAdapterFactory(DesfireTypeAdapterFactory.create()) - .registerTypeAdapterFactory(FelicaTypeAdapterFactory.create()) - .registerTypeAdapterFactory(UltralightTypeAdapterFactory.create()) - .registerTypeAdapterFactory(RawCardGsonTypeAdapterFactory()) - .registerTypeAdapterFactory(CardKeysGsonTypeAdapterFactory()) - .registerTypeAdapter(ByteArray::class.java, ByteArrayGsonTypeAdapter()) - .registerTypeAdapter(CardType::class.java, CardTypeGsonTypeAdapter()) - .create() - - @Provides - fun provideCardSerializer(gson: Gson): CardSerializer = GsonCardSerializer(gson) - - @Provides - fun provideCardKeysSerializer(gson: Gson): CardKeysSerializer = GsonCardKeysSerializer(gson) - - @Provides - fun provideFareBotDb(application: FareBotApplication): FareBotDb = FareBotDb.getInstance(application) - - @Provides - fun provideCardPersister(db: FareBotDb): CardPersister = DbCardPersister(db) - - @Provides - fun provideCardKeysPersister(db: FareBotDb): CardKeysPersister = DbCardKeysPersister(db) - - @Provides - fun provideExportHelper(cardPersister: CardPersister, cardSerializer: CardSerializer, gson: Gson): ExportHelper = - ExportHelper(cardPersister, cardSerializer, gson) - - @Provides - fun provideTagReaderFactory(): TagReaderFactory { - return TagReaderFactory() - } - - @Provides - fun provideTransitFactoryRegistry(application: FareBotApplication): TransitFactoryRegistry = - TransitFactoryRegistry(application) -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/KotterKnife.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/KotterKnife.kt deleted file mode 100644 index 88dd3eee7..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/KotterKnife.kt +++ /dev/null @@ -1,164 +0,0 @@ -// from https://github.com/JakeWharton/kotterknife - -@file:Suppress("unused", "UNUSED_ANONYMOUS_PARAMETER") - -package com.codebutler.farebot.app.core.kotlin - -import android.app.Activity -import android.app.Dialog -import android.app.DialogFragment -import android.app.Fragment -import androidx.recyclerview.widget.RecyclerView.ViewHolder -import android.view.View -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty -import androidx.fragment.app.DialogFragment as SupportDialogFragment -import androidx.fragment.app.Fragment as SupportFragment - -/* ktlint-disable colon-spacing */ - -fun View.bindView(id: Int) - : ReadOnlyProperty = required(id, viewFinder) - -fun Activity.bindView(id: Int) - : ReadOnlyProperty = required(id, viewFinder) - -fun Dialog.bindView(id: Int) - : ReadOnlyProperty = required(id, viewFinder) - -fun DialogFragment.bindView(id: Int) - : ReadOnlyProperty = required(id, viewFinder) - -fun SupportDialogFragment.bindView(id: Int) - : ReadOnlyProperty = required(id, viewFinder) - -fun Fragment.bindView(id: Int) - : ReadOnlyProperty = required(id, viewFinder) - -fun SupportFragment.bindView(id: Int) - : ReadOnlyProperty = required(id, viewFinder) - -fun ViewHolder.bindView(id: Int) - : ReadOnlyProperty = required(id, viewFinder) - -fun View.bindOptionalView(id: Int) - : ReadOnlyProperty = optional(id, viewFinder) - -fun Activity.bindOptionalView(id: Int) - : ReadOnlyProperty = optional(id, viewFinder) - -fun Dialog.bindOptionalView(id: Int) - : ReadOnlyProperty = optional(id, viewFinder) - -fun DialogFragment.bindOptionalView(id: Int) - : ReadOnlyProperty = optional(id, viewFinder) - -fun SupportDialogFragment.bindOptionalView(id: Int) - : ReadOnlyProperty = optional(id, viewFinder) - -fun Fragment.bindOptionalView(id: Int) - : ReadOnlyProperty = optional(id, viewFinder) - -fun SupportFragment.bindOptionalView(id: Int) - : ReadOnlyProperty = optional(id, viewFinder) - -fun ViewHolder.bindOptionalView(id: Int) - : ReadOnlyProperty = optional(id, viewFinder) - -fun View.bindViews(vararg ids: Int) - : ReadOnlyProperty> = required(ids, viewFinder) - -fun Activity.bindViews(vararg ids: Int) - : ReadOnlyProperty> = required(ids, viewFinder) - -fun Dialog.bindViews(vararg ids: Int) - : ReadOnlyProperty> = required(ids, viewFinder) - -fun DialogFragment.bindViews(vararg ids: Int) - : ReadOnlyProperty> = required(ids, viewFinder) - -fun SupportDialogFragment.bindViews(vararg ids: Int) - : ReadOnlyProperty> = required(ids, viewFinder) - -fun Fragment.bindViews(vararg ids: Int) - : ReadOnlyProperty> = required(ids, viewFinder) - -fun SupportFragment.bindViews(vararg ids: Int) - : ReadOnlyProperty> = required(ids, viewFinder) - -fun ViewHolder.bindViews(vararg ids: Int) - : ReadOnlyProperty> = required(ids, viewFinder) - -fun View.bindOptionalViews(vararg ids: Int) - : ReadOnlyProperty> = optional(ids, viewFinder) - -fun Activity.bindOptionalViews(vararg ids: Int) - : ReadOnlyProperty> = optional(ids, viewFinder) - -fun Dialog.bindOptionalViews(vararg ids: Int) - : ReadOnlyProperty> = optional(ids, viewFinder) - -fun DialogFragment.bindOptionalViews(vararg ids: Int) - : ReadOnlyProperty> = optional(ids, viewFinder) - -fun SupportDialogFragment.bindOptionalViews(vararg ids: Int) - : ReadOnlyProperty> = optional(ids, viewFinder) - -fun Fragment.bindOptionalViews(vararg ids: Int) - : ReadOnlyProperty> = optional(ids, viewFinder) - -fun SupportFragment.bindOptionalViews(vararg ids: Int) - : ReadOnlyProperty> = optional(ids, viewFinder) - -fun ViewHolder.bindOptionalViews(vararg ids: Int) - : ReadOnlyProperty> = optional(ids, viewFinder) - -private val View.viewFinder: View.(Int) -> View? - get() = { findViewById(it) } -private val Activity.viewFinder: Activity.(Int) -> View? - get() = { findViewById(it) } -private val Dialog.viewFinder: Dialog.(Int) -> View? - get() = { findViewById(it) } -private val DialogFragment.viewFinder: DialogFragment.(Int) -> View? - get() = { dialog?.findViewById(it) ?: view?.findViewById(it) } -private val SupportDialogFragment.viewFinder: SupportDialogFragment.(Int) -> View? - get() = { dialog?.findViewById(it) ?: view?.findViewById(it) } -private val Fragment.viewFinder: Fragment.(Int) -> View? - get() = { view.findViewById(it) } -private val SupportFragment.viewFinder: SupportFragment.(Int) -> View? - get() = { view!!.findViewById(it) } -private val ViewHolder.viewFinder: ViewHolder.(Int) -> View? - get() = { itemView.findViewById(it) } - -private fun viewNotFound(id: Int, desc: KProperty<*>): Nothing = - throw IllegalStateException("View ID $id for '${desc.name}' not found.") - -@Suppress("UNCHECKED_CAST") -private fun required(id: Int, finder: T.(Int) -> View?) = - Lazy { t: T, desc -> t.finder(id) as V? ?: viewNotFound(id, desc) } - -@Suppress("UNCHECKED_CAST") -private fun optional(id: Int, finder: T.(Int) -> View?) = - Lazy { t: T, desc -> t.finder(id) as V? } - -@Suppress("UNCHECKED_CAST") -private fun required(ids: IntArray, finder: T.(Int) -> View?) = - Lazy { t: T, desc -> ids.map { t.finder(it) as V? ?: viewNotFound(it, desc) } } - -@Suppress("UNCHECKED_CAST") -private fun optional(ids: IntArray, finder: T.(Int) -> View?) = - Lazy { t: T, desc -> ids.map { t.finder(it) as V? }.filterNotNull() } - -// Like Kotlin's lazy delegate but the initializer gets the target and metadata passed to it -private class Lazy(private val initializer: (T, KProperty<*>) -> V) : ReadOnlyProperty { - private object EMPTY - private var value: Any? = EMPTY - - override fun getValue(thisRef: T, property: KProperty<*>): V { - if (value == EMPTY) { - value = initializer(thisRef, property) - } - @Suppress("UNCHECKED_CAST") - return value as V - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/Optional.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/Optional.kt deleted file mode 100644 index a7a60b6f7..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/Optional.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Optional.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.kotlin - -import io.reactivex.Maybe -import io.reactivex.Observable -import io.reactivex.Single - -fun Observable>.filterAndGetOptional(): Observable = this - .filter { it.isPresent } - .map { it.get } - -fun Single>.filterAndGetOptional(): Maybe = this - .filter { it.isPresent } - .map { it.get } - -data class Optional(val value: T?) { - val isPresent: Boolean - get() = value != null - - val get: T - get() = value!! -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/ViewGroupExtensions.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/ViewGroupExtensions.kt deleted file mode 100644 index d681d6ff4..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/kotlin/ViewGroupExtensions.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * ViewGroupExtensions.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.kotlin - -import androidx.annotation.LayoutRes -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup - -fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View = - LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot) diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/nfc/NfcStream.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/nfc/NfcStream.kt deleted file mode 100644 index c08c6456f..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/nfc/NfcStream.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * NfcStream.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.nfc - -import android.app.Activity -import android.app.PendingIntent -import android.content.Intent -import android.content.IntentFilter -import android.nfc.NfcAdapter -import android.nfc.Tag -import android.nfc.tech.IsoDep -import android.nfc.tech.MifareClassic -import android.nfc.tech.MifareUltralight -import android.nfc.tech.NfcF -import android.os.Bundle -import com.cantrowitz.rxbroadcast.RxBroadcast -import com.codebutler.farebot.app.core.rx.LastValueRelay -import io.reactivex.Observable - -class NfcStream(private val activity: Activity) { - - companion object { - private val ACTION = "com.codebutler.farebot.ACTION_TAG" - private val INTENT_EXTRA_TAG = "android.nfc.extra.TAG" - - private val TECH_LISTS = arrayOf( - arrayOf(IsoDep::class.java.name), - arrayOf(MifareClassic::class.java.name), - arrayOf(MifareUltralight::class.java.name), - arrayOf(NfcF::class.java.name)) - } - - private val relay = LastValueRelay.create() - - fun onCreate(activity: Activity, savedInstanceState: Bundle?) { - if (savedInstanceState == null) { - activity.intent.getParcelableExtra(INTENT_EXTRA_TAG)?.let { - relay.accept(it) - } - } - } - - fun onResume() { - val intent = Intent(ACTION) - intent.`package` = activity.packageName - - val pendingIntent = PendingIntent.getBroadcast(activity, 0, intent, 0) - val nfcAdapter = NfcAdapter.getDefaultAdapter(activity) - nfcAdapter?.enableForegroundDispatch(activity, pendingIntent, null, TECH_LISTS) - } - - fun onPause() { - val nfcAdapter = NfcAdapter.getDefaultAdapter(activity) - nfcAdapter?.disableForegroundDispatch(activity) - } - - fun observe(): Observable { - val broadcastIntents = RxBroadcast.fromBroadcast(activity, IntentFilter(ACTION)) - .map { it.getParcelableExtra(INTENT_EXTRA_TAG) } - return Observable.merge(relay, broadcastIntents) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/rx/LastValueRelay.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/rx/LastValueRelay.kt deleted file mode 100644 index b7fa89287..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/rx/LastValueRelay.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * LastItemRelay.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.rx - -import com.jakewharton.rxrelay2.PublishRelay -import com.jakewharton.rxrelay2.Relay -import io.reactivex.Observer -import java.util.concurrent.atomic.AtomicReference - -class LastValueRelay private constructor() : Relay() { - - private val relay = PublishRelay.create() - private val lastValue: AtomicReference = AtomicReference() - - companion object { - fun create(): LastValueRelay = LastValueRelay() - } - - override fun accept(value: T) { - if (hasObservers()) { - relay.accept(value) - } else { - lastValue.set(value) - } - } - - override fun hasObservers(): Boolean = relay.hasObservers() - - override fun subscribeActual(observer: Observer) { - lastValue.getAndSet(null)?.let(observer::onNext) - relay.subscribe(observer) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/CardKeysGsonTypeAdapterFactory.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/CardKeysGsonTypeAdapterFactory.kt deleted file mode 100644 index 5eed20d1c..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/CardKeysGsonTypeAdapterFactory.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * CardKeysGsonTypeAdapterFactory.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.serialize.gson - -import com.codebutler.farebot.card.CardType -import com.codebutler.farebot.card.classic.key.ClassicCardKeys -import com.codebutler.farebot.key.CardKeys -import com.google.gson.Gson -import com.google.gson.TypeAdapter -import com.google.gson.TypeAdapterFactory -import com.google.gson.internal.Streams -import com.google.gson.reflect.TypeToken -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonWriter -import java.util.HashMap - -class CardKeysGsonTypeAdapterFactory : TypeAdapterFactory { - - companion object { - private val KEY_CARD_TYPE = "cardType" - - private val CLASSES = mapOf( - CardType.MifareClassic to ClassicCardKeys::class.java - ) - } - - @Suppress("UNCHECKED_CAST") - override fun create(gson: Gson, type: TypeToken): TypeAdapter? { - if (!CardKeys::class.java.isAssignableFrom(type.rawType)) { - return null - } - val delegates = HashMap>() - for ((key, value) in CLASSES) { - delegates.put(key, gson.getDelegateAdapter(this, TypeToken.get(value) as TypeToken)) - } - return CardKeysTypeAdapter(delegates) as TypeAdapter - } - - private inner class CardKeysTypeAdapter internal constructor( - private val delegates: Map> - ) : TypeAdapter() { - - override fun write(out: JsonWriter, value: CardKeys) { - val delegateAdapter = delegates[value.cardType()] - ?: throw IllegalArgumentException("Unknown type: ${value.cardType()}") - val jsonObject = delegateAdapter.toJsonTree(value).asJsonObject - Streams.write(jsonObject, out) - } - - override fun read(inJsonReader: JsonReader): CardKeys { - val rootElement = Streams.parse(inJsonReader) - val typeElement = rootElement.asJsonObject.get(KEY_CARD_TYPE) - val cardType = CardType.valueOf(typeElement.asString) - val delegateAdapter = delegates[cardType] - ?: throw IllegalArgumentException("Unknown type: $cardType") - return delegateAdapter.fromJsonTree(rootElement) - } - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/GsonCardKeysSerializer.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/GsonCardKeysSerializer.kt deleted file mode 100644 index 6862925d0..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/GsonCardKeysSerializer.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * GsonCardKeysSerializer.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.serialize.gson - -import com.codebutler.farebot.app.core.serialize.CardKeysSerializer -import com.codebutler.farebot.key.CardKeys -import com.google.gson.Gson - -class GsonCardKeysSerializer(private val gson: Gson) : CardKeysSerializer { - - override fun serialize(cardKeys: CardKeys): String { - return gson.toJson(cardKeys) - } - - override fun deserialize(data: String): CardKeys { - return gson.fromJson(data, CardKeys::class.java) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/RawCardGsonTypeAdapterFactory.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/RawCardGsonTypeAdapterFactory.kt deleted file mode 100644 index 63224e2c9..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/serialize/gson/RawCardGsonTypeAdapterFactory.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * RawCardGsonTypeAdapterFactory.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.serialize.gson - -import com.codebutler.farebot.app.core.sample.RawSampleCard -import com.codebutler.farebot.card.CardType -import com.codebutler.farebot.card.RawCard -import com.codebutler.farebot.card.cepas.raw.RawCEPASCard -import com.codebutler.farebot.card.classic.raw.RawClassicCard -import com.codebutler.farebot.card.desfire.raw.RawDesfireCard -import com.codebutler.farebot.card.felica.raw.RawFelicaCard -import com.codebutler.farebot.card.ultralight.raw.RawUltralightCard -import com.google.gson.Gson -import com.google.gson.JsonPrimitive -import com.google.gson.TypeAdapter -import com.google.gson.TypeAdapterFactory -import com.google.gson.internal.Streams -import com.google.gson.reflect.TypeToken -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonWriter -import java.util.HashMap - -class RawCardGsonTypeAdapterFactory : TypeAdapterFactory { - - companion object { - private val KEY_CARD_TYPE = "cardType" - - private val CLASSES = mapOf( - CardType.MifareDesfire to RawDesfireCard::class.java, - CardType.MifareClassic to RawClassicCard::class.java, - CardType.MifareUltralight to RawUltralightCard::class.java, - CardType.CEPAS to RawCEPASCard::class.java, - CardType.FeliCa to RawFelicaCard::class.java, - CardType.Sample to RawSampleCard::class.java) - } - - @Suppress("UNCHECKED_CAST") - override fun create(gson: Gson, type: TypeToken): TypeAdapter? { - if (!RawCard::class.java.isAssignableFrom(type.rawType)) { - return null - } - val delegates = HashMap>>() - for ((key, value) in CLASSES) { - delegates.put(key, gson.getDelegateAdapter(this, TypeToken.get(value) as TypeToken>)) - } - return RawCardTypeAdapter(delegates) as TypeAdapter - } - - private class RawCardTypeAdapter internal constructor( - private val delegates: Map>> - ) : TypeAdapter>() { - - override fun write(out: JsonWriter, value: RawCard<*>) { - val delegateAdapter = delegates[value.cardType()] - ?: throw IllegalArgumentException("Unknown type: ${value.cardType()}") - val jsonObject = delegateAdapter.toJsonTree(value).asJsonObject - jsonObject.add(KEY_CARD_TYPE, JsonPrimitive(value.cardType().name)) - Streams.write(jsonObject, out) - } - - override fun read(inJsonReader: JsonReader): RawCard<*> { - val rootElement = Streams.parse(inJsonReader) - val typeElement = rootElement.asJsonObject.remove(KEY_CARD_TYPE) - val cardType = CardType.valueOf(typeElement.asString) - val delegateAdapter = delegates[cardType] - ?: throw IllegalArgumentException("Unknown type: $cardType") - return delegateAdapter.fromJsonTree(rootElement) - } - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/transit/TransitFactoryRegistry.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/transit/TransitFactoryRegistry.kt deleted file mode 100644 index 7c0c445f8..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/transit/TransitFactoryRegistry.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * TransitFactoryRegistry.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.transit - -import android.content.Context -import com.codebutler.farebot.app.core.sample.SampleCard -import com.codebutler.farebot.app.core.sample.SampleTransitFactory -import com.codebutler.farebot.card.Card -import com.codebutler.farebot.card.cepas.CEPASCard -import com.codebutler.farebot.card.classic.ClassicCard -import com.codebutler.farebot.card.desfire.DesfireCard -import com.codebutler.farebot.card.felica.FelicaCard -import com.codebutler.farebot.transit.TransitFactory -import com.codebutler.farebot.transit.TransitIdentity -import com.codebutler.farebot.transit.TransitInfo -import com.codebutler.farebot.transit.bilhete_unico.BilheteUnicoSPTransitFactory -import com.codebutler.farebot.transit.clipper.ClipperTransitFactory -import com.codebutler.farebot.transit.easycard.EasyCardTransitFactory -import com.codebutler.farebot.transit.edy.EdyTransitFactory -import com.codebutler.farebot.transit.ezlink.EZLinkTransitFactory -import com.codebutler.farebot.transit.hsl.HSLTransitFactory -import com.codebutler.farebot.transit.manly_fast_ferry.ManlyFastFerryTransitFactory -import com.codebutler.farebot.transit.myki.MykiTransitFactory -import com.codebutler.farebot.transit.octopus.OctopusTransitFactory -import com.codebutler.farebot.transit.opal.OpalTransitFactory -import com.codebutler.farebot.transit.orca.OrcaTransitFactory -import com.codebutler.farebot.transit.ovc.OVChipTransitFactory -import com.codebutler.farebot.transit.seq_go.SeqGoTransitFactory -import com.codebutler.farebot.transit.stub.AdelaideMetrocardStubTransitFactory -import com.codebutler.farebot.transit.stub.AtHopStubTransitFactory -import com.codebutler.farebot.transit.suica.SuicaTransitFactory -import com.codebutler.farebot.transit.kmt.KMTTransitFactory - -class TransitFactoryRegistry(context: Context) { - - private val registry = mutableMapOf, MutableList>>() - - init { - registerFactory(FelicaCard::class.java, SuicaTransitFactory(context)) - registerFactory(FelicaCard::class.java, EdyTransitFactory()) - registerFactory(FelicaCard::class.java, OctopusTransitFactory()) - registerFactory(FelicaCard::class.java, KMTTransitFactory()) - - registerFactory(DesfireCard::class.java, OrcaTransitFactory()) - registerFactory(DesfireCard::class.java, ClipperTransitFactory()) - registerFactory(DesfireCard::class.java, HSLTransitFactory()) - registerFactory(DesfireCard::class.java, OpalTransitFactory()) - registerFactory(DesfireCard::class.java, MykiTransitFactory()) - registerFactory(DesfireCard::class.java, AdelaideMetrocardStubTransitFactory()) - registerFactory(DesfireCard::class.java, AtHopStubTransitFactory()) - - registerFactory(ClassicCard::class.java, OVChipTransitFactory(context)) - registerFactory(ClassicCard::class.java, BilheteUnicoSPTransitFactory()) - registerFactory(ClassicCard::class.java, ManlyFastFerryTransitFactory()) - registerFactory(ClassicCard::class.java, SeqGoTransitFactory(context)) - registerFactory(ClassicCard::class.java, EasyCardTransitFactory(context)) - - registerFactory(CEPASCard::class.java, EZLinkTransitFactory()) - - registerFactory(SampleCard::class.java, SampleTransitFactory()) - } - - fun parseTransitIdentity(card: Card): TransitIdentity? = findFactory(card)?.parseIdentity(card) - - fun parseTransitInfo(card: Card): TransitInfo? = findFactory(card)?.parseInfo(card) - - @Suppress("UNCHECKED_CAST") - private fun registerFactory(cardClass: Class, factory: TransitFactory<*, *>) { - var factories = registry[cardClass] - if (factories == null) { - factories = mutableListOf() - registry[cardClass] = factories - } - factories.add(factory as TransitFactory) - } - - private fun findFactory(card: Card): TransitFactory? = - registry[card.parentClass]?.find { it.check(card) } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/ui/FareBotCrossfadeTransition.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/ui/FareBotCrossfadeTransition.kt deleted file mode 100644 index 93bdd4b4c..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/ui/FareBotCrossfadeTransition.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * FareBotCrossfadeTransition.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.ui - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.content.Context -import android.view.View -import com.wealthfront.magellan.Direction -import com.wealthfront.magellan.NavigationType -import com.wealthfront.magellan.transitions.Transition - -class FareBotCrossfadeTransition(context: Context) : Transition { - - private val shortAnimationDuration = context.resources.getInteger(android.R.integer.config_shortAnimTime).toLong() - - override fun animate( - viewFrom: View, - viewTo: View, - navType: NavigationType, - direction: Direction, - callback: Transition.Callback - ) { - - viewTo.alpha = 0f - viewTo.visibility = View.VISIBLE - - viewTo.animate() - .alpha(1f) - .setDuration(shortAnimationDuration) - .setListener(null) - - viewFrom.animate() - .alpha(0f) - .setDuration(shortAnimationDuration) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - viewFrom.visibility = View.GONE - callback.onAnimationEnd() - } - }) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/ui/FareBotScreen.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/ui/FareBotScreen.kt deleted file mode 100644 index 08bb11ba2..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/ui/FareBotScreen.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * FareBotScreen.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.ui - -import android.content.Context -import android.view.ViewGroup -import androidx.annotation.CallSuper -import com.codebutler.farebot.app.core.analytics.AnalyticsEventName -import com.codebutler.farebot.app.core.analytics.logAnalyticsEvent -import com.codebutler.farebot.app.feature.main.MainActivity -import com.jakewharton.rxrelay2.BehaviorRelay -import com.uber.autodispose.LifecycleScopeProvider -import com.uber.autodispose.OutsideLifecycleException -import com.wealthfront.magellan.Screen -import com.wealthfront.magellan.ScreenView -import io.reactivex.Observable -import io.reactivex.functions.Function - -@Suppress("FINITE_BOUNDS_VIOLATION_IN_JAVA") -abstract class FareBotScreen : Screen(), LifecycleScopeProvider - where V : ViewGroup, V : ScreenView<*> { - - private val lifecycleRelay = BehaviorRelay.create() - - override fun createView(context: Context): V { - val parentComponent = (activity as MainActivity).component - inject(createComponent(parentComponent)) - return onCreateView(context) - } - - companion object { - private val CORRESPONDING_EVENTS = Function { lastEvent -> - when (lastEvent) { - ScreenLifecycleEvent.RESUME -> ScreenLifecycleEvent.PAUSE - ScreenLifecycleEvent.SHOW -> ScreenLifecycleEvent.HIDE - else -> throw OutsideLifecycleException("what! $lastEvent") - } - } - } - - @Deprecated("override getActionBarOptions instead") - final override fun getActionBarColorRes(): Int { - return super.getActionBarColorRes() - } - - open fun getActionBarOptions(): ActionBarOptions = ActionBarOptions() - - @CallSuper - override fun onResume(context: Context) { - super.onResume(context) - lifecycleRelay.accept(ScreenLifecycleEvent.RESUME) - } - - @CallSuper - override fun onShow(context: Context) { - super.onShow(context) - logAnalyticsEvent(AnalyticsEventName.VIEW_SCREEN, getTitle(activity)) - lifecycleRelay.accept(ScreenLifecycleEvent.SHOW) - } - - @CallSuper - override fun onPause(context: Context) { - super.onPause(context) - lifecycleRelay.accept(ScreenLifecycleEvent.PAUSE) - } - - @CallSuper - override fun onHide(context: Context) { - super.onHide(context) - lifecycleRelay.accept(ScreenLifecycleEvent.HIDE) - } - - final override fun lifecycle(): Observable = lifecycleRelay.hide() - - final override fun correspondingEvents(): Function = - CORRESPONDING_EVENTS - - final override fun peekLifecycle(): ScreenLifecycleEvent = lifecycleRelay.value!! - - protected abstract fun onCreateView(context: Context): V - - protected abstract fun createComponent(parentComponent: MainActivity.MainActivityComponent): C - - protected abstract fun inject(component: C) -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/util/ErrorUtils.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/util/ErrorUtils.kt deleted file mode 100644 index 7cff893dd..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/util/ErrorUtils.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * ErrorUtils.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.util - -import android.app.Activity -import androidx.appcompat.app.AlertDialog -import android.text.TextUtils -import android.util.Log -import android.widget.Toast - -object ErrorUtils { - - fun showErrorAlert(activity: Activity, ex: Throwable) { - Log.e(activity.javaClass.name, ex.message, ex) - AlertDialog.Builder(activity) - .setMessage(getErrorMessage(ex)) - .setPositiveButton(android.R.string.ok, null) - .show() - } - - fun showErrorToast(activity: Activity, ex: Throwable) { - Log.e(activity.javaClass.name, ex.message, ex) - Toast.makeText(activity, getErrorMessage(ex), Toast.LENGTH_SHORT).show() - } - - fun getErrorMessage(ex: Throwable): String { - val ex1 = if (ex.cause != null) ex.cause as Throwable else ex - var errorMessage = ex1.localizedMessage - if (TextUtils.isEmpty(errorMessage)) { - errorMessage = ex1.message - } - if (TextUtils.isEmpty(errorMessage)) { - errorMessage = ex1.toString() - } - return errorMessage - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/core/util/ExportHelper.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/core/util/ExportHelper.kt deleted file mode 100644 index b50ddae07..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/core/util/ExportHelper.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * ExportHelper.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.core.util - -import com.codebutler.farebot.BuildConfig -import com.codebutler.farebot.card.RawCard -import com.codebutler.farebot.card.serialize.CardSerializer -import com.codebutler.farebot.persist.CardPersister -import com.codebutler.farebot.persist.db.model.SavedCard -import com.google.gson.Gson - -class ExportHelper( - private val cardPersister: CardPersister, - private val cardSerializer: CardSerializer, - private val gson: Gson -) { - - fun exportCards(): String = gson.toJson(Export( - versionName = BuildConfig.VERSION_NAME, - versionCode = BuildConfig.VERSION_CODE, - cards = cardPersister.cards.map { cardSerializer.deserialize(it.data) } - )) - - fun importCards(exportJsonString: String): List = gson.fromJson(exportJsonString, Export::class.java) - .cards.map { cardPersister.insertCard(SavedCard( - type = it.cardType(), - serial = it.tagId().hex(), - data = cardSerializer.serialize(it))) - } - - private data class Export( - internal val versionName: String, - internal val versionCode: Int, - internal val cards: List> - ) -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/CardScreen.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/CardScreen.kt deleted file mode 100644 index cf060c15b..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/CardScreen.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * CardScreen.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.card - -import android.content.Context -import androidx.appcompat.app.AppCompatActivity -import android.view.Menu -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.activity.ActivityOperations -import com.codebutler.farebot.app.core.analytics.AnalyticsEventName -import com.codebutler.farebot.app.core.analytics.logAnalyticsEvent -import com.codebutler.farebot.app.core.inject.ScreenScope -import com.codebutler.farebot.app.core.transit.TransitFactoryRegistry -import com.codebutler.farebot.app.core.ui.ActionBarOptions -import com.codebutler.farebot.app.core.ui.FareBotScreen -import com.codebutler.farebot.app.feature.card.advanced.CardAdvancedScreen -import com.codebutler.farebot.app.feature.card.map.TripMapScreen -import com.codebutler.farebot.app.feature.main.MainActivity -import com.codebutler.farebot.card.Card -import com.codebutler.farebot.card.RawCard -import com.codebutler.farebot.transit.TransitInfo -import com.uber.autodispose.kotlin.autoDisposable -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers -import javax.inject.Inject - -class CardScreen(private val rawCard: RawCard<*>) : FareBotScreen() { - - data class Content( - val card: Card, - val transitInfo: TransitInfo?, - val viewModels: List - ) - - private var content: Content? = null - - @Inject lateinit var activityOperations: ActivityOperations - @Inject lateinit var transitFactoryRegistry: TransitFactoryRegistry - - override fun getActionBarOptions(): ActionBarOptions = ActionBarOptions( - backgroundColorRes = R.color.accent, - textColorRes = R.color.white, - shadow = false - ) - - override fun onCreateView(context: Context): CardScreenView = CardScreenView(context) - - override fun onShow(context: Context) { - super.onShow(context) - - logAnalyticsEvent(AnalyticsEventName.VIEW_CARD, rawCard.cardType().toString()) - - activityOperations.menuItemClick - .autoDisposable(this) - .subscribe({ menuItem -> - when (menuItem.itemId) { - R.id.card_advanced -> { - content?.let { - navigator.goTo(CardAdvancedScreen(it.card, it.transitInfo)) - } - } - } - }) - - loadContent() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(this) - .subscribe({ content -> - this.content = content - if (content.transitInfo != null) { - (activity as AppCompatActivity).supportActionBar?.apply { - title = content.transitInfo.getCardName(view.resources) - subtitle = content.transitInfo.serialNumber - } - view.setTransitInfo(content.transitInfo, content.viewModels) - } else { - (activity as AppCompatActivity).supportActionBar?.apply { - title = context.getString(R.string.unknown_card) - } - view.setError(context.getString(R.string.unknown_card_desc)) - } - activity.invalidateOptionsMenu() - - val type = content.transitInfo?.getCardName(activity.resources) ?: "Unknown" - logAnalyticsEvent(AnalyticsEventName.VIEW_TRANSIT, type) - }) - - view.observeItemClicks() - .autoDisposable(this) - .subscribe { viewModel -> - when (viewModel) { - is TransactionViewModel.TripViewModel -> { - val trip = viewModel.trip - if (trip.startStation?.hasLocation() == true || trip.endStation?.hasLocation() == true) { - navigator.goTo(TripMapScreen(trip)) - } - } - } - } - } - - override fun onUpdateMenu(menu: Menu?) { - menu?.clear() - activity.menuInflater.inflate(R.menu.screen_card, menu) - menu?.findItem(R.id.card_advanced)?.isVisible = content != null - } - - private fun loadContent(): Single = Single.create { e -> - try { - val card = rawCard.parse() - val transitInfo = transitFactoryRegistry.parseTransitInfo(card) - val viewModels = createViewModels(transitInfo) - e.onSuccess(Content(card, transitInfo, viewModels)) - } catch (ex: Exception) { - e.onError(ex) - } - } - - private fun createViewModels(transitInfo: TransitInfo?): List { - val subscriptions = transitInfo?.subscriptions?.map { - TransactionViewModel.SubscriptionViewModel(activity, it) - } ?: listOf() - val trips = transitInfo?.trips?.map { TransactionViewModel.TripViewModel(activity, it) } ?: listOf() - val refills = transitInfo?.refills?.map { TransactionViewModel.RefillViewModel(activity, it) } ?: listOf() - return subscriptions + (trips + refills).sortedByDescending { it.date } - } - - override fun createComponent(parentComponent: MainActivity.MainActivityComponent): Component = - DaggerCardScreen_Component.builder() - .mainActivityComponent(parentComponent) - .build() - - override fun inject(component: Component) { - component.inject(this) - } - - @ScreenScope - @dagger.Component(dependencies = arrayOf(MainActivity.MainActivityComponent::class)) - interface Component { - fun inject(screen: CardScreen) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/CardScreenView.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/CardScreenView.kt deleted file mode 100644 index e515c3700..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/CardScreenView.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * CardScreenView.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.card - -import android.content.Context -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import android.view.View -import android.widget.LinearLayout -import android.widget.TextView -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.kotlin.bindView -import com.codebutler.farebot.transit.TransitInfo -import com.jakewharton.rxrelay2.PublishRelay -import com.wealthfront.magellan.BaseScreenView -import com.xwray.groupie.GroupAdapter -import io.reactivex.Observable - -class CardScreenView(context: Context) : BaseScreenView(context) { - - private val clicksRelay = PublishRelay.create() - - private val balanceLayout: LinearLayout by bindView(R.id.balance_layout) - private val balanceTextView: TextView by bindView(R.id.balance) - private val errorTextView: TextView by bindView(R.id.error) - private val recycler: RecyclerView by bindView(R.id.recycler) - - init { - inflate(context, R.layout.screen_card, this) - recycler.layoutManager = LinearLayoutManager(context) - } - - internal fun observeItemClicks(): Observable = clicksRelay.hide() - - fun setTransitInfo(transitInfo: TransitInfo, viewModels: List) { - val balance = transitInfo.getBalanceString(resources) - if (balance.isEmpty()) { - setError(resources.getString(R.string.no_information)) - } else { - balanceTextView.text = balance - if (viewModels.isNotEmpty()) { - recycler.adapter = GroupAdapter() - recycler.adapter = TransactionAdapter(viewModels, clicksRelay) - } else { - recycler.visibility = View.GONE - balanceLayout.layoutParams = LinearLayout.LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) - } - } - } - - fun setError(error: String) { - recycler.visibility = View.GONE - balanceLayout.visibility = View.GONE - errorTextView.visibility = View.VISIBLE - errorTextView.text = error - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/TransactionAdapter.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/TransactionAdapter.kt deleted file mode 100644 index 69eeee54c..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/TransactionAdapter.kt +++ /dev/null @@ -1,191 +0,0 @@ -/* - * TransactionAdapter.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.card - -import androidx.annotation.LayoutRes -import androidx.recyclerview.widget.RecyclerView -import android.text.format.DateFormat -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.kotlin.bindView -import com.codebutler.farebot.app.core.kotlin.inflate -import com.codebutler.farebot.app.feature.card.TransactionAdapter.TransactionViewHolder.RefillViewHolder -import com.codebutler.farebot.app.feature.card.TransactionAdapter.TransactionViewHolder.SubscriptionViewHolder -import com.codebutler.farebot.app.feature.card.TransactionAdapter.TransactionViewHolder.TripViewHolder -import com.jakewharton.rxrelay2.PublishRelay -import com.xwray.groupie.ViewHolder -import java.util.Calendar -import java.util.Date - -class TransactionAdapter( - private val viewModels: List, - private val relayClicks: PublishRelay -) : - RecyclerView.Adapter() { - - companion object { - private const val TYPE_TRIP = 0 - private const val TYPE_REFILL = 1 - private const val TYPE_SUBSCRIPTION = 2 - } - - override fun getItemCount(): Int = viewModels.size - - override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): TransactionViewHolder = when (viewType) { - TYPE_TRIP -> TripViewHolder(viewGroup) - TYPE_REFILL -> RefillViewHolder(viewGroup) - TYPE_SUBSCRIPTION -> SubscriptionViewHolder(viewGroup) - else -> throw IllegalArgumentException() - } - - override fun onBindViewHolder(viewHolder: TransactionViewHolder, position: Int) { - val viewModel = viewModels[position] - viewHolder.updateHeader(viewModel, isFirstInSection(position)) - when (viewHolder) { - is TripViewHolder -> viewHolder.update(viewModel as TransactionViewModel.TripViewModel, relayClicks) - is RefillViewHolder -> viewHolder.update(viewModel as TransactionViewModel.RefillViewModel) - is SubscriptionViewHolder -> viewHolder.update(viewModel as TransactionViewModel.SubscriptionViewModel) - } - } - - override fun getItemViewType(position: Int): Int = when (viewModels[position]) { - is TransactionViewModel.TripViewModel -> TYPE_TRIP - is TransactionViewModel.RefillViewModel -> TYPE_REFILL - is TransactionViewModel.SubscriptionViewModel -> TYPE_SUBSCRIPTION - } - - sealed class TransactionViewHolder(itemView: View) : ViewHolder(itemView) { - - companion object { - fun wrapLayout(parent: ViewGroup, @LayoutRes layoutId: Int): View = - parent.inflate(R.layout.item_transaction).apply { - findViewById(R.id.container).inflate(layoutId, true) - } - } - - private val header: TextView by bindView(R.id.header) - - fun updateHeader(item: TransactionViewModel, isFirstInSection: Boolean) { - val showHeader = isFirstInSection - header.visibility = if (showHeader) View.VISIBLE else View.GONE - if (showHeader) { - if (item is TransactionViewModel.SubscriptionViewModel) { - header.text = header.context.getString(R.string.subscriptions) - } else { - header.text = DateFormat.getLongDateFormat(header.context).format(item.date) - } - } - } - - class TripViewHolder(parent: ViewGroup) : - TransactionViewHolder(wrapLayout(parent, R.layout.item_transaction_trip)) { - - val item: View by bindView(R.id.item) - val image: ImageView by bindView(R.id.image) - private val route: TextView by bindView(R.id.route) - private val agency: TextView by bindView(R.id.agency) - private val stations: TextView by bindView(R.id.stations) - private val fare: TextView by bindView(R.id.fare) - val time: TextView by bindView(R.id.time) - - fun update(viewModel: TransactionViewModel.TripViewModel, relayClicks: PublishRelay) { - image.setImageResource(viewModel.imageResId) - image.contentDescription = viewModel.trip.mode.toString() - - route.text = viewModel.route - agency.text = viewModel.agency - stations.text = viewModel.stations - fare.text = viewModel.fare - time.text = viewModel.time - - updateTextViewVisibility(route) - updateTextViewVisibility(agency) - updateTextViewVisibility(stations) - updateTextViewVisibility(fare) - updateTextViewVisibility(time) - - item.setOnClickListener { relayClicks.accept(viewModel) } - } - - private fun updateTextViewVisibility(textView: TextView) { - textView.visibility = if (textView.text.isNullOrEmpty()) View.GONE else View.VISIBLE - } - } - - class RefillViewHolder(parent: ViewGroup) : - TransactionViewHolder(wrapLayout(parent, R.layout.item_transaction_refill)) { - - private val agency: TextView by bindView(R.id.agency) - private val amount: TextView by bindView(R.id.amount) - val time: TextView by bindView(R.id.time) - - fun update(viewModel: TransactionViewModel.RefillViewModel) { - agency.text = viewModel.agency - amount.text = viewModel.amount - time.text = viewModel.time - } - } - - class SubscriptionViewHolder(parent: ViewGroup) : - TransactionViewHolder(wrapLayout(parent, R.layout.item_transaction_subscription)) { - - private val agency: TextView by bindView(R.id.agency) - val name: TextView by bindView(R.id.name) - private val valid: TextView by bindView(R.id.valid) - private val used: TextView by bindView(R.id.used) - - fun update(viewModel: TransactionViewModel.SubscriptionViewModel) { - agency.text = viewModel.agency - name.text = viewModel.name - valid.text = viewModel.valid - used.text = viewModel.used - used.visibility = if (!viewModel.used.isNullOrEmpty()) View.VISIBLE else View.GONE - } - } - } - - private fun isFirstInSection(position: Int): Boolean { - fun createCalendar(date: Date?): Calendar? { - if (date != null) { - val cal = Calendar.getInstance() - cal.time = date - return cal - } - return null - } - - if (position == 0) { - return true - } - - val cal1 = createCalendar(viewModels[position].date) ?: return false - val cal2 = createCalendar(viewModels[position - 1].date) ?: return true - - return cal1.get(Calendar.YEAR) != cal2.get(Calendar.YEAR) || - cal1.get(Calendar.MONTH) != cal2.get(Calendar.MONTH) || - cal1.get(Calendar.DAY_OF_MONTH) != cal2.get(Calendar.DAY_OF_MONTH) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/TransactionViewModel.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/TransactionViewModel.kt deleted file mode 100644 index 112f33cdf..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/TransactionViewModel.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * TransactionViewModel.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.card - -import android.content.Context -import androidx.annotation.DrawableRes -import com.codebutler.farebot.R -import com.codebutler.farebot.transit.Refill -import com.codebutler.farebot.transit.Subscription -import com.codebutler.farebot.transit.Trip -import java.text.DateFormat -import java.util.Date -import java.util.Locale - -sealed class TransactionViewModel(val context: Context) { - - abstract val date: Date? - - val time: String? - get() = if (date != null) DateFormat.getTimeInstance(DateFormat.SHORT).format(date) else null - - class TripViewModel(context: Context, val trip: Trip) : TransactionViewModel(context) { - - override val date = Date(trip.timestamp * 1000) - - val route = trip.getRouteName(context.resources) - - val agency = trip.getAgencyName(context.resources) - - val fare = trip.getFareString(context.resources) - - val stations: CharSequence? = trip.getFormattedStations(context) - - @DrawableRes - val imageResId: Int = when (trip.mode) { - Trip.Mode.BUS -> R.drawable.ic_transaction_bus_32dp - Trip.Mode.TRAIN -> R.drawable.ic_transaction_train_32dp - Trip.Mode.TRAM -> R.drawable.ic_transaction_tram_32dp - Trip.Mode.METRO -> R.drawable.ic_transaction_metro_32dp - Trip.Mode.FERRY -> R.drawable.ic_transaction_ferry_32dp - Trip.Mode.TICKET_MACHINE -> R.drawable.ic_transaction_tvm_32dp - Trip.Mode.VENDING_MACHINE -> R.drawable.ic_transaction_vend_32dp - Trip.Mode.POS -> R.drawable.ic_transaction_pos_32dp - Trip.Mode.HANDHELD -> R.drawable.ic_transaction_handheld_32dp - Trip.Mode.BANNED -> R.drawable.ic_transaction_banned_32dp - Trip.Mode.OTHER -> R.drawable.ic_transaction_unknown_32dp - else -> R.drawable.ic_transaction_unknown_32dp - } - } - - class RefillViewModel(context: Context, refill: Refill) : - TransactionViewModel(context) { - - override val date: Date = Date(refill.timestamp * 1000) - - val agency = refill.getShortAgencyName(context.resources) - - val amount = "+ ${refill.getAmountString(context.resources)}" - } - - class SubscriptionViewModel(context: Context, private val subscription: Subscription) : - TransactionViewModel(context) { - - override val date = null - - val agency = subscription.getShortAgencyName(context.resources) - - val name = subscription.getSubscriptionName(context.resources) - - val valid: CharSequence - get() { - val format = DateFormat.getDateInstance(DateFormat.SHORT, Locale.UK) - val validFrom = format.format(subscription.validFrom) - val validTo = format.format(subscription.validTo) - return context.getString(R.string.subscription_valid_format, validFrom, validTo) - } - - val used = subscription.activation - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedAdapter.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedAdapter.kt deleted file mode 100644 index 644452b53..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedAdapter.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * CardAdvancedAdapter.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.card.advanced - -import androidx.recyclerview.widget.RecyclerView -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.kotlin.inflate -import com.codebutler.farebot.app.core.kotlin.bindView -import com.codebutler.farebot.base.ui.FareBotUiTree -import com.codebutler.farebot.base.util.ByteArray -import java.util.Locale - -// This is not very efficient.️ -class CardAdvancedAdapter(fareBotUiTree: FareBotUiTree) : - RecyclerView.Adapter() { - - private var viewModels: List - private var visibleViewModels: List = listOf() - - init { - viewModels = flatten(fareBotUiTree.items) - filterViewModels() - } - - override fun getItemCount(): Int = visibleViewModels.size - - override fun onCreateViewHolder(parent: ViewGroup, position: Int) = - ViewHolder(parent.inflate(R.layout.item_card_advanced)) - - override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - viewHolder.bind(visibleViewModels[position]) - } - - private fun flatten(items: List, parent: ViewModel? = null, depth: Int = 0): List { - val viewModels = mutableListOf() - for (item in items) { - val viewModel = ViewModel( - title = item.title, - value = item.value, - parent = parent, - canExpand = item.children().isNotEmpty(), - depth = depth) - viewModels.add(viewModel) - viewModels.addAll(flatten(item.children(), viewModel, depth + 1)) - } - return viewModels - } - - private fun filterViewModels() { - visibleViewModels = viewModels.filter { viewModel -> viewModel.visible } - notifyDataSetChanged() - } - - data class ViewModel( - var title: String, - var value: Any?, - var parent: ViewModel?, - var canExpand: Boolean, - var expanded: Boolean = false, - var depth: Int - ) { - - val visible: Boolean - get() = parent?.let { it.visible && (if (it.canExpand) it.expanded else true) } ?: true - } - - inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - - val title: TextView by bindView(R.id.title) - val value: TextView by bindView(R.id.value) - - val padding = itemView.resources.getDimensionPixelSize(R.dimen.grid_unit_2x) - - fun bind(viewModel: ViewModel) { - itemView.apply { - title.text = viewModel.title - - val viewModelValue = viewModel.value - if (viewModelValue != null) { - when (viewModelValue) { - is ByteArray -> value.text = viewModelValue.hex().toUpperCase(Locale.US) - else -> value.text = viewModel.value.toString() - } - value.visibility = View.VISIBLE - } else { - value.text = null - value.visibility = View.GONE - } - - setPadding(padding * viewModel.depth, paddingTop, paddingRight, paddingBottom) - } - - itemView.setOnClickListener { - if (viewModel.canExpand) { - itemView.post { - viewModel.expanded = !viewModel.expanded - filterViewModels() - } - } - } - } - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedScreen.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedScreen.kt deleted file mode 100644 index 814b9122f..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedScreen.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * CardAdvancedScreen.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.card.advanced - -import android.content.Context -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.inject.ScreenScope -import com.codebutler.farebot.app.core.ui.ActionBarOptions -import com.codebutler.farebot.app.core.ui.FareBotScreen -import com.codebutler.farebot.app.feature.main.MainActivity -import com.codebutler.farebot.card.Card -import com.codebutler.farebot.transit.TransitInfo - -class CardAdvancedScreen(private val card: Card, private val transitInfo: TransitInfo?) : - FareBotScreen() { - - override fun getActionBarOptions(): ActionBarOptions = ActionBarOptions( - backgroundColorRes = R.color.accent, - textColorRes = R.color.white - ) - - override fun onCreateView(context: Context): CardAdvancedScreenView = CardAdvancedScreenView(context) - - override fun getTitle(context: Context): String = activity.getString(R.string.advanced) - - override fun onShow(context: Context) { - super.onShow(context) - if (transitInfo != null) { - val transitInfoUi = transitInfo.getAdvancedUi(activity) - if (transitInfoUi != null) { - view.addTab(transitInfo.getCardName(context.resources), transitInfoUi) - } - } - view.addTab(card.cardType.toString(), card.getAdvancedUi(activity)) - } - - override fun createComponent(parentComponent: MainActivity.MainActivityComponent): Component = - DaggerCardAdvancedScreen_Component.builder() - .mainActivityComponent(parentComponent) - .build() - - override fun inject(component: Component) { - component.inject(this) - } - - @ScreenScope - @dagger.Component(dependencies = arrayOf(MainActivity.MainActivityComponent::class)) - interface Component { - fun inject(screen: CardAdvancedScreen) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedScreenView.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedScreenView.kt deleted file mode 100644 index 183a32b52..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedScreenView.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * CardAdvancedScreenView.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.card.advanced - -import android.content.Context -import android.view.ViewGroup -import android.widget.TabHost -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.kotlin.inflate -import com.codebutler.farebot.app.core.kotlin.bindView -import com.codebutler.farebot.base.ui.FareBotUiTree -import com.wealthfront.magellan.BaseScreenView - -class CardAdvancedScreenView(context: Context) : BaseScreenView(context) { - - private val tabHost: TabHost by bindView(android.R.id.tabhost) - private val tabContent: ViewGroup by bindView(android.R.id.tabcontent) - - private var tabCount = 0 - - init { - inflate(context, R.layout.screen_card_advanced, this) - tabHost.setup() - } - - fun addTab(title: String, fareBotUiTree: FareBotUiTree) { - val contentView = tabContent.inflate(R.layout.tab_card_advanced, false) as CardAdvancedTabView - contentView.setAdvancedUi(fareBotUiTree) - tabHost.addTab(tabHost.newTabSpec("tab_$tabCount") - .setIndicator(title) - .setContent { contentView }) - tabCount++ - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedTabView.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedTabView.kt deleted file mode 100644 index d88a4642a..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/advanced/CardAdvancedTabView.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * CardAdvancedTabView.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.card.advanced - -import android.content.Context -import android.graphics.Canvas -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.widget.FrameLayout -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.kotlin.bindView -import com.codebutler.farebot.base.ui.FareBotUiTree - -class CardAdvancedTabView : FrameLayout { - - private val recyclerView: RecyclerView by bindView(R.id.recycler) - - constructor(context: Context?) : - super(context) - - constructor(context: Context?, attrs: AttributeSet?) : - super(context, attrs) - - constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : - super(context, attrs, defStyleAttr) - - @Suppress("unused") - constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : - super(context, attrs, defStyleAttr, defStyleRes) - - fun setAdvancedUi(fareBotUiTree: FareBotUiTree) { - recyclerView.layoutManager = LinearLayoutManager(context) - recyclerView.adapter = CardAdvancedAdapter(fareBotUiTree) - recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() { - val divider: Drawable - - init { - val attrs = intArrayOf(android.R.attr.listDivider) - val ta = context.applicationContext.obtainStyledAttributes(attrs) - divider = ta.getDrawable(0) - ta.recycle() - } - - override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { - for (i in 0 until parent.childCount) { - val child = parent.getChildAt(i) - - if (parent.getChildAdapterPosition(child) == parent.adapter!!.itemCount - 1) { - continue - } - - val params = child.layoutParams as RecyclerView.LayoutParams - val childLeft = left + child.paddingLeft - val childTop = child.bottom + params.bottomMargin - val childBottom = childTop + divider.intrinsicHeight - - divider.setBounds(childLeft, childTop, right, childBottom) - divider.draw(canvas) - } - } - }) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/map/TripMapScreen.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/map/TripMapScreen.kt deleted file mode 100644 index 647704bdf..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/map/TripMapScreen.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * TripMapScreen.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.card.map - -import android.content.Context -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.inject.ScreenScope -import com.codebutler.farebot.app.core.kotlin.compact -import com.codebutler.farebot.app.core.ui.ActionBarOptions -import com.codebutler.farebot.app.core.ui.FareBotScreen -import com.codebutler.farebot.app.feature.main.MainActivity -import com.codebutler.farebot.transit.Trip - -class TripMapScreen(private val trip: Trip) : FareBotScreen() { - - override fun getTitle(context: Context): String = context.getString(R.string.map) - - override fun getActionBarOptions(): ActionBarOptions = ActionBarOptions( - backgroundColorRes = R.color.accent, - textColorRes = R.color.white - ) - - override fun onCreateView(context: Context): TripMapScreenView = TripMapScreenView(context, trip) - - override fun onShow(context: Context) { - super.onShow(context) - - view.post { - (activity as AppCompatActivity).supportActionBar?.apply { - val resources = context.resources - setDisplayHomeAsUpEnabled(true) - title = arrayOf(trip.startStation?.shortStationName, trip.endStation?.shortStationName) - .compact() - .joinToString(" → ") - subtitle = arrayOf(trip.getAgencyName(resources), trip.getRouteName(resources)) - .compact() - .joinToString(" ") - } - } - - view.onCreate(Bundle()) - } - - override fun onHide(context: Context) { - super.onHide(context) - view.onDestroy() - } - - override fun onResume(context: Context) { - super.onResume(context) - view.onStart() - view.onResume() - } - - override fun onPause(context: Context) { - super.onPause(context) - view.onPause() - view.onStop() - } - - override fun onSave(outState: Bundle?) { - super.onSave(outState) - } - - override fun onRestore(savedInstanceState: Bundle?) { - super.onRestore(savedInstanceState) - } - - override fun createComponent(parentComponent: MainActivity.MainActivityComponent): Component = - DaggerTripMapScreen_Component.builder() - .mainActivityComponent(parentComponent) - .build() - - override fun inject(component: Component) { - component.inject(this) - } - - @ScreenScope - @dagger.Component(dependencies = arrayOf(MainActivity.MainActivityComponent::class)) - interface Component { - fun inject(screen: TripMapScreen) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/map/TripMapScreenView.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/map/TripMapScreenView.kt deleted file mode 100644 index 53a8babfc..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/card/map/TripMapScreenView.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * TripMapScreenView.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.card.map - -import android.annotation.SuppressLint -import android.content.Context -import android.os.Bundle -import androidx.annotation.DrawableRes -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.kotlin.bindView -import com.codebutler.farebot.transit.Station -import com.codebutler.farebot.transit.Trip -import com.google.android.gms.maps.CameraUpdateFactory -import com.google.android.gms.maps.GoogleMap -import com.google.android.gms.maps.MapView -import com.google.android.gms.maps.model.BitmapDescriptorFactory -import com.google.android.gms.maps.model.LatLng -import com.google.android.gms.maps.model.LatLngBounds -import com.google.android.gms.maps.model.MarkerOptions -import com.wealthfront.magellan.BaseScreenView -import java.util.ArrayList - -@SuppressLint("ViewConstructor") -class TripMapScreenView( - context: Context, - private val trip: Trip -) : - BaseScreenView(context) { - - private val mapView: MapView by bindView(R.id.map) - - init { - inflate(context, R.layout.screen_trip_map, this) - - mapView.getMapAsync { map -> - map.uiSettings.isZoomControlsEnabled = false - populateMap(map, trip) - } - } - - fun onCreate(bundle: Bundle) { - mapView.onCreate(bundle) - } - - fun onDestroy() { - mapView.onDestroy() - } - - fun onPause() { - mapView.onPause() - } - - fun onResume() { - mapView.onResume() - } - - fun onStart() { - mapView.onStart() - } - - fun onStop() { - mapView.onStop() - } - - private fun populateMap(map: GoogleMap, trip: Trip) { - val startMarkerId = R.drawable.marker_start - val endMarkerId = R.drawable.marker_end - - val points = ArrayList() - val builder = LatLngBounds.builder() - - val startStation = trip.startStation - if (startStation != null) { - val startStationLatLng = addStationMarker(map, startStation, startMarkerId) - builder.include(startStationLatLng) - points.add(startStationLatLng) - } - - val endStation = trip.endStation - if (endStation != null) { - val endStationLatLng = addStationMarker(map, endStation, endMarkerId) - builder.include(endStationLatLng) - points.add(endStationLatLng) - } - - if (points.isNotEmpty()) { - val bounds = builder.build() - if (points.size == 1) { - map.moveCamera(CameraUpdateFactory.newLatLngZoom(points[0], 17f)) - } else { - val width = resources.displayMetrics.widthPixels - val height = resources.displayMetrics.heightPixels - val padding = resources.getDimensionPixelSize(R.dimen.map_padding) - map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, width, height, padding)) - } - } - } - - private fun addStationMarker(map: GoogleMap, station: Station, @DrawableRes iconId: Int): LatLng { - val pos = LatLng(station.latitude?.toDoubleOrNull() ?: 0.0, station.longitude?.toDoubleOrNull() ?: 0.0) - map.addMarker(MarkerOptions() - .position(pos) - .title(station.stationName) - .snippet(station.companyName) - .icon(BitmapDescriptorFactory.fromResource(iconId))) - return pos - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/help/HelpScreen.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/help/HelpScreen.kt deleted file mode 100644 index cdc6e3add..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/help/HelpScreen.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * HelpScreen.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.help - -import android.content.Context -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.inject.ScreenScope -import com.codebutler.farebot.app.core.ui.ActionBarOptions -import com.codebutler.farebot.app.core.ui.FareBotScreen -import com.codebutler.farebot.app.feature.main.MainActivity -import dagger.Component - -class HelpScreen : FareBotScreen() { - - override fun getTitle(context: Context): String = context.getString(R.string.supported_cards) - - override fun getActionBarOptions(): ActionBarOptions = ActionBarOptions( - backgroundColorRes = R.color.accent, - textColorRes = R.color.white - ) - - override fun onCreateView(context: Context): HelpScreenView = HelpScreenView(context) - - override fun createComponent(parentComponent: MainActivity.MainActivityComponent): HelpComponent = - DaggerHelpScreen_HelpComponent.builder() - .mainActivityComponent(parentComponent) - .build() - - override fun inject(component: HelpComponent) { - component.inject(this) - } - - @ScreenScope - @Component(dependencies = arrayOf(MainActivity.MainActivityComponent::class)) - interface HelpComponent { - fun inject(helpScreen: HelpScreen) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/help/HelpScreenView.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/help/HelpScreenView.kt deleted file mode 100644 index 745241f59..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/help/HelpScreenView.kt +++ /dev/null @@ -1,276 +0,0 @@ -/* - * HelpScreenView.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.help - -import android.content.Context -import android.nfc.NfcAdapter -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import android.widget.Toast -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.kotlin.bindView -import com.codebutler.farebot.card.CardType -import com.codebutler.farebot.transit.manly_fast_ferry.ManlyFastFerryTransitInfo -import com.codebutler.farebot.transit.myki.MykiTransitInfo -import com.codebutler.farebot.transit.octopus.OctopusTransitInfo -import com.codebutler.farebot.transit.opal.OpalTransitInfo -import com.codebutler.farebot.transit.seq_go.SeqGoTransitInfo -import com.wealthfront.magellan.BaseScreenView -import java.util.ArrayList - -class HelpScreenView(context: Context) : BaseScreenView(context) { - - companion object { - private val SUPPORTED_CARDS = listOf( - SupportedCard( - imageResId = R.drawable.orca_card, - name = "ORCA", - locationResId = R.string.location_seattle, - cardType = CardType.MifareDesfire - ), - SupportedCard( - imageResId = R.drawable.clipper_card, - name = "Clipper", - locationResId = R.string.location_san_francisco, - cardType = CardType.MifareDesfire - ), - SupportedCard( - imageResId = R.drawable.suica_card, - name = "Suica", - locationResId = R.string.location_tokyo, - cardType = CardType.FeliCa - ), - SupportedCard( - imageResId = R.drawable.pasmo_card, - name = "PASMO", - locationResId = R.string.location_tokyo, - cardType = CardType.FeliCa - ), - SupportedCard( - imageResId = R.drawable.icoca_card, - name = "ICOCA", - locationResId = R.string.location_kansai, - cardType = CardType.FeliCa - ), - SupportedCard( - imageResId = R.drawable.edy_card, - name = "Edy", - locationResId = R.string.location_tokyo, - cardType = CardType.FeliCa - ), - SupportedCard( - imageResId = R.drawable.ezlink_card, - name = "EZ-Link", - locationResId = R.string.location_singapore, - cardType = CardType.CEPAS, - extraNoteResId = R.string.ezlink_card_note - ), - SupportedCard( - imageResId = R.drawable.octopus_card, - name = OctopusTransitInfo.OCTOPUS_NAME, - locationResId = R.string.location_hong_kong, - cardType = CardType.FeliCa - ), - SupportedCard( - imageResId = R.drawable.bilheteunicosp_card, - name = "Bilhete Único", - locationResId = R.string.location_sao_paulo, - cardType = CardType.MifareClassic - ), - SupportedCard( - imageResId = R.drawable.seqgo_card, - name = SeqGoTransitInfo.NAME, - locationResId = R.string.location_brisbane_seq_australia, - cardType = CardType.MifareClassic, - keysRequired = true, - preview = true, - extraNoteResId = R.string.seqgo_card_note - ), - SupportedCard( - imageResId = R.drawable.hsl_card, - name = "HSL", - locationResId = R.string.location_helsinki_finland, - cardType = CardType.MifareDesfire - ), - SupportedCard( - imageResId = R.drawable.manly_fast_ferry_card, - name = ManlyFastFerryTransitInfo.NAME, - locationResId = R.string.location_sydney_australia, - cardType = CardType.MifareClassic, - keysRequired = true - ), - SupportedCard( - imageResId = R.drawable.myki_card, - name = MykiTransitInfo.NAME, - locationResId = R.string.location_victoria_australia, - cardType = CardType.MifareDesfire, - keysRequired = false, - preview = false, - extraNoteResId = R.string.myki_card_note - ), - SupportedCard( - imageResId = R.drawable.nets_card, - name = "NETS FlashPay", - locationResId = R.string.location_singapore, - cardType = CardType.CEPAS - ), - SupportedCard( - imageResId = R.drawable.opal_card, - name = OpalTransitInfo.NAME, - locationResId = R.string.location_sydney_australia, - cardType = CardType.MifareDesfire - ), - SupportedCard( - imageResId = R.drawable.ovchip_card, - name = "OV-chipkaart", - locationResId = R.string.location_the_netherlands, - cardType = CardType.MifareClassic, - keysRequired = true - ), - SupportedCard( - imageResId = R.drawable.easycard, - name = "EasyCard", - locationResId = R.string.easycard_card_location, - cardType = CardType.MifareClassic, - keysRequired = true, - extraNoteResId = R.string.easycard_card_note - ), - SupportedCard( - imageResId = R.drawable.kmt_card, - name = "Kartu Multi Trip", - locationResId = R.string.location_jakarta, - cardType = CardType.FeliCa, - keysRequired = false, - extraNoteResId = R.string.kmt_notes - ) - - ) - } - - private val recyclerView: RecyclerView by bindView(R.id.recycler) - - init { - inflate(context, R.layout.screen_help, this) - recyclerView.layoutManager = LinearLayoutManager(context) - recyclerView.adapter = SupportedCardsAdapter(context, SUPPORTED_CARDS) - } - - internal class SupportedCardsAdapter( - private val context: Context, - private val supportedCards: List - ) : - RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SupportedCardViewHolder { - val layoutInflater = LayoutInflater.from(parent.context) - return SupportedCardViewHolder(layoutInflater.inflate(R.layout.item_supported_card, parent, false)) - } - - override fun onBindViewHolder(holder: SupportedCardViewHolder, position: Int) { - holder.bind(context, supportedCards[position]) - } - - override fun getItemCount(): Int = supportedCards.size - } - - internal class SupportedCardViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - - private val textViewName: TextView by bindView(R.id.card_name) - private val textViewLocation: TextView by bindView(R.id.card_location) - private val textViewNote: TextView by bindView(R.id.card_note) - private val imageView: ImageView by bindView(R.id.card_image) - private val imageViewSecure: ImageView by bindView(R.id.card_secure) - private val viewNotSupported: View by bindView(R.id.card_not_supported) - - init { - imageViewSecure.setOnClickListener { - Toast.makeText(imageViewSecure.context, R.string.keys_required, Toast.LENGTH_SHORT).show() - } - } - - fun bind(context: Context, supportedCard: SupportedCard) { - textViewName.text = supportedCard.name - textViewLocation.setText(supportedCard.locationResId) - imageView.setImageResource(supportedCard.imageResId) - - imageViewSecure.visibility = if (supportedCard.keysRequired) View.VISIBLE else View.GONE - - val notes = getNotes(context, supportedCard) - if (notes != null) { - textViewNote.text = notes - textViewNote.visibility = View.VISIBLE - } else { - textViewNote.text = null - textViewNote.visibility = View.GONE - } - - viewNotSupported.visibility = if (isCardSupported(context, supportedCard)) View.GONE else View.VISIBLE - } - - private fun getNotes(context: Context, supportedCard: SupportedCard): String? { - val notes = ArrayList() - val extraNoteResId = supportedCard.extraNoteResId - if (extraNoteResId != null) { - notes.add(context.getString(extraNoteResId)) - } - if (supportedCard.preview) { - notes.add(context.getString(R.string.card_experimental)) - } - if (supportedCard.cardType == CardType.CEPAS) { - notes.add(context.getString(R.string.card_not_compatible)) - } - if (!notes.isEmpty()) { - return notes.joinToString(" ") - } - return null - } - - private fun isCardSupported(context: Context, supportedCard: SupportedCard): Boolean { - if (NfcAdapter.getDefaultAdapter(context) == null) { - return true - } - val supportsMifareClassic = context.packageManager.hasSystemFeature("com.nxp.mifare") - if (supportedCard.cardType == CardType.MifareClassic && !supportsMifareClassic) { - return false - } - return true - } - } - - data class SupportedCard( - @get:DrawableRes val imageResId: Int, - val name: String, - @get:StringRes val locationResId: Int, - val cardType: CardType, - val keysRequired: Boolean = false, - val preview: Boolean = false, - @get:StringRes val extraNoteResId: Int? = null - ) -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryAdapter.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryAdapter.kt deleted file mode 100644 index ea6329716..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryAdapter.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* - * HistoryAdapter.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.history - -import android.annotation.SuppressLint -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.kotlin.bindView -import com.codebutler.farebot.app.core.kotlin.inflate -import com.jakewharton.rxrelay2.PublishRelay -import java.text.DateFormat -import java.text.SimpleDateFormat - -internal class HistoryAdapter( - private val viewModels: List, - private val clicksRelay: PublishRelay, - private val selectionRelay: PublishRelay> -) : - RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryViewHolder = - HistoryViewHolder(parent.inflate(R.layout.item_history)) - - override fun onBindViewHolder(holder: HistoryViewHolder, position: Int) { - val viewModel = viewModels[position] - holder.update(viewModel) - holder.itemView.setOnClickListener { - if (hasSelectedItems()) { - viewModel.isSelected = !viewModel.isSelected - notifySelectionChanged() - } else { - clicksRelay.accept(viewModel) - } - } - holder.itemView.setOnLongClickListener { - if (!hasSelectedItems()) { - viewModel.isSelected = true - notifySelectionChanged() - true - } else { - false - } - } - } - - override fun getItemCount(): Int = viewModels.size - - private fun hasSelectedItems(): Boolean = viewModels.any { it.isSelected } - - private fun notifySelectionChanged() { - notifyDataSetChanged() - selectionRelay.accept(viewModels.filter { it.isSelected }) - } - - fun clearSelectedItems() { - for (viewModel in viewModels) { - viewModel.isSelected = false - } - notifySelectionChanged() - } - - internal class HistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - private val textViewCardName: TextView by bindView(R.id.card_name) - private val textViewCardSerial: TextView by bindView(R.id.card_serial) - private val textViewCardTime: TextView by bindView(R.id.card_time) - private val textViewCardDate: TextView by bindView(R.id.card_date) - - @SuppressLint("SetTextI18n") - fun update(viewModel: HistoryViewModel) { - val scannedAt = viewModel.savedCard.scannedAt - val identity = viewModel.transitIdentity - val savedCard = viewModel.savedCard - - val timeInstance = SimpleDateFormat.getTimeInstance(DateFormat.SHORT) - val dateInstance = SimpleDateFormat.getDateInstance(DateFormat.SHORT) - - textViewCardDate.text = dateInstance.format(scannedAt) - textViewCardTime.text = timeInstance.format(scannedAt) - - if (identity != null) { - val serial = identity.serialNumber ?: savedCard.serial - textViewCardName.text = identity.name - textViewCardSerial.text = serial - } else { - textViewCardName.text = itemView.resources.getString(R.string.unknown_card) - textViewCardSerial.text = "${savedCard.type} - ${savedCard.serial}" - } - - itemView.isSelected = viewModel.isSelected - } - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryScreen.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryScreen.kt deleted file mode 100644 index 4e74d905a..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryScreen.kt +++ /dev/null @@ -1,276 +0,0 @@ -/* - * HistoryScreen.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -@file:Suppress("UNNECESSARY_NOT_NULL_ASSERTION") - -package com.codebutler.farebot.app.feature.history - -import android.Manifest -import android.app.Activity -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Environment -import android.view.Menu -import android.widget.Toast -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.activity.ActivityOperations -import com.codebutler.farebot.app.core.inject.ScreenScope -import com.codebutler.farebot.app.core.kotlin.Optional -import com.codebutler.farebot.app.core.kotlin.filterAndGetOptional -import com.codebutler.farebot.app.core.transit.TransitFactoryRegistry -import com.codebutler.farebot.app.core.ui.ActionBarOptions -import com.codebutler.farebot.app.core.ui.FareBotScreen -import com.codebutler.farebot.app.core.util.ErrorUtils -import com.codebutler.farebot.app.core.util.ExportHelper -import com.codebutler.farebot.app.feature.card.CardScreen -import com.codebutler.farebot.app.feature.main.MainActivity -import com.codebutler.farebot.card.serialize.CardSerializer -import com.codebutler.farebot.persist.CardPersister -import com.codebutler.farebot.persist.db.model.SavedCard -import com.codebutler.farebot.transit.TransitIdentity -import com.uber.autodispose.kotlin.autoDisposable -import dagger.Component -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers -import java.io.File -import javax.inject.Inject - -class HistoryScreen : FareBotScreen(), HistoryScreenView.Listener { - - companion object { - private const val REQUEST_SELECT_FILE = 1 - private const val REQUEST_PERMISSION_STORAGE = 2 - private const val FILENAME = "farebot-export.json" - } - - @Inject lateinit var activityOperations: ActivityOperations - @Inject lateinit var cardPersister: CardPersister - @Inject lateinit var cardSerializer: CardSerializer - @Inject lateinit var exportHelper: ExportHelper - @Inject lateinit var transitFactoryRegistry: TransitFactoryRegistry - - override fun getTitle(context: Context): String = context.getString(R.string.history) - - override fun getActionBarOptions(): ActionBarOptions = ActionBarOptions( - backgroundColorRes = R.color.accent, - textColorRes = R.color.white - ) - - override fun onCreateView(context: Context): HistoryScreenView = - HistoryScreenView(context, activityOperations, this) - - override fun onUpdateMenu(menu: Menu) { - activity.menuInflater.inflate(R.menu.screen_history, menu) - } - - override fun onShow(context: Context) { - super.onShow(context) - - activityOperations.menuItemClick - .autoDisposable(this) - .subscribe { menuItem -> - val clipboardManager = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - when (menuItem.itemId) { - R.id.import_file -> { - val storageUri = Uri.fromFile(Environment.getExternalStorageDirectory()) - val target = Intent(Intent.ACTION_GET_CONTENT) - target.putExtra(Intent.EXTRA_STREAM, storageUri) - target.type = "*/*" - activity.startActivityForResult( - Intent.createChooser(target, activity.getString(R.string.select_file)), - REQUEST_SELECT_FILE) - } - R.id.import_clipboard -> { - val importClip = clipboardManager.primaryClip - if (importClip != null && importClip.itemCount > 0) { - val text = importClip.getItemAt(0).coerceToText(activity).toString() - Single.fromCallable { exportHelper.importCards(text) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(this) - .subscribe { cards -> onCardsImported(cards) } - } - } - R.id.copy -> { - val exportClip = ClipData.newPlainText(null, exportHelper.exportCards()) - clipboardManager.primaryClip = exportClip - Toast.makeText(activity, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() - } - R.id.share -> { - val intent = Intent(Intent.ACTION_SEND) - intent.type = "text/plain" - intent.putExtra(Intent.EXTRA_TEXT, exportHelper.exportCards()) - activity.startActivity(intent) - } - R.id.save -> exportToFile() - } - } - - activityOperations.permissionResult - .autoDisposable(this) - .subscribe { (requestCode, _, grantResults) -> - when (requestCode) { - REQUEST_PERMISSION_STORAGE -> { - if (grantResults.getOrNull(0) == PackageManager.PERMISSION_GRANTED) { - exportToFileWithPermission() - } - } - } - } - - activityOperations.activityResult - .autoDisposable(this) - .subscribe { (requestCode, resultCode, data) -> - when (requestCode) { - REQUEST_SELECT_FILE -> { - if (resultCode == Activity.RESULT_OK) { - data?.data?.let { - importFromFile(it) - } - } - } - } - } - - loadCards() - - view.observeItemClicks() - .autoDisposable(this) - .subscribe { viewModel -> navigator.goTo(CardScreen(viewModel.rawCard)) } - } - - override fun onDeleteSelectedItems(items: List) { - for ((savedCard) in items) { - cardPersister.deleteCard(savedCard) - } - loadCards() - } - - override fun createComponent(parentComponent: MainActivity.MainActivityComponent): HistoryComponent = - DaggerHistoryScreen_HistoryComponent.builder() - .mainActivityComponent(parentComponent) - .build() - - override fun inject(component: HistoryComponent) { - component.inject(this) - } - - private fun loadCards() { - observeCards() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(this) - .subscribe( - { viewModels -> view.setViewModels(viewModels) }, - { e -> ErrorUtils.showErrorToast(activity, e) }) - } - - private fun observeCards(): Single> { - return Single.create> { e -> - try { - e.onSuccess(cardPersister.cards) - } catch (error: Throwable) { - e.onError(error) - } - }.map { savedCards -> - savedCards.map { savedCard -> - val rawCard = cardSerializer.deserialize(savedCard.data) - var transitIdentity: TransitIdentity? = null - var parseException: Exception? = null - try { - transitIdentity = transitFactoryRegistry.parseTransitIdentity(rawCard.parse()) - } catch (ex: Exception) { - parseException = ex - } - HistoryViewModel(savedCard, rawCard, transitIdentity, parseException) - } - } - } - - private fun onCardsImported(cardIds: List) { - loadCards() - - val text = activity.resources.getQuantityString(R.plurals.cards_imported, cardIds.size, cardIds.size) - Toast.makeText(activity, text, Toast.LENGTH_SHORT).show() - - if (cardIds.size == 1) { - Single.create> { e -> e.onSuccess(Optional(cardPersister.getCard(cardIds[0]))) } - .filterAndGetOptional() - .map { savedCard -> cardSerializer.deserialize(savedCard.data) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(this) - .subscribe { rawCard -> navigator.goTo(CardScreen(rawCard)) } - } - } - - private fun exportToFile() { - val permission = ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) - if (permission != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(activity, - arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), - REQUEST_PERMISSION_STORAGE) - } else { - exportToFileWithPermission() - } - } - - private fun exportToFileWithPermission() { - Single.fromCallable { - val file = File(Environment.getExternalStorageDirectory(), FILENAME) - file.writeText(exportHelper.exportCards()) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(this) - .subscribe({ - Toast.makeText(activity, activity.getString(R.string.saved_to_x, FILENAME), Toast.LENGTH_SHORT) - .show() - }, { ex -> ErrorUtils.showErrorAlert(activity, ex) }) - } - - private fun importFromFile(uri: Uri) { - Single.fromCallable { - val json = activity.contentResolver.openInputStream(uri) - .bufferedReader() - .use { it.readText() } - exportHelper.importCards(json) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(this) - .subscribe { cards -> onCardsImported(cards) } - } - - @ScreenScope - @Component(dependencies = [MainActivity.MainActivityComponent::class]) - interface HistoryComponent { - fun inject(historyScreen: HistoryScreen) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryScreenView.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryScreenView.kt deleted file mode 100644 index 135a8c22a..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryScreenView.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * HistoryScreenView.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.history - -import android.annotation.SuppressLint -import android.content.Context -import androidx.appcompat.view.ActionMode -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import android.view.Menu -import android.view.MenuItem -import android.view.View -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.activity.ActivityOperations -import com.codebutler.farebot.app.core.kotlin.bindView -import com.jakewharton.rxrelay2.PublishRelay -import com.uber.autodispose.android.scope -import com.uber.autodispose.kotlin.autoDisposable -import com.wealthfront.magellan.BaseScreenView -import io.reactivex.Observable - -@SuppressLint("ViewConstructor") -class HistoryScreenView( - context: Context, - val activityOperations: ActivityOperations, - val listener: Listener -) : - BaseScreenView(context) { - - private val clicksRelay = PublishRelay.create() - private val selectionRelay = PublishRelay.create>() - - private val recyclerView: RecyclerView by bindView(R.id.recycler) - private val emptyView: View by bindView(R.id.empty) - - private var actionMode: ActionMode? = null - - init { - inflate(context, R.layout.screen_history, this) - recyclerView.layoutManager = LinearLayoutManager(context) - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - selectionRelay - .autoDisposable(scope()) - .subscribe { items -> - if (items.isNotEmpty()) { - if (actionMode == null) { - actionMode = activityOperations.startActionMode(object : ActionMode.Callback { - override fun onCreateActionMode(actionMode: ActionMode, menu: Menu): Boolean { - actionMode.menuInflater.inflate(R.menu.action_history, menu) - return true - } - - override fun onActionItemClicked(actionMode: ActionMode, menuItem: MenuItem): Boolean { - @Suppress("UNCHECKED_CAST") - when (menuItem.itemId) { - R.id.delete -> { - listener.onDeleteSelectedItems(actionMode.tag as List) - } - } - actionMode.finish() - return false - } - - override fun onPrepareActionMode(p0: ActionMode?, p1: Menu?): Boolean = false - - override fun onDestroyActionMode(actionMode: ActionMode?) { - this@HistoryScreenView.actionMode = null - (recyclerView.adapter as? HistoryAdapter)?.clearSelectedItems() - } - }) - } - actionMode?.title = items.size.toString() - actionMode?.tag = items - } else { - actionMode?.finish() - } - } - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - actionMode?.finish() - } - - internal fun observeItemClicks(): Observable = clicksRelay.hide() - - internal fun setViewModels(viewModels: List) { - recyclerView.adapter = HistoryAdapter(viewModels, clicksRelay, selectionRelay) - emptyView.visibility = if (viewModels.isEmpty()) View.VISIBLE else View.GONE - } - - interface Listener { - fun onDeleteSelectedItems(items: List) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryViewModel.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryViewModel.kt deleted file mode 100644 index 1d59c745b..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/history/HistoryViewModel.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * HistoryViewModel.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.history - -import com.codebutler.farebot.card.RawCard -import com.codebutler.farebot.persist.db.model.SavedCard -import com.codebutler.farebot.transit.TransitIdentity - -data class HistoryViewModel( - val savedCard: SavedCard, - val rawCard: RawCard<*>, - val transitIdentity: TransitIdentity? = null, - val parseException: Exception? = null, - var isSelected: Boolean = false -) diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/home/CardStream.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/home/CardStream.kt deleted file mode 100644 index 99917dbf4..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/home/CardStream.kt +++ /dev/null @@ -1,112 +0,0 @@ -/* - * CardStream.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.home - -import com.codebutler.farebot.app.core.app.FareBotApplication -import com.codebutler.farebot.app.core.kotlin.Optional -import com.codebutler.farebot.app.core.kotlin.filterAndGetOptional -import com.codebutler.farebot.app.core.nfc.NfcStream -import com.codebutler.farebot.app.core.nfc.TagReaderFactory -import com.codebutler.farebot.app.core.sample.RawSampleCard -import com.codebutler.farebot.app.core.serialize.CardKeysSerializer -import com.codebutler.farebot.base.util.ByteUtils -import com.codebutler.farebot.card.RawCard -import com.codebutler.farebot.card.serialize.CardSerializer -import com.codebutler.farebot.key.CardKeys -import com.codebutler.farebot.persist.CardKeysPersister -import com.codebutler.farebot.persist.CardPersister -import com.codebutler.farebot.persist.db.model.SavedCard -import com.jakewharton.rxrelay2.BehaviorRelay -import com.jakewharton.rxrelay2.PublishRelay -import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers -import java.util.concurrent.TimeUnit - -class CardStream( - private val application: FareBotApplication, - private val cardPersister: CardPersister, - private val cardSerializer: CardSerializer, - private val cardKeysPersister: CardKeysPersister, - private val cardKeysSerializer: CardKeysSerializer, - private val nfcStream: NfcStream, - private val tagReaderFactory: TagReaderFactory -) { - - private val loadingRelay: BehaviorRelay = BehaviorRelay.createDefault(false) - private val errorRelay: PublishRelay = PublishRelay.create() - private val sampleRelay: PublishRelay> = PublishRelay.create() - - fun observeCards(): Observable> { - val realCards = nfcStream.observe() - .observeOn(Schedulers.io()) - .doOnNext { loadingRelay.accept(true) } - .map { tag -> Optional( - try { - val cardKeys = getCardKeys(ByteUtils.getHexString(tag.id)) - val rawCard = tagReaderFactory.getTagReader(tag.id, tag, cardKeys).readTag() - if (rawCard.isUnauthorized) { - throw CardUnauthorizedException() - } - rawCard - } catch (error: Throwable) { - errorRelay.accept(error) - loadingRelay.accept(false) - null - }) - } - .filterAndGetOptional() - - val sampleCards = sampleRelay - .observeOn(Schedulers.io()) - .doOnNext { loadingRelay.accept(true) } - .delay(3, TimeUnit.SECONDS) - - return Observable.merge(realCards, sampleCards) - .doOnNext { card -> - application.updateTimestamp(card.tagId().hex()) - cardPersister.insertCard(SavedCard( - type = card.cardType(), - serial = card.tagId().hex(), - data = cardSerializer.serialize(card))) - } - .doOnNext { loadingRelay.accept(false) } - } - - fun observeLoading(): Observable = loadingRelay.hide() - - fun observeErrors(): Observable = errorRelay.hide() - - fun emitSample() { - sampleRelay.accept(RawSampleCard()) - } - - private fun getCardKeys(tagId: String): CardKeys? { - val savedKey = cardKeysPersister.getForTagId(tagId) ?: return null - return cardKeysSerializer.deserialize(savedKey.keyData) - } - - class CardUnauthorizedException : Throwable() { - override val message: String? - get() = "Unauthorized" - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/home/HomeScreen.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/home/HomeScreen.kt deleted file mode 100644 index de2375f28..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/home/HomeScreen.kt +++ /dev/null @@ -1,160 +0,0 @@ -/* - * HomeScreen.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.home - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.nfc.NfcAdapter -import android.nfc.TagLostException -import android.provider.Settings -import androidx.appcompat.app.AlertDialog -import android.view.Menu -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.activity.ActivityOperations -import com.codebutler.farebot.app.core.analytics.AnalyticsEventName -import com.codebutler.farebot.app.core.analytics.logAnalyticsEvent -import com.codebutler.farebot.app.core.inject.ScreenScope -import com.codebutler.farebot.app.core.ui.ActionBarOptions -import com.codebutler.farebot.app.core.ui.FareBotScreen -import com.codebutler.farebot.app.core.util.ErrorUtils -import com.codebutler.farebot.app.feature.card.CardScreen -import com.codebutler.farebot.app.feature.help.HelpScreen -import com.codebutler.farebot.app.feature.history.HistoryScreen -import com.codebutler.farebot.app.feature.keys.KeysScreen -import com.codebutler.farebot.app.feature.main.MainActivity.MainActivityComponent -import com.codebutler.farebot.app.feature.prefs.FareBotPreferenceActivity -import com.crashlytics.android.Crashlytics -import com.uber.autodispose.kotlin.autoDisposable -import dagger.Component -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers -import javax.inject.Inject - -class HomeScreen : FareBotScreen(), - HomeScreenView.Listener { - - companion object { - private val URL_ABOUT = Uri.parse("https://codebutler.github.com/farebot") - } - - @Inject lateinit var activityOperations: ActivityOperations - @Inject lateinit var cardStream: CardStream - - override fun onCreateView(context: Context): HomeScreenView = HomeScreenView(context, this) - - override fun getTitle(context: Context): String = context.getString(R.string.app_name) - - override fun getActionBarOptions(): ActionBarOptions = ActionBarOptions(shadow = false) - - override fun onShow(context: Context) { - super.onShow(context) - - activityOperations.menuItemClick - .autoDisposable(this) - .subscribe({ menuItem -> - when (menuItem.itemId) { - R.id.history -> navigator.goTo(HistoryScreen()) - R.id.help -> navigator.goTo(HelpScreen()) - R.id.prefs -> activity.startActivity(FareBotPreferenceActivity.newIntent(activity)) - R.id.keys -> navigator.goTo(KeysScreen()) - R.id.about -> { - activity.startActivity(Intent(Intent.ACTION_VIEW, URL_ABOUT)) - } - } - }) - - val adapter = NfcAdapter.getDefaultAdapter(context) - if (adapter == null) { - view.showNfcError(HomeScreenView.NfcError.UNAVAILABLE) - } else if (!adapter.isEnabled) { - view.showNfcError(HomeScreenView.NfcError.DISABLED) - } else { - view.showNfcError(HomeScreenView.NfcError.NONE) - } - - cardStream.observeCards() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(this) - .subscribe { card -> - logAnalyticsEvent(AnalyticsEventName.SCAN_CARD, card.cardType().toString()) - navigator.goTo(CardScreen(card)) - } - - cardStream.observeLoading() - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(this) - .subscribe { loading -> view.showLoading(loading) } - - cardStream.observeErrors() - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(this) - .subscribe { ex -> - logAnalyticsEvent(AnalyticsEventName.SCAN_CARD_ERROR, ErrorUtils.getErrorMessage(ex)) - when (ex) { - is CardStream.CardUnauthorizedException -> AlertDialog.Builder(activity) - .setTitle(R.string.locked_card) - .setMessage(R.string.keys_required) - .setPositiveButton(android.R.string.ok, null) - .show() - is TagLostException -> AlertDialog.Builder(activity) - .setTitle(R.string.tag_lost) - .setMessage(R.string.tag_lost_message) - .setPositiveButton(android.R.string.ok, null) - .show() - else -> { - Crashlytics.logException(ex) - ErrorUtils.showErrorAlert(activity, ex) - } - } - } - } - - override fun onUpdateMenu(menu: Menu) { - activity.menuInflater.inflate(R.menu.screen_main, menu) - } - - override fun onNfcErrorButtonClicked() { - activity.startActivity(Intent(Settings.ACTION_WIRELESS_SETTINGS)) - } - - override fun onSampleButtonClicked() { - cardStream.emitSample() - } - - override fun createComponent(parentComponent: MainActivityComponent): HomeComponent = - DaggerHomeScreen_HomeComponent.builder() - .mainActivityComponent(parentComponent) - .build() - - override fun inject(component: HomeComponent) { - component.inject(this) - } - - @ScreenScope - @Component(dependencies = arrayOf(MainActivityComponent::class)) - interface HomeComponent { - fun inject(homeScreen: HomeScreen) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/home/HomeScreenView.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/home/HomeScreenView.kt deleted file mode 100644 index c0acff447..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/home/HomeScreenView.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * HomeScreenView.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.home - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.annotation.SuppressLint -import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.view.ViewPropertyAnimator -import android.widget.ImageView -import android.widget.ProgressBar -import android.widget.TextView -import com.codebutler.farebot.BuildConfig -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.kotlin.bindView -import com.wealthfront.magellan.BaseScreenView - -@SuppressLint("ViewConstructor") -class HomeScreenView internal constructor(ctx: Context, private val listener: Listener) : - BaseScreenView(ctx) { - - private val splashImageView: ImageView by bindView(R.id.splash) - private val progressBar: ProgressBar by bindView(R.id.progress) - private val errorViewGroup: ViewGroup by bindView(R.id.nfc_error_viewgroup) - private val errorTextView: TextView by bindView(R.id.nfc_error_text) - private val errorButton: TextView by bindView(R.id.nfc_error_button) - - private val shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime).toLong() - - private var fadeInAnim: ViewPropertyAnimator? = null - private var fadeOutAnim: ViewPropertyAnimator? = null - - init { - inflate(context, R.layout.screen_home, this) - errorButton.setOnClickListener { listener.onNfcErrorButtonClicked() } - - if (BuildConfig.DEBUG) { - splashImageView.setOnLongClickListener { listener.onSampleButtonClicked(); true } - } - } - - fun showLoading(show: Boolean) { - fadeInAnim?.cancel() - fadeOutAnim?.cancel() - - val viewFadeIn = if (show) progressBar else splashImageView - val viewFadeOut = if (show) splashImageView else progressBar - - viewFadeIn.alpha = 0f - viewFadeIn.visibility = View.VISIBLE - - fadeInAnim = viewFadeIn.animate() - .alpha(1f) - .setDuration(shortAnimationDuration) - .setListener(null) - - fadeOutAnim = viewFadeOut.animate() - .alpha(0f) - .setDuration(shortAnimationDuration) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - viewFadeOut.visibility = View.GONE - } - }) - } - - internal fun showNfcError(error: NfcError) { - if (error == NfcError.NONE) { - errorViewGroup.visibility = View.GONE - return - } - when (error) { - HomeScreenView.NfcError.DISABLED -> { - errorTextView.setText(R.string.nfc_off_error) - errorButton.visibility = View.VISIBLE - } - HomeScreenView.NfcError.UNAVAILABLE -> { - errorTextView.setText(R.string.nfc_unavailable) - errorButton.visibility = View.GONE - } - HomeScreenView.NfcError.NONE -> { /* Unreachable */ } - } - errorViewGroup.visibility = View.VISIBLE - } - - internal enum class NfcError { - NONE, - DISABLED, - UNAVAILABLE - } - - internal interface Listener { - fun onNfcErrorButtonClicked() - fun onSampleButtonClicked() - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeysAdapter.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeysAdapter.kt deleted file mode 100644 index fb7e57d0b..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeysAdapter.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * KeysAdapter.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.keys - -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.kotlin.bindView -import com.codebutler.farebot.app.core.kotlin.inflate -import com.jakewharton.rxrelay2.PublishRelay - -class KeysAdapter( - private val viewModels: List, - private val selectionRelay: PublishRelay> -) : - RecyclerView.Adapter() { - - override fun onCreateViewHolder(parent: ViewGroup, position: Int): KeyViewHolder = - KeyViewHolder(parent.inflate(R.layout.item_key)) - - override fun onBindViewHolder(holder: KeyViewHolder, position: Int) { - val viewModel = viewModels[position] - holder.update(viewModel) - holder.itemView.setOnClickListener { - if (hasSelectedItems()) { - viewModel.isSelected = !viewModel.isSelected - notifySelectionChanged() - } - } - holder.itemView.setOnLongClickListener { - if (!hasSelectedItems()) { - viewModel.isSelected = true - notifySelectionChanged() - true - } else { - false - } - } - } - - override fun getItemCount(): Int = viewModels.size - - private fun hasSelectedItems(): Boolean = viewModels.any { it.isSelected } - - private fun notifySelectionChanged() { - notifyDataSetChanged() - selectionRelay.accept(viewModels.filter { it.isSelected }) - } - - fun clearSelectedItems() { - for (viewModel in viewModels) { - viewModel.isSelected = false - } - notifySelectionChanged() - } - - class KeyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - private val textView1: TextView by bindView(android.R.id.text1) - private val textView2: TextView by bindView(android.R.id.text2) - - internal fun update(viewModel: KeyViewModel) { - textView1.text = viewModel.savedKey.cardId - textView2.text = viewModel.savedKey.cardType.toString() - itemView.isSelected = viewModel.isSelected - } - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeysScreen.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeysScreen.kt deleted file mode 100644 index d9004a80b..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeysScreen.kt +++ /dev/null @@ -1,121 +0,0 @@ -/* - * KeysScreen.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.keys - -import android.content.Context -import android.view.Menu -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.activity.ActivityOperations -import com.codebutler.farebot.app.core.inject.ScreenScope -import com.codebutler.farebot.app.core.ui.ActionBarOptions -import com.codebutler.farebot.app.core.ui.FareBotScreen -import com.codebutler.farebot.app.core.util.ErrorUtils -import com.codebutler.farebot.app.feature.keys.add.AddKeyScreen -import com.codebutler.farebot.app.feature.main.MainActivity -import com.codebutler.farebot.persist.CardKeysPersister -import com.codebutler.farebot.persist.db.model.SavedKey -import com.uber.autodispose.kotlin.autoDisposable -import dagger.Component -import io.reactivex.Single -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers -import javax.inject.Inject - -class KeysScreen : FareBotScreen(), KeysScreenView.Listener { - - @Inject lateinit var activityOperations: ActivityOperations - @Inject lateinit var keysPersister: CardKeysPersister - - override fun getTitle(context: Context): String = context.getString(R.string.keys) - - override fun getActionBarOptions(): ActionBarOptions = ActionBarOptions( - backgroundColorRes = R.color.accent, - textColorRes = R.color.white - ) - - override fun onCreateView(context: Context): KeysScreenView = KeysScreenView(context, activityOperations, this) - - override fun onShow(context: Context) { - super.onShow(context) - - activityOperations.menuItemClick - .autoDisposable(this) - .subscribe({ menuItem -> - when (menuItem.itemId) { - R.id.add -> navigator.goTo(AddKeyScreen()) - } - }) - - loadKeys() - } - - override fun onUpdateMenu(menu: Menu?) { - activity.menuInflater.inflate(R.menu.screen_keys, menu) - } - - override fun onDeleteSelectedItems(items: List) { - for ((savedKey) in items) { - keysPersister.delete(savedKey) - } - loadKeys() - } - - override fun createComponent(parentComponent: MainActivity.MainActivityComponent): KeysScreen.KeysComponent = - DaggerKeysScreen_KeysComponent.builder() - .mainActivityComponent(parentComponent) - .build() - - override fun inject(component: KeysScreen.KeysComponent) { - component.inject(this) - } - - private fun loadKeys() { - observeKeys() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(this) - .subscribe( - { keys -> view.setViewModels(keys) }, - { e -> ErrorUtils.showErrorToast(activity, e) } - ) - } - - private fun observeKeys(): Single> { - return Single.create> { e -> - try { - e.onSuccess(keysPersister.savedKeys) - } catch (error: Throwable) { - e.onError(error) - } - }.map { savedKeys -> - savedKeys.map { savedKey -> KeyViewModel(savedKey) } - } - } - - @ScreenScope - @Component(dependencies = arrayOf(MainActivity.MainActivityComponent::class)) - interface KeysComponent { - - fun inject(screen: KeysScreen) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeysScreenView.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeysScreenView.kt deleted file mode 100644 index 2c6c2b1e5..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/KeysScreenView.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * KeysScreenView.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.keys - -import android.annotation.SuppressLint -import android.content.Context -import androidx.appcompat.view.ActionMode -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import android.view.Menu -import android.view.MenuItem -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.activity.ActivityOperations -import com.codebutler.farebot.app.core.kotlin.bindView -import com.jakewharton.rxrelay2.PublishRelay -import com.uber.autodispose.android.scope -import com.uber.autodispose.kotlin.autoDisposable -import com.wealthfront.magellan.BaseScreenView - -@SuppressLint("ViewConstructor") -class KeysScreenView( - context: Context, - val activityOperations: ActivityOperations, - val listener: KeysScreenView.Listener -) : - BaseScreenView(context) { - - private val selectionRelay = PublishRelay.create>() - - private val recyclerView: RecyclerView by bindView(R.id.recycler) - - private var actionMode: ActionMode? = null - - init { - inflate(context, R.layout.screen_keys, this) - recyclerView.layoutManager = LinearLayoutManager(context) - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - selectionRelay - .autoDisposable(scope()) - .subscribe { items -> - if (items.isNotEmpty()) { - if (actionMode == null) { - actionMode = activityOperations.startActionMode(object : ActionMode.Callback { - override fun onCreateActionMode(actionMode: ActionMode, menu: Menu): Boolean { - actionMode.menuInflater.inflate(R.menu.action_keys, menu) - return true - } - - override fun onActionItemClicked(actionMode: ActionMode, menuItem: MenuItem): Boolean { - @Suppress("UNCHECKED_CAST") - when (menuItem.itemId) { - R.id.delete -> { - listener.onDeleteSelectedItems(actionMode.tag as List) - } - } - actionMode.finish() - return false - } - - override fun onPrepareActionMode(p0: ActionMode?, p1: Menu?): Boolean = false - - override fun onDestroyActionMode(actionMode: ActionMode?) { - this@KeysScreenView.actionMode = null - (recyclerView.adapter as? KeysAdapter)?.clearSelectedItems() - } - }) - } - actionMode?.title = items.size.toString() - actionMode?.tag = items - } else { - actionMode?.finish() - } - } - } - - fun setViewModels(viewModels: List) { - recyclerView.adapter = KeysAdapter(viewModels, selectionRelay) - } - - interface Listener { - fun onDeleteSelectedItems(items: List) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/add/AddKeyScreen.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/add/AddKeyScreen.kt deleted file mode 100644 index c037d103a..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/add/AddKeyScreen.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * AddKeyScreen.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.keys.add - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.nfc.Tag -import android.os.Environment -import androidx.appcompat.app.AlertDialog -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.activity.ActivityOperations -import com.codebutler.farebot.app.core.inject.ScreenScope -import com.codebutler.farebot.app.core.nfc.NfcStream -import com.codebutler.farebot.app.core.serialize.CardKeysSerializer -import com.codebutler.farebot.app.core.ui.ActionBarOptions -import com.codebutler.farebot.app.core.ui.FareBotScreen -import com.codebutler.farebot.app.feature.main.MainActivity -import com.codebutler.farebot.base.util.ByteUtils -import com.codebutler.farebot.card.CardType -import com.codebutler.farebot.card.classic.key.ClassicCardKeys -import com.codebutler.farebot.persist.CardKeysPersister -import com.codebutler.farebot.persist.db.model.SavedKey -import com.uber.autodispose.kotlin.autoDisposable -import dagger.Component -import io.reactivex.android.schedulers.AndroidSchedulers -import javax.inject.Inject - -class AddKeyScreen : FareBotScreen(), AddKeyScreenView.Listener { - - companion object { - private val REQUEST_SELECT_FILE = 1 - } - - @Inject lateinit var activityOperations: ActivityOperations - @Inject lateinit var cardKeysPersister: CardKeysPersister - @Inject lateinit var cardKeysSerializer: CardKeysSerializer - @Inject lateinit var nfcStream: NfcStream - - private var tagInfo: TagInfo = TagInfo() - - override fun getTitle(context: Context): String = context.getString(R.string.add_key) - - override fun getActionBarOptions(): ActionBarOptions = ActionBarOptions( - backgroundColorRes = R.color.accent, - textColorRes = R.color.white - ) - - override fun onShow(context: Context) { - super.onShow(context) - - nfcStream.observe() - .observeOn(AndroidSchedulers.mainThread()) - .autoDisposable(this) - .subscribe { tag -> tag.id - val cardType = getCardType(tag) - if (cardType == null) { - AlertDialog.Builder(activity) - .setMessage(R.string.card_keys_not_supported) - .setPositiveButton(android.R.string.ok, null) - .show() - return@subscribe - } - tagInfo.tagId = tag.id - tagInfo.cardType = cardType - view.update(tagInfo) - } - - activityOperations.activityResult - .autoDisposable(this) - .subscribe { (requestCode, resultCode, dataIntent) -> - when (requestCode) { - REQUEST_SELECT_FILE -> { - if (resultCode == Activity.RESULT_OK && dataIntent != null) { - setKey(activity.contentResolver.openInputStream(dataIntent.data).readBytes()) - } - } - } - } - } - - override fun onCreateView(context: Context): AddKeyScreenView = AddKeyScreenView(context, this) - - override fun onImportFile() { - val storageUri = Uri.fromFile(Environment.getExternalStorageDirectory()) - val target = Intent(Intent.ACTION_GET_CONTENT) - target.putExtra(Intent.EXTRA_STREAM, storageUri) - target.type = "*/*" - activity.startActivityForResult( - Intent.createChooser(target, activity.getString(R.string.select_file)), - REQUEST_SELECT_FILE) - } - - override fun onSave() { - val tagId = tagInfo.tagId - val keyData = tagInfo.keyData - val cardType = tagInfo.cardType - if (tagId != null && keyData != null && cardType != null) { - val serializedKey = cardKeysSerializer.serialize(ClassicCardKeys.fromProxmark3(keyData)) - cardKeysPersister.insert(SavedKey( - cardId = ByteUtils.getHexString(tagId), - cardType = cardType, - keyData = serializedKey)) - navigator.goBack() - } - } - - override fun createComponent(parentComponent: MainActivity.MainActivityComponent): AddKeyComponent = - DaggerAddKeyScreen_AddKeyComponent.builder() - .mainActivityComponent(parentComponent) - .build() - - override fun inject(component: AddKeyComponent) { - component.inject(this) - } - - private fun setKey(keyData: ByteArray) { - tagInfo.keyData = keyData - view.update(tagInfo) - } - - private fun getCardType(tag: Tag): CardType? = when { - "android.nfc.tech.MifareClassic" in tag.techList -> CardType.MifareClassic - else -> null - } - - data class TagInfo(var tagId: ByteArray? = null, var cardType: CardType? = null, var keyData: ByteArray? = null) - - @ScreenScope - @Component(dependencies = arrayOf(MainActivity.MainActivityComponent::class)) - interface AddKeyComponent { - - fun inject(screen: AddKeyScreen) - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/add/AddKeyScreenView.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/add/AddKeyScreenView.kt deleted file mode 100644 index e91e7c987..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/keys/add/AddKeyScreenView.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * AddKeyScreenView.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.keys.add - -import android.annotation.SuppressLint -import android.content.Context -import android.view.View -import android.widget.Button -import android.widget.TextView -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.kotlin.bindView -import com.codebutler.farebot.base.util.ByteUtils -import com.wealthfront.magellan.BaseScreenView - -@SuppressLint("ViewConstructor") -class AddKeyScreenView(context: Context, private val listener: Listener) : - BaseScreenView(context) { - - private val cardTypeTextView: TextView by bindView(R.id.card_type) - private val contentView: View by bindView(R.id.content) - private val importFileButton: Button by bindView(R.id.import_file) - private val keyDataTextView: TextView by bindView(R.id.key_data) - private val saveButton: Button by bindView(R.id.save) - private val splashView: View by bindView(R.id.splash) - private val tagIdTextView: TextView by bindView(R.id.tag_id) - - init { - inflate(context, R.layout.screen_keys_add, this) - - importFileButton.setOnClickListener { listener.onImportFile() } - saveButton.setOnClickListener { listener.onSave() } - } - - fun update(tagInfo: AddKeyScreen.TagInfo) { - tagIdTextView.text = ByteUtils.getHexString(tagInfo.tagId) - cardTypeTextView.text = tagInfo.cardType.toString() - keyDataTextView.text = ByteUtils.getHexString(tagInfo.keyData, null) - - contentView.visibility = if (tagInfo.tagId != null) View.VISIBLE else View.GONE - splashView.visibility = if (tagInfo.tagId != null) View.GONE else View.VISIBLE - saveButton.isEnabled = tagInfo.tagId != null && tagInfo.cardType != null && tagInfo.keyData != null - } - - interface Listener { - fun onImportFile() - fun onSave() - } -} diff --git a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/main/MainActivity.kt b/farebot-app/src/main/java/com/codebutler/farebot/app/feature/main/MainActivity.kt deleted file mode 100644 index 7df8b0ac5..000000000 --- a/farebot-app/src/main/java/com/codebutler/farebot/app/feature/main/MainActivity.kt +++ /dev/null @@ -1,297 +0,0 @@ -/* - * MainActivity.kt - * - * This file is part of FareBot. - * Learn more at: https://codebutler.github.io/farebot/ - * - * Copyright (C) 2017 Eric Butler - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.codebutler.farebot.app.feature.main - -import android.animation.ObjectAnimator -import android.annotation.SuppressLint -import android.content.Intent -import android.content.SharedPreferences -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.os.Bundle -import android.os.Handler -import com.google.android.material.appbar.AppBarLayout -import androidx.core.view.ViewCompat -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.Toolbar -import android.view.Menu -import android.view.MenuItem -import android.view.View -import com.codebutler.farebot.R -import com.codebutler.farebot.app.core.activity.ActivityOperations -import com.codebutler.farebot.app.core.activity.ActivityResult -import com.codebutler.farebot.app.core.activity.RequestPermissionsResult -import com.codebutler.farebot.app.core.app.FareBotApplication -import com.codebutler.farebot.app.core.app.FareBotApplicationComponent -import com.codebutler.farebot.app.core.inject.ActivityScope -import com.codebutler.farebot.app.core.kotlin.adjustAlpha -import com.codebutler.farebot.app.core.kotlin.bindView -import com.codebutler.farebot.app.core.kotlin.getColor -import com.codebutler.farebot.app.core.nfc.NfcStream -import com.codebutler.farebot.app.core.nfc.TagReaderFactory -import com.codebutler.farebot.app.core.serialize.CardKeysSerializer -import com.codebutler.farebot.app.core.transit.TransitFactoryRegistry -import com.codebutler.farebot.app.core.ui.FareBotCrossfadeTransition -import com.codebutler.farebot.app.core.ui.FareBotScreen -import com.codebutler.farebot.app.core.util.ExportHelper -import com.codebutler.farebot.app.feature.home.CardStream -import com.codebutler.farebot.app.feature.home.HomeScreen -import com.codebutler.farebot.card.serialize.CardSerializer -import com.codebutler.farebot.persist.CardKeysPersister -import com.codebutler.farebot.persist.CardPersister -import com.jakewharton.rxrelay2.PublishRelay -import com.wealthfront.magellan.ActionBarConfig -import com.wealthfront.magellan.NavigationListener -import com.wealthfront.magellan.Navigator -import com.wealthfront.magellan.Screen -import com.wealthfront.magellan.ScreenLifecycleListener -import dagger.BindsInstance -import dagger.Component -import dagger.Module -import dagger.Provides -import javax.inject.Inject - -class MainActivity : AppCompatActivity(), - ScreenLifecycleListener, - NavigationListener { - - @Inject internal lateinit var navigator: Navigator - @Inject internal lateinit var nfcStream: NfcStream - - private val appBarLayout by bindView(R.id.appBarLayout) - private val toolbar by bindView(R.id.toolbar) - - private val activityResultRelay = PublishRelay.create() - private val handler = Handler() - private val menuItemClickRelay = PublishRelay.create() - private val permissionsResultRelay = PublishRelay.create() - - private val shortAnimationDuration: Long by lazy { - resources.getInteger(android.R.integer.config_shortAnimTime).toLong() - } - - private val toolbarElevation: Float by lazy { - resources.getDimensionPixelSize(R.dimen.toolbar_elevation).toFloat() - } - - private var animToolbarBg: ObjectAnimator? = null - - val component: MainActivityComponent by lazy { - DaggerMainActivity_MainActivityComponent.builder() - .applicationComponent((application as FareBotApplication).component) - .activity(this) - .mainActivityModule(MainActivityModule()) - .activityOperations(ActivityOperations( - this, - activityResultRelay.hide(), - menuItemClickRelay.hide(), - permissionsResultRelay.hide())) - .build() - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - setSupportActionBar(toolbar) - component.inject(this) - navigator.addLifecycleListener(this) - nfcStream.onCreate(this, savedInstanceState) - } - - override fun onPostCreate(savedInstanceState: Bundle?) { - super.onPostCreate(savedInstanceState) - navigator.onCreate(this, savedInstanceState) - } - - override fun onSaveInstanceState(outState: Bundle?) { - super.onSaveInstanceState(outState) - navigator.onSaveInstanceState(outState) - } - - override fun onResume() { - super.onResume() - navigator.onResume(this) - nfcStream.onResume() - } - - override fun onPause() { - super.onPause() - navigator.onPause(this) - nfcStream.onPause() - } - - override fun onDestroy() { - super.onDestroy() - navigator.removeLifecycleListener(this) - navigator.onDestroy(this) - } - - override fun onBackPressed() { - if (!navigator.handleBack()) { - super.onBackPressed() - } - } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - navigator.onCreateOptionsMenu(menu) - return true - } - - override fun onPrepareOptionsMenu(menu: Menu): Boolean { - navigator.onPrepareOptionsMenu(menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } - menuItemClickRelay.accept(item) - return true - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - handler.post { - activityResultRelay.accept(ActivityResult(requestCode, resultCode, data)) - } - } - - override fun onNavigate(actionBarConfig: ActionBarConfig) { - toolbar.visibility = if (actionBarConfig.visible()) View.VISIBLE else View.GONE - } - - @SuppressLint("ResourceType") // Lint bug? - override fun onShow(screen: Screen<*>) { - val options = (screen as FareBotScreen<*, *>).getActionBarOptions() - - supportActionBar?.setDisplayHomeAsUpEnabled(!navigator.atRoot()) - - toolbar.setTitleTextColor(getColor(options.textColorRes, Color.BLACK)) - toolbar.title = screen.getTitle(this) - toolbar.subtitle = null - - val newColor = getColor(options.backgroundColorRes, Color.TRANSPARENT) - val curColor = (toolbar.background as? ColorDrawable)?.color ?: Color.TRANSPARENT - - val curColorForAnim = if (curColor == Color.TRANSPARENT) adjustAlpha(newColor) else curColor - val newColorForAnim = if (newColor == Color.TRANSPARENT) adjustAlpha(curColor) else newColor - - animToolbarBg?.cancel() - animToolbarBg = ObjectAnimator.ofArgb(toolbar, "backgroundColor", curColorForAnim, newColorForAnim).apply { - duration = shortAnimationDuration - start() - } - - ViewCompat.setElevation(appBarLayout, if (options.shadow) toolbarElevation else 0f) - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - permissionsResultRelay.accept(RequestPermissionsResult(requestCode, permissions, grantResults)) - } - - override fun onHide(screen: Screen<*>?) { } - - @Module - class MainActivityModule { - - @Provides - @ActivityScope - fun provideNfcTagStream(activity: MainActivity): NfcStream = NfcStream(activity) - - @Provides - @ActivityScope - fun provideCardStream( - application: FareBotApplication, - cardPersister: CardPersister, - cardSerializer: CardSerializer, - cardKeysPersister: CardKeysPersister, - cardKeysSerializer: CardKeysSerializer, - nfcStream: NfcStream, - tagReaderFactory: TagReaderFactory - ): CardStream { - return CardStream( - application, - cardPersister, - cardSerializer, - cardKeysPersister, - cardKeysSerializer, - nfcStream, - tagReaderFactory) - } - - @Provides - @ActivityScope - fun provideNavigator(activity: MainActivity): Navigator = Navigator.withRoot(HomeScreen()) - .transition(FareBotCrossfadeTransition(activity)) - .build() - } - - @ActivityScope - @Component(dependencies = arrayOf(FareBotApplicationComponent::class), modules = arrayOf(MainActivityModule::class)) - interface MainActivityComponent { - - fun activityOperations(): ActivityOperations - - fun application(): FareBotApplication - - fun cardPersister(): CardPersister - - fun cardSerializer(): CardSerializer - - fun cardKeysPersister(): CardKeysPersister - - fun cardKeysSerializer(): CardKeysSerializer - - fun cardStream(): CardStream - - fun exportHelper(): ExportHelper - - fun nfcStream(): NfcStream - - fun sharedPreferences(): SharedPreferences - - fun tagReaderFactory(): TagReaderFactory - - fun transitFactoryRegistry(): TransitFactoryRegistry - - fun inject(mainActivity: MainActivity) - - @Component.Builder - interface Builder { - - fun applicationComponent(applicationComponent: FareBotApplicationComponent): Builder - - fun mainActivityModule(mainActivityModule: MainActivityModule): Builder - - @BindsInstance - fun activity(activity: MainActivity): Builder - - @BindsInstance - fun activityOperations(activityOperations: ActivityOperations): Builder - - fun build(): MainActivityComponent - } - } -} diff --git a/farebot-app/src/main/res/layout/activity_main.xml b/farebot-app/src/main/res/layout/activity_main.xml deleted file mode 100644 index c89b83201..000000000 --- a/farebot-app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - diff --git a/farebot-app/src/main/res/layout/item_card_advanced.xml b/farebot-app/src/main/res/layout/item_card_advanced.xml deleted file mode 100644 index 44b61943a..000000000 --- a/farebot-app/src/main/res/layout/item_card_advanced.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - diff --git a/farebot-app/src/main/res/layout/item_history.xml b/farebot-app/src/main/res/layout/item_history.xml deleted file mode 100644 index cc5ae2971..000000000 --- a/farebot-app/src/main/res/layout/item_history.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/farebot-app/src/main/res/layout/item_key.xml b/farebot-app/src/main/res/layout/item_key.xml deleted file mode 100644 index 3cdb615c9..000000000 --- a/farebot-app/src/main/res/layout/item_key.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - diff --git a/farebot-app/src/main/res/layout/item_supported_card.xml b/farebot-app/src/main/res/layout/item_supported_card.xml deleted file mode 100644 index 6c92b0132..000000000 --- a/farebot-app/src/main/res/layout/item_supported_card.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/farebot-app/src/main/res/layout/item_transaction.xml b/farebot-app/src/main/res/layout/item_transaction.xml deleted file mode 100644 index c0c4b93a8..000000000 --- a/farebot-app/src/main/res/layout/item_transaction.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - diff --git a/farebot-app/src/main/res/layout/item_transaction_refill.xml b/farebot-app/src/main/res/layout/item_transaction_refill.xml deleted file mode 100644 index 12c40f256..000000000 --- a/farebot-app/src/main/res/layout/item_transaction_refill.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/farebot-app/src/main/res/layout/item_transaction_subscription.xml b/farebot-app/src/main/res/layout/item_transaction_subscription.xml deleted file mode 100644 index 9b42ce4d2..000000000 --- a/farebot-app/src/main/res/layout/item_transaction_subscription.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/farebot-app/src/main/res/layout/item_transaction_trip.xml b/farebot-app/src/main/res/layout/item_transaction_trip.xml deleted file mode 100644 index db6faf436..000000000 --- a/farebot-app/src/main/res/layout/item_transaction_trip.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/farebot-app/src/main/res/layout/screen_card.xml b/farebot-app/src/main/res/layout/screen_card.xml deleted file mode 100644 index cfb1f8604..000000000 --- a/farebot-app/src/main/res/layout/screen_card.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/farebot-app/src/main/res/layout/screen_card_advanced.xml b/farebot-app/src/main/res/layout/screen_card_advanced.xml deleted file mode 100644 index d612bf9f5..000000000 --- a/farebot-app/src/main/res/layout/screen_card_advanced.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - diff --git a/farebot-app/src/main/res/layout/screen_help.xml b/farebot-app/src/main/res/layout/screen_help.xml deleted file mode 100644 index 0dca32dc8..000000000 --- a/farebot-app/src/main/res/layout/screen_help.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - diff --git a/farebot-app/src/main/res/layout/screen_history.xml b/farebot-app/src/main/res/layout/screen_history.xml deleted file mode 100644 index 96d9341b7..000000000 --- a/farebot-app/src/main/res/layout/screen_history.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - diff --git a/farebot-app/src/main/res/layout/screen_home.xml b/farebot-app/src/main/res/layout/screen_home.xml deleted file mode 100644 index 5b2fb0240..000000000 --- a/farebot-app/src/main/res/layout/screen_home.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - -