diff --git a/LICENSE b/LICENSE deleted file mode 100644 index d511905c..00000000 --- a/LICENSE +++ /dev/null @@ -1,339 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - 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 2 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, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..ffd20e50 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +Ack Android +=========== + +[Download] The Ack Android Application from the Google Play Store! + +---------- + +In order to support development for the open source decoding library, +the Ack Android client must now be purchased through the play store. +Any support is appreciated! + +The [Ack] open-source decoding core library remains open source! + +[Download]: https://play.google.com/store/apps/details?id=com.inkapplications.ack.android +[Ack]: https://ack.inkapplications.com + diff --git a/android-application/build.gradle.kts b/android-application/build.gradle.kts deleted file mode 100644 index 3fec3d40..00000000 --- a/android-application/build.gradle.kts +++ /dev/null @@ -1,144 +0,0 @@ -plugins { - id("com.android.application") - kotlin("android") - kotlin("kapt") - id("com.google.gms.google-services") apply false - id("com.google.android.gms.oss-licenses-plugin") - id("com.google.firebase.crashlytics") - id("com.google.dagger.hilt.android") -} - -val useGoogleServices = project.file("google-services.json").exists() - -if (useGoogleServices) { - apply(plugin = "com.google.gms.google-services") -} - -android { - compileSdk = 35 - namespace = "com.inkapplications.ack.android" - defaultConfig { - minSdk = 21 - targetSdk = 35 - multiDexEnabled = true - buildConfigField("boolean", "USE_GOOGLE_SERVICES", useGoogleServices.toString()) - buildConfigField("String", "COMMIT", optionalStringProperty("commit").buildQuote()) - versionCode = intProperty("versionCode", 1) - versionName = stringProperty("versionName", "SNAPSHOT") - javaCompileOptions.annotationProcessorOptions.arguments["dagger.hilt.disableModulesHaveInstallInCheck"] = "true" - } - buildFeatures { - compose = true - buildConfig = true - } - - signingConfigs { - create("parameterSigning") { - storeFile = project.properties.getOrDefault("signingFile", null) - ?.toString() - ?.let { File("${project.rootDir}/$it") } - keyAlias = project.properties.getOrDefault("signingAlias", null)?.toString() - keyPassword = project.properties.getOrDefault("signingKeyPassword", null)?.toString() - storePassword = project.properties.getOrDefault("signingStorePassword", null)?.toString() - } - } - - buildTypes { - getByName("debug") { - applicationIdSuffix = ".debug" - resValue("string", "app_name", "Ack: APRS*") - } - getByName("release") { - resValue("string", "app_name", "Ack: APRS") - applicationIdSuffix = if (project.booleanProperty("snapshot", false)) ".snapshot" else null - signingConfig = if (project.hasProperty("signingFile")) { - signingConfigs.getByName("parameterSigning") - } else { - signingConfigs.getByName("debug") - } - - isMinifyEnabled = true - isShrinkResources = true - proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") - } - } - flavorDimensions += "function" - productFlavors { - create("functional") { - isDefault = true - dimension = "function" - } - create("stub") { - dimension = "function" - } - } - compileOptions { - targetCompatibility = JavaVersion.VERSION_11 - sourceCompatibility = JavaVersion.VERSION_11 - isCoreLibraryDesugaringEnabled = true - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - freeCompilerArgs += listOf( - "-P", - "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" + - "${project.projectDir.absolutePath}/compose_compiler_config.conf" - ) - } - - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() - } - - packaging { - resources { - excludes += "META-INF/core.kotlin_module" - excludes += "META-INF/kotlinx-coroutines-core.kotlin_module" - } - } - - testOptions { - unitTests { - isReturnDefaultValues = true - } - } -} - -dependencies { - implementation(projects.androidExtensions) - implementation(projects.aprsAndroid) - - implementation(libs.coroutines.android) - - implementation(libs.androidx.appcompat) - implementation(libs.androidx.preference) - implementation(libs.androidx.activity.ktx) - implementation(libs.bundles.androidx.compose.full) - - implementation(libs.google.material.core) - - implementation(libs.bundles.dagger.libraries) - kapt(libs.bundles.dagger.kapt) - - implementation(libs.google.license.core) - - implementation(libs.kimchi.core) - implementation(libs.kimchi.firebase.analytics) - implementation(libs.kimchi.firebase.crashlytics) - - api(libs.spondee.units) - implementation(libs.ack.client) - - implementation(libs.bundles.watermelon) - - implementation(libs.firebase.config) - implementation(libs.firebase.analytics) - - implementation(projects.maps) - "functionalImplementation"(projects.mapbox) - - testImplementation(libs.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.kotlin.test.core) - coreLibraryDesugaring(libs.android.tools.desugar.jdk) -} diff --git a/android-application/compose_compiler_config.conf b/android-application/compose_compiler_config.conf deleted file mode 100644 index 8ffa7ea4..00000000 --- a/android-application/compose_compiler_config.conf +++ /dev/null @@ -1,2 +0,0 @@ -// Consider everything stable -**.* diff --git a/android-application/proguard-rules.pro b/android-application/proguard-rules.pro deleted file mode 100644 index b914daba..00000000 --- a/android-application/proguard-rules.pro +++ /dev/null @@ -1,27 +0,0 @@ -# Kotlin --keepattributes InnerClasses # Needed for `getDeclaredClasses`. --keep public class kotlinx.serialization.* { public *; } --dontwarn kotlinx.serialization.** --keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept. - static <1>$$serializer INSTANCE; -} - -# Okhttp --keep public class org.bouncycastle.jsse.** { *; } --dontwarn org.bouncycastle.jsse.** --keep public class org.conscrypt.** { *; } --dontwarn org.conscrypt.** --keep public class org.openjsse.** { *; } --dontwarn org.openjsse.** - -# Mapbox --keep class com.mapbox.android.telemetry.** --keep class com.mapbox.android.core.location.** --keep class android.arch.lifecycle.** { *; } --keep class com.mapbox.android.core.location.** { *; } --dontnote com.mapbox.mapboxsdk.** --dontnote com.mapbox.android.gestures.** --dontnote com.mapbox.mapboxsdk.plugins.** - --keep public class com.google.android.gms.* { public *; } --dontwarn com.google.android.gms.** diff --git a/android-application/src/functional/kotlin/com/inkapplications/ack/android/maps/MapsImplementation.kt b/android-application/src/functional/kotlin/com/inkapplications/ack/android/maps/MapsImplementation.kt deleted file mode 100644 index 6ff2d232..00000000 --- a/android-application/src/functional/kotlin/com/inkapplications/ack/android/maps/MapsImplementation.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.inkapplications.ack.android.maps - -import com.inkapplications.ack.android.mapbox.MapboxMapRenderer - -val MapsImplementation = MapboxMapRenderer diff --git a/android-application/src/main/AndroidManifest.xml b/android-application/src/main/AndroidManifest.xml deleted file mode 100644 index 5717fedb..00000000 --- a/android-application/src/main/AndroidManifest.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android-application/src/main/ic_launcher-playstore.png b/android-application/src/main/ic_launcher-playstore.png deleted file mode 100644 index b28ee7c3..00000000 Binary files a/android-application/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/android-application/src/main/java/com/inkapplications/ack/android/AnalyticsEvents.kt b/android-application/src/main/java/com/inkapplications/ack/android/AnalyticsEvents.kt deleted file mode 100644 index b404f142..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/AnalyticsEvents.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.inkapplications.ack.android - -import kimchi.analytics.KimchiAnalytics -import kimchi.analytics.stringProperty - -/** - * Track navigation events within the app. - */ -fun KimchiAnalytics.trackNavigation(destination: String) = trackEvent( - name = "navigate", - properties = listOf(stringProperty("destination", destination)) -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/AprsApplication.kt b/android-application/src/main/java/com/inkapplications/ack/android/AprsApplication.kt deleted file mode 100644 index ac4b31bd..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/AprsApplication.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.inkapplications.ack.android - -import android.app.Application -import com.inkapplications.ack.android.startup.InitJob -import dagger.hilt.android.HiltAndroidApp -import javax.inject.Inject - -@HiltAndroidApp -class AprsApplication: Application() { - @Inject - lateinit var initJob: InitJob - - override fun onCreate() { - super.onCreate() - - initJob.init(this) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ExternalModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/ExternalModule.kt deleted file mode 100644 index 05ece7de..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ExternalModule.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.inkapplications.ack.android - -import com.google.android.gms.common.GoogleApiAvailability -import dagger.Module -import dagger.Provides -import dagger.Reusable -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import kimchi.Kimchi -import kimchi.logger.KimchiLogger -import kotlinx.datetime.Clock -import kotlinx.datetime.TimeZone - -@Module -@InstallIn(SingletonComponent::class) -class ExternalModule { - @Provides - @Reusable - fun kimchi(): KimchiLogger = Kimchi - - @Provides - @Reusable - fun clock(): Clock = Clock.System - - @Provides - @Reusable - fun timezone(): TimeZone = TimeZone.currentSystemDefault() - - @Provides - @Reusable - fun googleApis(): GoogleApiAvailability = GoogleApiAvailability.getInstance() -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureActivity.kt deleted file mode 100644 index 264572ae..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureActivity.kt +++ /dev/null @@ -1,206 +0,0 @@ -package com.inkapplications.ack.android.capture - -import android.Manifest -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.provider.Settings -import androidx.activity.compose.setContent -import androidx.appcompat.app.AlertDialog -import androidx.compose.runtime.collectAsState -import androidx.core.view.WindowCompat -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.inkapplications.ack.android.capture.insights.InsightsController -import com.inkapplications.ack.android.capture.messages.conversation.startConversationActivity -import com.inkapplications.ack.android.capture.messages.create.CreateConversationActivity -import com.inkapplications.ack.android.capture.messages.index.MessagesScreenController -import com.inkapplications.ack.android.capture.service.BackgroundCaptureService -import com.inkapplications.ack.android.capture.service.BackgroundCaptureServiceAudio -import com.inkapplications.ack.android.connection.DriverSelection -import com.inkapplications.ack.android.log.LogItemViewState -import com.inkapplications.ack.android.log.details.startLogInspectActivity -import com.inkapplications.ack.android.log.index.LogIndexController -import com.inkapplications.ack.android.map.MapEvents -import com.inkapplications.ack.android.map.MapViewState -import com.inkapplications.ack.android.maps.CameraPositionDefaults -import com.inkapplications.ack.android.maps.MapViewModel -import com.inkapplications.ack.android.settings.SettingsActivity -import com.inkapplications.ack.android.station.startStationActivity -import com.inkapplications.ack.android.tnc.startConnectTncActivity -import com.inkapplications.ack.android.trackNavigation -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.structures.station.Callsign -import com.inkapplications.android.PermissionGate -import com.inkapplications.android.extensions.ExtendedActivity -import com.inkapplications.android.startActivity -import dagger.hilt.android.AndroidEntryPoint -import kimchi.Kimchi -import kimchi.analytics.stringProperty -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import javax.inject.Inject - -/** - * Capture controls and data exploration. - * - * This is the primary activity in the application and provides controls - * to capture APRS packets and explore the data that has been captured. - */ -@AndroidEntryPoint -class CaptureActivity: ExtendedActivity(), CaptureNavController, LogIndexController, InsightsController { - @Inject - lateinit var mapEvents: MapEvents - - @Inject - lateinit var captureEvents: CaptureEvents - - private val permissionGate = PermissionGate(this) - private val backgroundCaptureServiceIntent by lazy { Intent(this, BackgroundCaptureService::class.java) } - private val backgroundCaptureAudioServiceIntent by lazy { Intent(this, BackgroundCaptureServiceAudio::class.java) } - - override fun onCreate() { - super.onCreate() - - setContent { - val mapState = mapEvents.viewState.collectAsState(MapViewState( - mapViewModel = MapViewModel( - cameraPosition = CameraPositionDefaults.unknownLocation, - markers = emptyList(), - ) - )) - val messagesScreenController = object: MessagesScreenController { - override fun onCreateMessageClick() { - startActivity(CreateConversationActivity::class) - } - override fun onConversationClick(callsign: Callsign) { - startConversationActivity(callsign) - } - } - - CaptureScreen( - mapState = mapState, - logIndexController = this, - messagesScreenController = messagesScreenController, - insightsController = this, - controller = this, - ) - } - } - - override fun onLogMapItemClick(log: LogItemViewState) { - Kimchi.trackEvent("map_log_click") - startStationActivity(log.source) - } - - override fun onMapItemClick(captureId: CaptureId?) { - Kimchi.trackEvent("map_item_select") - mapEvents.selectedItemId.value = captureId - } - - override fun onLogListItemClick(item: LogItemViewState) { - Kimchi.trackEvent("log_item_click") - startLogInspectActivity(item.id) - } - - override fun onStationItemClicked(item: LogItemViewState) { - Kimchi.trackEvent("insights_item_click") - startLogInspectActivity(item.id) - } - - override fun onLocationEnableClick() { - Kimchi.trackEvent("location_track_enable") - foregroundScope.launch { - permissionGate.withPermissions(Manifest.permission.ACCESS_FINE_LOCATION) { - mapEvents.trackingEnabled.value = true - } - } - } - - override fun onLocationDisableClick() { - Kimchi.trackEvent("location_track_disable") - mapEvents.trackingEnabled.value = false - } - - override fun onSettingsClick() { - Kimchi.trackNavigation("settings") - startActivity(SettingsActivity::class) - } - - override fun onConnectClick() { - Kimchi.trackEvent("capture_connect") - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val hasBackgroundLocation = checkSelfPermission(Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED - - if (!hasBackgroundLocation) { - AlertDialog.Builder(this) - .setTitle("Background Location Permission Required") - .setMessage("To capture data in the background, this app needs access to your location even when the app is closed.\n\nPlease grant \"Allow all the time\" location permission in the application settings.") - .setPositiveButton("Open Settings") { _, _ -> - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", packageName, null) - } - startActivity(intent) - } - .setNegativeButton("Cancel", null) - .show() - return - } - } - - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - permissionGate.withPermissions(*captureEvents.getDriverConnectPermissions().toTypedArray()) { - lifecycleScope.launch { - when (captureEvents.driverSelection.first()) { - DriverSelection.Tnc -> { - startConnectTncActivity(backgroundCaptureServiceIntent) - } - DriverSelection.Audio -> { - startForegroundService(backgroundCaptureAudioServiceIntent) - } - else -> { - startForegroundService(backgroundCaptureServiceIntent) - } - } - } - } - } - } - } - - override fun onDisconnectClick() { - Kimchi.trackEvent("capture_disconnect") - lifecycleScope.launch { - captureEvents.disconnectDriver() - stopService(backgroundCaptureServiceIntent) - } - } - - override fun onEnableLocationTransmitClick() { - Kimchi.trackEvent("transmit_enable") - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - permissionGate.withPermissions(*captureEvents.getDriverTransmitPermissions().toTypedArray() + Manifest.permission.ACCESS_FINE_LOCATION) { - captureEvents.locationTransmitState.value = true - } - } - } - } - - override fun onDisableLocationTransmitClick() { - Kimchi.trackEvent("transmit_disable") - captureEvents.locationTransmitState.value = false - } - - override fun onDriverSelected(selection: DriverSelection) { - Kimchi.trackEvent("driver_select", listOf(stringProperty("selection", selection.name))) - lifecycleScope.launch { - stopService(backgroundCaptureServiceIntent) - captureEvents.changeDriver(selection) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureEvents.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureEvents.kt deleted file mode 100644 index fc1f4d24..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureEvents.kt +++ /dev/null @@ -1,166 +0,0 @@ -package com.inkapplications.ack.android.capture - -import com.inkapplications.ack.android.connection.ConnectionSettings -import com.inkapplications.ack.android.connection.DriverSelection -import com.inkapplications.ack.android.settings.* -import com.inkapplications.ack.android.transmit.TransmitSettings -import com.inkapplications.ack.data.drivers.DriverConnectionState -import com.inkapplications.ack.data.drivers.PacketDriver -import com.inkapplications.ack.data.drivers.PacketDrivers -import com.inkapplications.ack.structures.* -import com.inkapplications.android.extensions.location.LocationAccess -import com.inkapplications.coroutines.combinePair -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.isActive -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.coroutines.coroutineContext - -/** - * Provides data access to abstractions around capturing APRS packets. - */ -@Singleton -class CaptureEvents @Inject constructor( - private val drivers: PacketDrivers, - private val readSettings: SettingsReadAccess, - private val writeSettings: SettingsWriteAccess, - private val connectionSettings: ConnectionSettings, - private val transmitSettings: TransmitSettings, - private val locationAccess: LocationAccess, - private val logger: KimchiLogger, -) { - val driverSelection = readSettings.observeData(connectionSettings.driver) - - private val currentDriver = driverSelection - .map { - when (it) { - DriverSelection.Audio -> drivers.afskDriver - DriverSelection.Internet -> drivers.internetDriver - DriverSelection.Tnc -> drivers.tncDriver - } - } - - /** - * The current state of the selected APRS driver connection. - */ - val connectionState: Flow = currentDriver - .flatMapLatest { it.connectionState } - - /** - * Whether the option to repeatedly transmit the device location is enabled. - */ - val locationTransmitState = MutableStateFlow(false) - - /** - * The current audio input level, or null when not capturing. - */ - val audioInputVolume = drivers.afskDriver.volume.combine(connectionState) { volume, state -> - when (state) { - DriverConnectionState.Connected -> volume - else -> null - } - } - - /** - * Change the current APRS driver. - * - * Note: This will disconnect the current driver and location state - * if enabled, and will require a reconnection. - */ - suspend fun changeDriver(value: DriverSelection) { - currentDriver.first().disconnect() - locationTransmitState.value = false - writeSettings.setData(connectionSettings.driver, value) - } - - suspend fun getDriverConnectPermissions(): Set { - return currentDriver.first().receivePermissions - } - - suspend fun getDriverTransmitPermissions(): Set { - return currentDriver.first().transmitPermissions - } - - suspend fun connectDriver() { - currentDriver.first().connect() - } - - suspend fun disconnectDriver() { - currentDriver.first().disconnect() - } - - suspend fun locationTransmitLoop() { - locationTransmitState.combinePair(currentDriver) - .collectLatest { (transmit, driver) -> - if (transmit) transmitLoop(driver) - } - } - - /** - * Transmit an APRS packet to the specified driver at the configured interval. - */ - private suspend fun transmitLoop(driver: PacketDriver) { - logger.trace("Starting transmit loop") - readSettings.observeData(connectionSettings.address) - .filterNotNull() - .combine(readSettings.observeData(transmitSettings.digipath)) { callsign, path -> - TransmitPrototype( - path = path, - destination = transmitSettings.destination.defaultData, - callsign = callsign, - symbol = transmitSettings.symbol.defaultData, - comment = transmitSettings.comment.defaultValue, - minRate = transmitSettings.minRate.defaultData, - maxRate = transmitSettings.maxRate.defaultData, - distance = transmitSettings.distance.defaultData, - ) - } - .combine(readSettings.observeData(transmitSettings.symbol)) { prototype, symbol -> - prototype.copy(symbol = symbol) - } - .combine(readSettings.observeString(transmitSettings.comment)) { prototype, comment -> - prototype.copy(comment = comment) - } - .combine(readSettings.observeData(transmitSettings.destination)) { prototype, destination -> - prototype.copy(destination = destination) - } - .combine(readSettings.observeData(transmitSettings.minRate)) { prototype, rate -> - prototype.copy(minRate = rate) - } - .combine(readSettings.observeData(transmitSettings.maxRate)) { prototype, rate -> - prototype.copy(maxRate = rate) - } - .combine(readSettings.observeData(transmitSettings.distance)) { prototype, distance -> - prototype.copy(distance = distance) - } - .onEach { logger.debug("Location Transmit Prototype: $it") } - .flatMapLatest { prototype -> - locationAccess.observeLocationChanges(prototype.maxRate, prototype.distance) - .map { prototype to it } - .onCompletion { logger.warn("Location Transmit Stopped") } - } - .collectLatest { (prototype, update) -> - while (coroutineContext.isActive) { - val packet = AprsPacket( - route = PacketRoute( - source = prototype.callsign, - digipeaters = prototype.path, - destination = prototype.destination, - ), - data = PacketData.Position( - coordinates = update.location, - symbol = prototype.symbol, - altitude = update.altitude, - comment = prototype.comment, - ) - ) - val encodingConfig = EncodingConfig(compression = EncodingPreference.Disfavored) - - driver.transmitPacket(packet, encodingConfig) - delay(prototype.minRate) - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureNavController.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureNavController.kt deleted file mode 100644 index bfe2de44..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureNavController.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.inkapplications.ack.android.capture - -import com.inkapplications.ack.android.connection.DriverSelection -import com.inkapplications.ack.android.log.LogItemViewState -import com.inkapplications.ack.data.CaptureId - -/** - * Capture navigation and control panel options. - */ -interface CaptureNavController { - /** - * Invoked when the user clicks to enable location tracking on the map. - */ - fun onLocationEnableClick() - - /** - * Invoked when the user clicks to disable location tracking on the map. - */ - fun onLocationDisableClick() - - /** - * Invoked when the user clicks on the highlighted log item. - * - * Note, this is the popover overlay, not the pin click. - */ - fun onLogMapItemClick(log: LogItemViewState) - - /** - * Invoked when the user clicks on the settings button. - */ - fun onSettingsClick() - - /** - * Invoked when the user clicks on the button to enable the driver connection. - */ - fun onConnectClick() - - /** - * Invoked when the user clicks on the button to disable the driver connection. - */ - fun onDisconnectClick() - - /** - * Invoked when the user clicks on the location transmit enable button. - */ - fun onEnableLocationTransmitClick() - - /** - * Invoked when the user clicks on the location transmit disable button. - */ - fun onDisableLocationTransmitClick() - - /** - * Invoked when the user selects a new radio driver. - */ - fun onDriverSelected(selection: DriverSelection) - - /** - * Invoked when the user clicks an item on the map - */ - fun onMapItemClick(captureId: CaptureId?) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreen.kt deleted file mode 100644 index 5962bd38..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreen.kt +++ /dev/null @@ -1,421 +0,0 @@ -@file:OptIn(ExperimentalLayoutApi::class) - -package com.inkapplications.ack.android.capture - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.capture.insights.InsightsController -import com.inkapplications.ack.android.capture.insights.InsightsScreen -import com.inkapplications.ack.android.capture.messages.MessageIndexScreen -import com.inkapplications.ack.android.capture.messages.index.MessagesScreenController -import com.inkapplications.ack.android.connection.DriverSelection -import com.inkapplications.ack.android.connection.readableName -import com.inkapplications.ack.android.log.index.LogIndexController -import com.inkapplications.ack.android.log.index.LogIndexScreen -import com.inkapplications.ack.android.map.MapScreen -import com.inkapplications.ack.android.map.MapViewState -import com.inkapplications.ack.android.trackNavigation -import com.inkapplications.ack.android.ui.AckChip -import com.inkapplications.ack.android.ui.SelectionRow -import com.inkapplications.ack.android.ui.StateLabelledIconButton -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.android.ui.theme.AckTheme -import com.inkapplications.android.extensions.control.ControlState -import kimchi.Kimchi -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun CaptureScreen( - mapState: State, - logIndexController: LogIndexController, - messagesScreenController: MessagesScreenController, - controller: CaptureNavController, - insightsController: InsightsController, - viewModel: CaptureViewModel = hiltViewModel(), -) = AckScreen { - val settingsSheetState = rememberBottomSheetScaffoldState() - val navController = rememberNavController() - navController.enableOnBackPressed(!settingsSheetState.bottomSheetState.isExpanded) - val captureScreenState = viewModel.controlPanelState.collectAsState() - SettingsSheetWrapper( - settingsSheetState = settingsSheetState, - controlPanelState = captureScreenState, - captureController = controller, - ) { - Column { - Scaffold( - modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars), - bottomBar = { CaptureBottomBar(navController) }, - floatingActionButton = { CaptureSettingsFab(settingsSheetState) }, - isFloatingActionButtonDocked = true, - floatingActionButtonPosition = FabPosition.Center, - ) { - CaptureNavHost( - navController = navController, - mapState = mapState, - logIndexController = logIndexController, - messagesScreenController = messagesScreenController, - captureController = controller, - insightsController = insightsController, - ) - } - } - } -} - -@Composable -private fun CaptureBottomBar( - navController: NavHostController, -) { - BottomAppBar( - backgroundColor = AckTheme.colors.background, - contentColor = contentColorFor(AckTheme.colors.surface), - cutoutShape = RoundedCornerShape(50), - contentPadding = PaddingValues(0.dp), - modifier = Modifier.fillMaxWidth(), - ) { - val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route - BottomNavigation( - backgroundColor = AckTheme.colors.surface, - contentColor = contentColorFor(AckTheme.colors.surface), - modifier = Modifier.fillMaxWidth() - ) { - BottomNavigationItem( - icon = { Icon(Icons.Default.Map, contentDescription = null) }, - label = { Text(stringResource(R.string.menu_capture_map), softWrap = false) }, - selected = currentRoute == "map", - onClick = { - Kimchi.info("Navigate to map") - Kimchi.trackNavigation("map") - if (currentRoute != "map") { - navController.navigate("map") - } - } - ) - BottomNavigationItem( - icon = { Icon(Icons.Default.List, contentDescription = null) }, - label = { Text(stringResource(R.string.menu_capture_log), softWrap = false) }, - selected = currentRoute == "log", - onClick = { - Kimchi.info("Navigate to log") - Kimchi.trackNavigation("log") - if (currentRoute != "log") { - navController.navigate("log") - } - } - ) - Spacer(Modifier.weight(1f, true)) - BottomNavigationItem( - icon = { Icon(Icons.Default.Mail, contentDescription = null) }, - label = { Text("Messages", softWrap = false) }, - selected = currentRoute == "messages", - onClick = { - Kimchi.info("Navigate to messages") - Kimchi.trackNavigation("messages") - if (currentRoute != "messages") { - navController.navigate("messages") - } - } - ) - BottomNavigationItem( - icon = { Icon(Icons.Default.Lightbulb, contentDescription = null) }, - label = { Text("Insights", softWrap = false) }, - selected = currentRoute == "insights", - onClick = { - Kimchi.info("Navigate to insights") - Kimchi.trackNavigation("insights") - if (currentRoute != "insights") { - navController.navigate("insights") - } - } - ) - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun CaptureSettingsFab( - settingsSheetState: BottomSheetScaffoldState, -) { - val scope = rememberCoroutineScope() - FloatingActionButton( - onClick = { - scope.launch { - if (settingsSheetState.bottomSheetState.isExpanded) { - settingsSheetState.bottomSheetState.collapse() - } else { - settingsSheetState.bottomSheetState.expand() - } - } - }, - shape = RoundedCornerShape(50), - backgroundColor = AckTheme.colors.accent, - contentColor = AckTheme.colors.onAccent - ) { - Icon(Icons.Default.SettingsInputAntenna, stringResource(R.string.capture_controls_expand_action)) - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun SettingsSheetWrapper( - settingsSheetState: BottomSheetScaffoldState, - controlPanelState: State, - captureController: CaptureNavController, - content: @Composable () -> Unit, -) { - BottomSheetScaffold( - sheetContent = { CaptureSettingsSheet(controlPanelState.value, captureController, settingsSheetState) }, - scaffoldState = settingsSheetState, - sheetPeekHeight = 0.dp, - ) { - content() - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun CaptureSettingsSheet( - controlPanelState: ControlPanelState, - captureController: CaptureNavController, - settingsSheetState: BottomSheetScaffoldState, -) = Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.windowInsetsPadding(WindowInsets.navigationBars), -) { - val scope = rememberCoroutineScope() - BackHandler(enabled = settingsSheetState.bottomSheetState.isExpanded) { - scope.launch { settingsSheetState.bottomSheetState.collapse() } - } - Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth(), - ) { - IconButton( - onClick = { scope.launch { settingsSheetState.bottomSheetState.collapse() } }, - ) { - Icon(Icons.Default.DragHandle, stringResource(R.string.capture_controls_collapse_action)) - } - } - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = AckTheme.spacing.gutter), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - if (controlPanelState is ControlPanelState.Loaded) { - Icon( - imageVector = Icons.Default.Badge, - contentDescription = null, - tint = AckTheme.colors.foreground, - modifier = Modifier.padding(horizontal = AckTheme.spacing.icon) - ) - Text( - text = controlPanelState.userCallsign, - style = AckTheme.typography.h1, - ) - } - } - Row { - IconButton( - onClick = { - scope.launch { - settingsSheetState.bottomSheetState.collapse() - } - captureController.onSettingsClick() - } - ) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = stringResource(R.string.capture_controls_settings_name), - tint = AckTheme.colors.foreground, - ) - } - } - } - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxWidth(), - ) { - var selector by remember { mutableStateOf(false) } - - if (selector) { - Dialog( - onDismissRequest = { selector = false }, - ) { - Card( - modifier = Modifier.fillMaxWidth(), - ) { - Column { - Text( - text = "Connection", - style = AckTheme.typography.h1, - modifier = Modifier.padding( - top = AckTheme.spacing.gutter, - start = AckTheme.spacing.gutter, - end = AckTheme.spacing.gutter, - bottom = AckTheme.spacing.content, - ) - ) - DriverSelection.values().forEach { driver -> - SelectionRow( - name = stringResource(id = driver.readableName), - selected = driver == (controlPanelState as? ControlPanelState.Loaded)?.connectionType, - onClick = { - captureController.onDriverSelected(driver) - selector = false - }, - ) - } - } - } - } - } - - AckChip( - onClick = { selector = true }, - ) { - Icon( - imageVector = Icons.Default.Tune, - contentDescription = null, - tint = AckTheme.colors.foreground, - modifier = Modifier.padding(end = AckTheme.spacing.icon), - ) - Text( - text = if (controlPanelState is ControlPanelState.Loaded) { - controlPanelState.connection - } else { - stringResource(R.string.capture_controls_connection_type_unknown) - } - ) - } - } - val volume = (controlPanelState as? ControlPanelState.Loaded)?.volumeLevel - if (volume != null) { - LinearProgressIndicator( - progress = volume, - modifier = Modifier - .fillMaxWidth() - .padding(top = AckTheme.spacing.item), - ) - } - FlowRow( - horizontalArrangement = Arrangement.Center, - ) { - Row { - StateLabelledIconButton( - icon = when (controlPanelState.connectState) { - ControlState.On -> Icons.Default.WifiTethering - ControlState.Off, - ControlState.Disabled, - ControlState.Hidden -> Icons.Default.WifiTetheringOff - }, - title = when (controlPanelState.connectState) { - ControlState.On -> stringResource(R.string.capture_controls_connection_on) - ControlState.Off, - ControlState.Disabled, - ControlState.Hidden, -> stringResource(R.string.capture_controls_connection_off) - }, - state = controlPanelState.connectState, - onClick = { - when (it) { - ControlState.On -> captureController.onDisconnectClick() - ControlState.Off -> captureController.onConnectClick() - ControlState.Disabled, - ControlState.Hidden -> {} - } - }, - ) - StateLabelledIconButton( - icon = when (controlPanelState.positionTransmitState) { - ControlState.On -> Icons.Default.ModeOfTravel - ControlState.Off, - ControlState.Disabled, - ControlState.Hidden -> Icons.Default.LocationOff - }, - title = when (controlPanelState.positionTransmitState) { - ControlState.On -> stringResource(R.string.capture_controls_position_on) - ControlState.Off, - ControlState.Disabled, - ControlState.Hidden, -> stringResource(R.string.capture_controls_position_off) - }, - state = controlPanelState.positionTransmitState, - onClick = { - when (it) { - ControlState.On -> captureController.onDisableLocationTransmitClick() - ControlState.Off -> captureController.onEnableLocationTransmitClick() - ControlState.Disabled, - ControlState.Hidden -> {} - } - }, - ) - } - } -} - -@Composable -private fun CaptureNavHost( - navController: NavHostController, - mapState: State, - logIndexController: LogIndexController, - messagesScreenController: MessagesScreenController, - captureController: CaptureNavController, - insightsController: InsightsController, -) { - NavHost( - navController = navController, - startDestination = "map", - modifier = Modifier.fillMaxSize(), - ) { - composable("map") { - MapScreen( - state = mapState.value, - onMapItemClick = captureController::onMapItemClick, - onLogItemClick = captureController::onLogMapItemClick, - onEnableLocation = captureController::onLocationEnableClick, - onDisableLocation = captureController::onLocationDisableClick, - bottomContentProtection = AckTheme.spacing.bottomBarHeight, - ) - } - composable("log") { - LogIndexScreen( - controller = logIndexController, - ) - } - composable("messages") { - MessageIndexScreen( - controller = messagesScreenController, - bottomProtection = AckTheme.spacing.bottomBarHeight, - bottomContentProtection = AckTheme.spacing.navigationProtection, - ) - } - composable("insights") { - InsightsScreen( - controller = insightsController - ) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreenStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreenStateFactory.kt deleted file mode 100644 index b400332d..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureScreenStateFactory.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.inkapplications.ack.android.capture - -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.connection.DriverSelection -import com.inkapplications.ack.android.connection.readableName -import com.inkapplications.ack.android.settings.LicenseData -import com.inkapplications.ack.data.drivers.DriverConnectionState -import com.inkapplications.android.extensions.StringResources -import com.inkapplications.android.extensions.control.ControlState -import dagger.Reusable -import inkapplications.spondee.scalar.Percentage -import inkapplications.spondee.structure.toFloat -import javax.inject.Inject - -/** - * Create view state models from current state data. - */ -@Reusable -class CaptureScreenStateFactory @Inject constructor( - private val stringResources: StringResources, -) { - /** - * Create a new model from the capture data provided. - */ - fun controlPanelState( - currentDriver: DriverSelection, - driverConnectionState: DriverConnectionState, - positionTransmit: Boolean, - license: LicenseData, - inputAudioLevel: Percentage?, - ): ControlPanelState { - return ControlPanelState.Loaded( - userCallsign = license.address?.toString() ?: stringResources.getString(R.string.capture_callsign_missing), - volumeLevel = inputAudioLevel?.toDecimal()?.toFloat(), - connection = stringResources.getString(currentDriver.readableName), - connectState = when { - driverConnectionState == DriverConnectionState.Connected -> ControlState.On - currentDriver == DriverSelection.Internet && license.address == null -> ControlState.Disabled - else -> ControlState.Off - }, - connectionType = currentDriver, - positionTransmitState = when { - positionTransmit -> ControlState.On - driverConnectionState != DriverConnectionState.Connected -> ControlState.Disabled - currentDriver == DriverSelection.Internet && license.passcode == null -> ControlState.Disabled - license.address == null -> ControlState.Disabled - else -> ControlState.Off - }, - ) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureViewModel.kt deleted file mode 100644 index 442cdf95..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/CaptureViewModel.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.inkapplications.ack.android.capture - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.inkapplications.ack.android.connection.ConnectionSettings -import com.inkapplications.ack.android.settings.SettingsAccess -import com.inkapplications.ack.android.settings.SettingsReadAccess -import com.inkapplications.ack.android.settings.observeData -import com.inkapplications.ack.data.drivers.PacketDrivers -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -/** - * Android ViewModel to create and store the state of the capture screen. - */ -@HiltViewModel -class CaptureViewModel @Inject constructor( - captureEvents: CaptureEvents, - settings: SettingsReadAccess, - connectionSettings: ConnectionSettings, - settingsAccess: SettingsAccess, - drivers: PacketDrivers, - captureScreenStateFactory: CaptureScreenStateFactory, -): ViewModel() { - val controlPanelState: StateFlow = combine( - settings.observeData(connectionSettings.driver), - settingsAccess.licenseData, - captureEvents.audioInputVolume, - captureEvents.connectionState, - captureEvents.locationTransmitState, - ) { driver, license, audioInputVolume, connectedState, positionTransmitState -> - captureScreenStateFactory.controlPanelState( - currentDriver = driver, - driverConnectionState = connectedState, - positionTransmit = positionTransmitState, - license = license, - inputAudioLevel = audioInputVolume, - ) - }.stateIn(viewModelScope, SharingStarted.Eagerly, ControlPanelState.Initial) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/ControlPanelState.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/ControlPanelState.kt deleted file mode 100644 index 45af515a..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/ControlPanelState.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.inkapplications.ack.android.capture - -import com.inkapplications.ack.android.connection.DriverSelection -import com.inkapplications.android.extensions.control.ControlState - -/** - * Represents the states for the capture screen's control panel. - */ -sealed interface ControlPanelState { - /** - * State of the connection toggle button. - */ - val connectState: ControlState - - /** - * State of the position toggle button. - */ - val positionTransmitState: ControlState - - /** - * Object used before any data has been loaded successfully. - */ - object Initial: ControlPanelState { - override val connectState: ControlState = ControlState.Disabled - override val positionTransmitState: ControlState = ControlState.Disabled - } - - /** - * Data for how to display the control panel based on application state. - */ - data class Loaded( - /** - * The user's current callsign, as a readable string. - */ - val userCallsign: String, - - /** - * Level of the input volume, if applicable to the connection. - */ - val volumeLevel: Float?, - - /** - * Name of the current connection type. - */ - val connection: String, - - /** - * Data type for the current connection - */ - val connectionType: DriverSelection, - - /** - * State of the connection toggle button. - */ - override val connectState: ControlState, - - /** - * State of the position toggle button. - */ - override val positionTransmitState: ControlState, - ): ControlPanelState -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/TransmitPrototype.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/TransmitPrototype.kt deleted file mode 100644 index 85d8c913..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/TransmitPrototype.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.inkapplications.ack.android.capture - -import com.inkapplications.ack.structures.Digipeater -import com.inkapplications.ack.structures.Symbol -import com.inkapplications.ack.structures.station.StationAddress -import inkapplications.spondee.measure.Length -import kotlin.time.Duration - -/** - * Data used when creating a transmitted packet. - */ -data class TransmitPrototype( - val path: List, - val destination: StationAddress, - val callsign: StationAddress, - val symbol: Symbol, - val comment: String, - val minRate: Duration, - val maxRate: Duration, - val distance: Length, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsController.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsController.kt deleted file mode 100644 index 8dd11744..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsController.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.inkapplications.ack.android.capture.insights - -import com.inkapplications.ack.android.log.LogItemViewState - -/** - * User actions available on the insights screen. - */ -interface InsightsController { - /** - * Invoked when the user clicks on a station in the nearby frequency list. - */ - fun onStationItemClicked(item: LogItemViewState) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsScreen.kt deleted file mode 100644 index 7ff950ba..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsScreen.kt +++ /dev/null @@ -1,198 +0,0 @@ -package com.inkapplications.ack.android.capture.insights - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Thermostat -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.log.AprsLogItem -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun InsightsScreen( - viewModel: InsightsViewModel = hiltViewModel(), - controller: InsightsController, -) { - AckScreen { - val weatherState = viewModel.weatherState.collectAsState() - val stationsState = viewModel.stations.collectAsState() - val statsState = viewModel.statsState.collectAsState() - - Column( - modifier = Modifier.verticalScroll(rememberScrollState()).padding( - top = AckTheme.spacing.gutter, - start = AckTheme.spacing.gutter, - end = AckTheme.spacing.gutter, - bottom = AckTheme.spacing.navigationProtection + AckTheme.spacing.gutter, - ) - ) { - Text(stringResource(R.string.insights_title), style = AckTheme.typography.h1) - Weather(weatherState.value) - NearbyStations(stationsState.value, controller) - Stats(statsState.value) - } - } -} - -@Composable -private fun NearbyStations( - state: NearbyStationsState, - controller: InsightsController, -) { - Column { - Text(stringResource(R.string.insights_stations_title), style = AckTheme.typography.h2, modifier = Modifier.padding(vertical = AckTheme.spacing.content)) - when (state) { - NearbyStationsState.Initial -> {} - NearbyStationsState.Empty -> { - Text(stringResource(id = R.string.insights_stations_empty_caption), style = AckTheme.typography.caption) - } - is NearbyStationsState.StationList -> { - state.stations.forEach { station -> - AprsLogItem(station, onClick = controller::onStationItemClicked) - } - } - } - } -} - -@Composable -private fun Stats( - state: InsightsStatsState, -) { - Column { - Text(stringResource(R.string.insights_packets_title), style = AckTheme.typography.h2, modifier = Modifier.padding(vertical = AckTheme.spacing.content)) - when (state) { - InsightsStatsState.Initial -> {} - InsightsStatsState.None -> { - Text(stringResource(id = R.string.insights_stats_placeholder), style = AckTheme.typography.body) - } - is InsightsStatsState.LoadedData -> { - Text(stringResource(R.string.insights_packets_count, state.packets), style = AckTheme.typography.body) - Text(stringResource(R.string.insights_stations_count, state.stations), style = AckTheme.typography.body) - } - } - } -} - -@Composable -private fun Weather( - state: InsightsWeatherState, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = AckTheme.spacing.content), - ) { - when (state) { - InsightsWeatherState.Initial, - InsightsWeatherState.Unknown -> WeatherPlaceholder() - is InsightsWeatherState.DisplayRecent -> WeatherData(state) - } - } -} - -@Composable -private fun WeatherData( - data: InsightsWeatherState.DisplayRecent, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = AckTheme.spacing.content), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - WeatherIcon(iconState = data.icon) - Text(data.temperature, style = AckTheme.typography.display) - } - Text( - text = stringResource( - R.string.insights_weather_report_info_template, - data.weatherReporter, - data.weatherReportTime - ), - style = AckTheme.typography.caption - ) - } -} - -@Composable -private fun WeatherPlaceholder() { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = AckTheme.spacing.content), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Default.Thermostat, - contentDescription = stringResource(R.string.insights_weather_icon_normal), - modifier = Modifier.size(AckTheme.sizing.dispayIcon), - tint = AckTheme.colors.foreground, - ) - Text("--", style = AckTheme.typography.display) - } - Text( - text = stringResource(R.string.insights_weather_placeholder_caption), - style = AckTheme.typography.caption - ) - } -} - -@Composable -private fun WeatherIcon( - iconState: InsightsWeatherState.WeatherIcon, -) { - val iconModifier = Modifier.size(AckTheme.sizing.displayDecorationIcon) - val iconTint = AckTheme.colors.foreground - - when (iconState) { - InsightsWeatherState.WeatherIcon.Normal -> Icon( - imageVector = Icons.Default.Thermostat, - contentDescription = stringResource(R.string.insights_weather_icon_normal), - modifier = iconModifier, - tint = iconTint, - ) - InsightsWeatherState.WeatherIcon.Rain -> Icon( - painter = painterResource(id = R.drawable.ic_rain), - contentDescription = stringResource(R.string.insights_weather_icon_rain), - modifier = iconModifier, - tint = iconTint, - ) - InsightsWeatherState.WeatherIcon.Snow -> Icon( - painter = painterResource(id = R.drawable.ic_snow), - contentDescription = stringResource(R.string.insights_weather_icon_snow), - modifier = iconModifier, - tint = iconTint, - ) - InsightsWeatherState.WeatherIcon.Humid -> Icon( - painter = painterResource(id = R.drawable.ic_humid), - contentDescription = stringResource(R.string.insights_weather_icon_humid), - modifier = iconModifier, - tint = iconTint, - ) - InsightsWeatherState.WeatherIcon.Windy -> Icon( - painter = painterResource(id = R.drawable.ic_windy), - contentDescription = stringResource(R.string.insights_weather_icon_wind), - modifier = iconModifier, - tint = iconTint, - ) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsStatsState.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsStatsState.kt deleted file mode 100644 index deeab967..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsStatsState.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.inkapplications.ack.android.capture.insights - -/** - * States for the "stats" section of the insight screen. - */ -sealed interface InsightsStatsState { - /** - * Indicates that no data has been loaded yet. - */ - object Initial: InsightsStatsState - - /** - * Indicates that data has been loaded, but the user has no packets - * to generate stats from. - */ - object None: InsightsStatsState - - /** - * Loaded Stats about captured packets - */ - data class LoadedData( - /** - * Total number of captured packets. - */ - val packets: Long, - - /** - * Distinct stations that have been seen in the captured packets. - */ - val stations: Long, - ): InsightsStatsState -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsViewModel.kt deleted file mode 100644 index b0e7d167..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsViewModel.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.inkapplications.ack.android.capture.insights - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.inkapplications.ack.android.locale.LocaleSettings -import com.inkapplications.ack.android.log.LogItemViewStateFactory -import com.inkapplications.ack.android.settings.SettingsReadAccess -import com.inkapplications.ack.android.settings.observeBoolean -import com.inkapplications.ack.data.PacketStorage -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.coroutines.filterItems -import dagger.hilt.android.lifecycle.HiltViewModel -import kimchi.logger.EmptyLogger -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.flow.* -import kotlinx.datetime.Clock -import javax.inject.Inject -import kotlin.time.Duration.Companion.hours - -/** - * Android Viewmodel to load various view state information for the Insights page. - */ -@HiltViewModel -class InsightsViewModel @Inject constructor( - packetStorage: PacketStorage, - settings: SettingsReadAccess, - localeSettings: LocaleSettings, - private val weatherStateFactory: WeatherStateFactory, - private val statsStateFactory: StatsStateFactory, - private val logItemViewStateFactory: LogItemViewStateFactory, - private val clock: Clock = Clock.System, - private val logger: KimchiLogger = EmptyLogger, -): ViewModel() { - /** - * State of the weather insights section. - */ - val weatherState: StateFlow = packetStorage.findMostRecentByType(PacketData.Weather::class) - .map { if (it?.received?.let { it > clock.now() - 2.hours } == true) it else null } - .combine(settings.observeBoolean(localeSettings.preferMetric)) { weatherPacket, metric -> - weatherStateFactory.createState(weatherPacket, metric) - } - .stateIn(viewModelScope, SharingStarted.Lazily, InsightsWeatherState.Initial) - - /** - * State of the packet stats section. - */ - val statsState: StateFlow = packetStorage.count() - .onEach { logger.debug("Building stats for $it packets") } - .combine(packetStorage.countStations()) { packetCount, stationCount -> - statsStateFactory.createState( - packetCount = packetCount, - stationCount = stationCount, - ) - } - .stateIn(viewModelScope, SharingStarted.Lazily, InsightsStatsState.Initial) - - /** - * State of nearby stations broadcasting contact frequencies. - */ - val stations: StateFlow = packetStorage.findByStationComments(limit = 10) - .filterItems { it.received > clock.now() - 1.hours } - .combine(settings.observeBoolean(localeSettings.preferMetric)) { stations, metric -> - logItemViewStateFactory.create(stations, metric) - } - .map { - if (it.isEmpty()) NearbyStationsState.Empty - else NearbyStationsState.StationList(it) - } - .stateIn(viewModelScope, SharingStarted.Lazily, NearbyStationsState.Initial) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsWeatherState.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsWeatherState.kt deleted file mode 100644 index 641d8af9..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/InsightsWeatherState.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.inkapplications.ack.android.capture.insights - -/** - * States for the weather section of the insight screen. - */ -sealed interface InsightsWeatherState { - /** - * Indicates that no data has been loaded yet. - */ - object Initial: InsightsWeatherState - - /** - * Indicates that no relevant weather packets have been captured yet. - */ - object Unknown: InsightsWeatherState - - /** - * Recent Weather Information - */ - data class DisplayRecent( - /** - * Current Temperature, as a formatted string. - */ - val temperature: String, - - /** - * The reporting station callsign of the weather data. - */ - val weatherReporter: String, - - /** - * The time that the weather data was reported, as a preformatted - * string in the local time. - */ - val weatherReportTime: String, - - /** - * An indication of what iconography to display with the weather. - */ - val icon: WeatherIcon - ): InsightsWeatherState - - /** - * Iconography options that hint at the current weather conditions. - */ - enum class WeatherIcon { - Normal, - Rain, - Snow, - Humid, - Windy, - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/NearbyStationsState.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/NearbyStationsState.kt deleted file mode 100644 index 346e5351..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/NearbyStationsState.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.inkapplications.ack.android.capture.insights - -import com.inkapplications.ack.android.log.LogItemViewState - -sealed interface NearbyStationsState { - object Initial: NearbyStationsState - object Empty: NearbyStationsState - data class StationList( - val stations: List, - ): NearbyStationsState -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/StatsStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/StatsStateFactory.kt deleted file mode 100644 index b040c812..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/StatsStateFactory.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.inkapplications.ack.android.capture.insights - -import dagger.Reusable -import javax.inject.Inject - -/** - * Creates stats view state models based on current data. - */ -@Reusable -class StatsStateFactory @Inject constructor() { - /** - * Generate a view state for the given statistics. - * - * @param packetCount The total number of packets that have been collected. - * @param stationCount The number of distinct stations that have reported packets. - */ - fun createState(packetCount: Long, stationCount: Long): InsightsStatsState { - return when (packetCount) { - 0L -> InsightsStatsState.None - else -> InsightsStatsState.LoadedData( - packets = packetCount, - stations = stationCount, - ) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/WeatherStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/WeatherStateFactory.kt deleted file mode 100644 index 2c9d9399..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/insights/WeatherStateFactory.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.inkapplications.ack.android.capture.insights - -import com.inkapplications.ack.android.locale.format -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.android.extensions.format.DateTimeFormatter -import dagger.Reusable -import inkapplications.spondee.measure.us.toFahrenheit -import inkapplications.spondee.measure.us.toInches -import inkapplications.spondee.measure.us.toMilesPerHourValue -import inkapplications.spondee.structure.toFloat -import inkapplications.spondee.structure.toInt -import javax.inject.Inject - -/** - * Generate weather view state models based on the most relevant weather packet. - */ -@Reusable -class WeatherStateFactory @Inject constructor( - private val dateTimeFormatter: DateTimeFormatter, -) { - /** - * Generate a view state for weather conditions. - * - * @param weatherPacket The most relevant weather packet seen by the user - * @param metric Whether to generate the view data in metric dimensions - */ - fun createState(weatherPacket: CapturedPacket?, metric: Boolean): InsightsWeatherState { - return when (val weatherData = weatherPacket?.parsed?.data as? PacketData.Weather) { - null -> InsightsWeatherState.Unknown - else -> InsightsWeatherState.DisplayRecent( - temperature = weatherData.temperature?.format(metric) ?: "--", - weatherReporter = weatherPacket.parsed.route.source.toString(), - weatherReportTime = weatherPacket.received.let { dateTimeFormatter.formatTimestamp(it) }, - icon = when { - weatherData.precipitation.snowLast24Hours?.toInches()?.toFloat() ?: 0f > 0 -> InsightsWeatherState.WeatherIcon.Snow - weatherData.precipitation.rainLastHour?.toInches()?.toFloat() ?: 0f > 0 -> InsightsWeatherState.WeatherIcon.Rain - weatherData.windData.speed?.toMilesPerHourValue()?.toInt() ?: 0 > 20 -> InsightsWeatherState.WeatherIcon.Windy - weatherData.windData.gust?.toMilesPerHourValue()?.toInt() ?: 0 > 20 -> InsightsWeatherState.WeatherIcon.Windy - weatherData.humidity?.toDecimal()?.toFloat() ?: 0f > .75f - && weatherData.temperature?.toFahrenheit()?.toInt() ?: 0 > 65 -> InsightsWeatherState.WeatherIcon.Humid - else -> InsightsWeatherState.WeatherIcon.Normal - } - ) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/MessageData.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/MessageData.kt deleted file mode 100644 index ef9f6a7d..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/MessageData.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.inkapplications.ack.android.capture.messages - -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.structures.station.Callsign - -/** - * Contextual information used for displaying messages. - */ -data class MessageData( - /** - * Callsign of the user of the app. Used to distinguished "my" messages. - */ - val selfCallsign: Callsign, - - /** - * Packet data for the message. - */ - val message: CapturedPacket, -) { - /** - * Whether the sender matches the users callsign. - */ - val isOutgoing = message.parsed.route.source.callsign == selfCallsign -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/MessageEvents.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/MessageEvents.kt deleted file mode 100644 index 36879619..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/MessageEvents.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.inkapplications.ack.android.capture.messages - -import com.inkapplications.ack.android.connection.ConnectionSettings -import com.inkapplications.ack.android.connection.DriverSelection -import com.inkapplications.ack.android.settings.SettingsReadAccess -import com.inkapplications.ack.android.settings.observeData -import com.inkapplications.ack.android.settings.observeString -import com.inkapplications.ack.android.transmit.TransmitSettings -import com.inkapplications.ack.codec.AprsCodec -import com.inkapplications.ack.data.PacketOrigin -import com.inkapplications.ack.data.PacketStorage -import com.inkapplications.ack.data.drivers.PacketDrivers -import com.inkapplications.ack.structures.* -import com.inkapplications.ack.structures.station.Callsign -import com.inkapplications.ack.structures.station.StationAddress -import com.inkapplications.ack.structures.station.toStationAddress -import com.inkapplications.coroutines.filterItems -import com.inkapplications.coroutines.mapItems -import dagger.Reusable -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -/** - * Aggregates events and settings for packet data relating to messages. - */ -@Reusable -class MessageEvents @Inject constructor( - private val packetStorage: PacketStorage, - private val settings: SettingsReadAccess, - private val connectionSettings: ConnectionSettings, - private val transmitSettings: TransmitSettings, - private val codec: AprsCodec, - private val drivers: PacketDrivers, - private val logger: KimchiLogger, -) { - /** - * Observe a list of the latest message in each distinct conversation - * by station callsign. - */ - val latestMessageByConversation = settings.observeData(connectionSettings.address) - .onEach { logger.debug("Observing conversations for addressee: $it") } - .flatMapLatest { address -> - address?.callsign?.let(packetStorage::findLatestByConversation) - ?.mapItems { MessageData(address.callsign, it) } - ?: flowOf(emptyList()) - } - .onEach { logger.debug("Found ${it.size} conversations") } - - private val currentDriver = settings.observeData(connectionSettings.driver) - .map { - when (it) { - DriverSelection.Audio -> drivers.afskDriver - DriverSelection.Internet -> drivers.internetDriver - DriverSelection.Tnc -> drivers.tncDriver - } - } - - /** - * Observe the total list of messages sent to and from a particular station. - * - * @param address The callsign of the station to find messages for. - */ - fun conversationList(address: Callsign): Flow> { - return settings.observeData(connectionSettings.address) - .flatMapLatest { self -> - (self?.callsign?.let { packetStorage.findConversation(address, it) }?.mapItems { MessageData(self.callsign, it) } ?: flowOf(emptyList())) - } - .filterItems { it.message.parsed.data is PacketData.Message } - .onEach { logger.debug("Loaded ${it.size} messages from $address") } - } - - /** - * Transmit a new message via all connected transmission drivers. - * - * This will immediately transmit the message to any connected drivers - * as well as save the message in the local database as a locally - * transmitted packet. - * - * @param addressee The callsign of the station to send the message to. - * @param message The body text of the message to be sent. - */ - suspend fun transmitMessage(addressee: Callsign, message: String) { - val packetData = PacketData.Message( - addressee = StationAddress(addressee), - message = message, - ) - - val address = settings.observeData(connectionSettings.address).first() ?: run { - logger.error("Cannot transmit without a callsign set!") - return - } - val path = settings.observeString(transmitSettings.digipath).first() - val destination = settings.observeString(transmitSettings.destination).first() - - val packet = AprsPacket( - route = PacketRoute( - source = address, - digipeaters = path.split(',').map { Digipeater(it.toStationAddress()) }, - destination = destination.toStationAddress(), - ), - data = packetData, - ) - val encodingConfig = EncodingConfig(compression = EncodingPreference.Disfavored) - - currentDriver.first().transmitPacket(packet, encodingConfig) - val encoded = codec.toString(packet) - packetStorage.save(encoded.toByteArray(), packet, PacketOrigin.Local) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationActivity.kt deleted file mode 100644 index 2364e7b5..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationActivity.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.conversation - -import android.app.Activity -import androidx.activity.compose.setContent -import com.inkapplications.ack.android.capture.messages.MessageEvents -import com.inkapplications.ack.android.trackNavigation -import com.inkapplications.ack.structures.station.Callsign -import com.inkapplications.android.extensions.ExtendedActivity -import com.inkapplications.android.startActivity -import dagger.hilt.android.AndroidEntryPoint -import kimchi.Kimchi -import kotlinx.coroutines.launch -import javax.inject.Inject - -/** - * Intent Extra used for the addressee of the conversation. - */ -const val EXTRA_ADDRESS = "aprs.conversation.extra.address" - -/** - * Activity for viewing a single conversation's messages with another station. - */ -@AndroidEntryPoint -class ConversationActivity: ExtendedActivity(), ConversationController { - @Inject - lateinit var messageEvents: MessageEvents - - private val callsign get() = intent.getStringExtra(EXTRA_ADDRESS)!!.let(::Callsign) - - override fun onCreate() { - super.onCreate() - Kimchi.trackScreen("conversation") - setContent { - ConversationScreen(this) - } - } - - override fun onNavigateUpPressed() { - finish() - } - - override fun onSendMessage(message: String) { - Kimchi.debug("Sending Message: $message") - Kimchi.trackEvent("messages_send") - foregroundScope.launch { - messageEvents.transmitMessage(callsign, message) - Kimchi.trace("Message transmit action complete") - } - } -} - -/** - * Start a conversation activity for a particular station. - * - * @param callsign The callsign of the station to load messages for. - */ -fun Activity.startConversationActivity(callsign: Callsign) { - Kimchi.trackNavigation("conversation") - startActivity(ConversationActivity::class) { - putExtra(EXTRA_ADDRESS, callsign.canonical) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationController.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationController.kt deleted file mode 100644 index 4cdc6772..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationController.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.conversation - -/** - * Actions that can be taken from the conversation screen. - */ -interface ConversationController { - /** - * Invoked when the user presses the back/up button on the navigation bar. - */ - fun onNavigateUpPressed() - - /** - * Invoked when the user presses the send button in the message box. - * - * @param message The text the user has entered in the message box. - */ - fun onSendMessage(message: String) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationScreen.kt deleted file mode 100644 index 603ff2ea..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationScreen.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.conversation - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Message -import androidx.compose.material.icons.filled.Send -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.ui.EmptyBox -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun ConversationScreen( - controller: ConversationController, - viewModel: ConversationViewModel = hiltViewModel(), -) = AckScreen { - Column { - val viewState = viewModel.conversationState.collectAsState().value - TopAppBar( - elevation = 1.dp, - ) { - IconButton( - onClick = controller::onNavigateUpPressed, - ) { - Icon(Icons.Default.ArrowBack, stringResource(R.string.navigate_up)) - } - Text(viewState.title, style = AckTheme.typography.h1) - } - when (viewState) { - is ConversationViewState.Initial -> {} - is ConversationViewState.Empty -> EmptyBox( - icon = Icons.Default.Message, - caption = stringResource(R.string.messages_conversation_empty), - modifier = Modifier.weight(1f), - ) - is ConversationViewState.MessageList -> LazyColumn( - contentPadding = PaddingValues(vertical = AckTheme.spacing.gutter), - reverseLayout = true, - modifier = Modifier.weight(1f) - ) { - items(viewState.messages.reversed()) { IncomingMessage(it) } - } - } - Surface( - shape = AckTheme.shapes.corners, - color = AckTheme.colors.surface, - modifier = Modifier - .fillMaxWidth() - .padding(AckTheme.spacing.gutter), - ) { - Column { - Text( - text = viewState.connectionText, - style = AckTheme.typography.caption, - modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = AckTheme.spacing.item) - ) - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - val textFieldValue = remember { mutableStateOf("") } - OutlinedTextField( - value = textFieldValue.value, - onValueChange = { textFieldValue.value = it }, - singleLine = true, - placeholder = { Text(stringResource(R.string.messages_conversation_send_placeholder)) }, - colors = TextFieldDefaults.outlinedTextFieldColors( - backgroundColor = Color.Transparent, - focusedBorderColor = Color.Transparent, - unfocusedBorderColor = Color.Transparent, - disabledBorderColor = Color.Transparent, - ), - modifier = Modifier.weight(1f), - ) - IconButton( - onClick = { - controller.onSendMessage(textFieldValue.value) - textFieldValue.value = "" - }, - enabled = viewState.sendEnabled, - ) { - Icon(Icons.Default.Send, stringResource(R.string.messages_conversation_send_action)) - } - } - } - } - } -} - -@Composable -private fun IncomingMessage(viewModel: MessageItemState) { - Box( - contentAlignment = viewModel.alignment, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = AckTheme.spacing.gutter, vertical = AckTheme.spacing.singleItem) - ) { - Message(viewModel) - } -} - -@Composable -private fun Message(viewModel: MessageItemState) { - Card( - shape = RoundedCornerShape(16.dp), - modifier = Modifier.defaultMinSize(minWidth = 75.dp) - ) { - Column( - modifier = Modifier.padding(AckTheme.spacing.content) - ) { - Text(viewModel.message, style = AckTheme.typography.body) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.align(Alignment.End), - ) { - Text( - text = viewModel.timestamp, - style = AckTheme.typography.caption, - modifier = Modifier.padding(end = AckTheme.spacing.item), - ) - Icon( - imageVector = viewModel.icon, - contentDescription = viewModel.iconDescription, - modifier = Modifier.size(10.dp), - ) - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationViewModel.kt deleted file mode 100644 index 0bf668e7..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationViewModel.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.conversation - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.inkapplications.ack.android.capture.CaptureEvents -import com.inkapplications.ack.android.capture.messages.MessageEvents -import com.inkapplications.ack.structures.station.Callsign -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -/** - * Android Viewmodel to load state information for the conversation with [EXTRA_ADDRESS] as the - * target addressee. - */ -@HiltViewModel -class ConversationViewModel @Inject constructor( - savedState: SavedStateHandle, - messageEvents: MessageEvents, - captureEvents: CaptureEvents, - conversationViewStateFactory: ConversationViewStateFactory, -): ViewModel() { - private val conversationAddress = savedState.get(EXTRA_ADDRESS)?.let(::Callsign)!! - - val conversationState = combine( - messageEvents.conversationList(conversationAddress), - captureEvents.driverSelection, - captureEvents.connectionState - ) { messages, driverSelection, connectionState -> - conversationViewStateFactory.createMessageList(conversationAddress, messages, connectionState, driverSelection) - }.stateIn(viewModelScope, SharingStarted.Eagerly, conversationViewStateFactory.createInitial(conversationAddress)) -} - - diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationViewState.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationViewState.kt deleted file mode 100644 index 87215ffa..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationViewState.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.conversation - -/** - * States for a single conversation view. - */ -sealed interface ConversationViewState { - /** - * Title of the page to display in navigation. - */ - val title: String - - /** - * Whether the send button in the message form should be clickable. - */ - val sendEnabled: Boolean - - /** - * Text displayed above the message field to signify the connection status. - */ - val connectionText: String - - /** - * Indicates that no data has been loaded yet. - */ - data class Initial( - override val title: String, - override val connectionText: String, - ): ConversationViewState { - override val sendEnabled: Boolean = false - } - - /** - * Indicates that data has been loaded, but there are no messages yet. - */ - data class Empty( - override val title: String, - override val sendEnabled: Boolean, - override val connectionText: String, - ): ConversationViewState - - /** - * Message data for the conversation. - * - * @param messages The list of message history. - */ - data class MessageList( - override val title: String, - val messages: List, - override val sendEnabled: Boolean, - override val connectionText: String, - ): ConversationViewState -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationViewStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationViewStateFactory.kt deleted file mode 100644 index 11199726..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationViewStateFactory.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.conversation - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Bluetooth -import androidx.compose.material.icons.filled.Cloud -import androidx.compose.material.icons.filled.SettingsInputAntenna -import androidx.compose.material.icons.filled.Storage -import androidx.compose.ui.Alignment -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.capture.messages.MessageData -import com.inkapplications.ack.android.connection.DriverSelection -import com.inkapplications.ack.android.connection.readableName -import com.inkapplications.ack.data.PacketOrigin -import com.inkapplications.ack.data.drivers.DriverConnectionState -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.station.Callsign -import com.inkapplications.android.extensions.StringResources -import com.inkapplications.android.extensions.format.DateTimeFormatter -import dagger.Reusable -import javax.inject.Inject - -/** - * Creates Conversation state models based on a list of message packets. - */ -@Reusable -class ConversationViewStateFactory @Inject constructor( - private val dateTimeFormatter: DateTimeFormatter, - private val stringResources: StringResources, -) { - /** - * Create the initial viewstate for the view. - * - * @param adressee The callsign of the station this conversation is with. - */ - fun createInitial(adressee: Callsign): ConversationViewState { - return ConversationViewState.Initial( - title = createTitle(adressee), - connectionText = stringResources.getString(R.string.messages_conversation_disconnected) - ) - } - - /** - * Create a message list state model. - * - * @param adressee The callsign of the station this conversation is with. - * @param messages a list of message objectsto be displayed in the conversation. - */ - fun createMessageList( - addressee: Callsign, - messages: List, - connectionState: DriverConnectionState, - driverSelection: DriverSelection, - ): ConversationViewState { - val title = createTitle(addressee) - val connectionText = when (connectionState) { - DriverConnectionState.Connected -> stringResources.getString(R.string.messages_conversation_connected, stringResources.getString(driverSelection.readableName)) - else -> stringResources.getString(R.string.messages_conversation_disconnected) - } - - return when { - messages.isEmpty() -> ConversationViewState.Empty( - title = title, - sendEnabled = connectionState == DriverConnectionState.Connected, - connectionText = connectionText, - ) - else -> ConversationViewState.MessageList( - title = title, - messages = messages.map { createMessageView(it) }, - sendEnabled = connectionState == DriverConnectionState.Connected, - connectionText = connectionText, - ) - } - } - - /** - * Create a single message item's state model. - * - * @param data The message data to render into the state model - */ - private fun createMessageView(data: MessageData): MessageItemState { - return MessageItemState( - message = (data.message.parsed.data as PacketData.Message).message, - timestamp = dateTimeFormatter.formatTimestamp(data.message.received), - alignment = if (data.isOutgoing) { - Alignment.CenterEnd - } else { - Alignment.CenterStart - }, - icon = when (data.message.origin) { - PacketOrigin.Ax25 -> Icons.Default.SettingsInputAntenna - PacketOrigin.AprsIs -> Icons.Default.Cloud - PacketOrigin.Tnc -> Icons.Default.Bluetooth - PacketOrigin.Local -> Icons.Default.Storage - }, - iconDescription = when (data.message.origin) { - PacketOrigin.Ax25 -> stringResources.getString(R.string.messages_item_icon_ax25_description) - PacketOrigin.AprsIs -> stringResources.getString(R.string.messages_item_icon_internet_description) - PacketOrigin.Tnc -> stringResources.getString(R.string.messages_item_icon_tnc_description) - PacketOrigin.Local -> stringResources.getString(R.string.messages_item_icon_local_description) - }, - ) - } - - /** - * Create a readable title from a callsign object. - * - * @param callsign The callsign to be displayed. - */ - private fun createTitle(callsign: Callsign): String = callsign.canonical -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/MessageItemState.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/MessageItemState.kt deleted file mode 100644 index f2be807c..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/conversation/MessageItemState.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.conversation - -import androidx.compose.ui.Alignment -import androidx.compose.ui.graphics.vector.ImageVector - -/** - * State model for a single message in a conversation. - */ -data class MessageItemState( - /** - * The content of the message that was sent. - */ - val message: String, - - /** - * Readable timestamp in local time. - */ - val timestamp: String, - - /** - * Alignment of the text bubble based on the sender. - */ - val alignment: Alignment, - - /** - * State icon used to indicate how the message was sent. - */ - val icon: ImageVector, - - /** - * Content description used for [icon]'s accessibility. - */ - val iconDescription: String, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/create/CreateConversationActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/create/CreateConversationActivity.kt deleted file mode 100644 index 8792ded3..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/create/CreateConversationActivity.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.create - -import androidx.activity.compose.setContent -import com.inkapplications.ack.android.capture.messages.conversation.startConversationActivity -import com.inkapplications.ack.structures.station.Callsign -import com.inkapplications.android.extensions.ExtendedActivity -import kimchi.Kimchi - -/** - * Screen to prompt the user to start a conversation with a specific station. - */ -class CreateConversationActivity: ExtendedActivity(), CreateConversationController { - override fun onCreate() { - super.onCreate() - - Kimchi.trackScreen("create_conversation") - setContent { - CreateConversationScreen(this) - } - } - - override fun onCreateClick(callsign: String) { - Kimchi.trackEvent("create_conversation_start") - finish() - startConversationActivity(Callsign(callsign)) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/create/CreateConversationController.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/create/CreateConversationController.kt deleted file mode 100644 index 6bb3ac19..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/create/CreateConversationController.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.create - -/** - * Actions invoked on the new message screen - */ -interface CreateConversationController { - /** - * Invoked when the user clicks the "start messaging" button. - */ - fun onCreateClick(callsign: String) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/create/CreateConversationScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/create/CreateConversationScreen.kt deleted file mode 100644 index 78cdbeea..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/create/CreateConversationScreen.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.create - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Button -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun CreateConversationScreen( - controller: CreateConversationController, -) { - AckScreen { - Column( - modifier = Modifier.padding(AckTheme.spacing.gutter) - ) { - Text(stringResource(R.string.messages_create_title), style = AckTheme.typography.h1) - - val callsign = rememberSaveable { mutableStateOf("") } - TextField( - value = callsign.value, - label = { Text(stringResource(R.string.messages_create_field_callsign_label)) }, - onValueChange = { callsign.value = it }, - modifier = Modifier.fillMaxWidth().padding(top = AckTheme.spacing.content), - ) - - Button( - onClick = { controller.onCreateClick(callsign.value.trim()) }, - modifier = Modifier.fillMaxWidth().padding(top = AckTheme.spacing.item), - ) { - Text(stringResource(R.string.messages_create_start_action, callsign.value)) - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/ConversationItemState.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/ConversationItemState.kt deleted file mode 100644 index b46d4373..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/ConversationItemState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.index - -import com.inkapplications.ack.structures.station.Callsign - -/** - * Models the view state for a single conversation item in the index. - */ -data class ConversationItemState( - /** - * The displayable name of the correspondent. - */ - val name: String, - - /** - * A preview snippet of the latest message in the conversation. - */ - val messagePreview: String, - - /** - * The callsign of the correspondent, used as an identifier when navigating. - */ - val correspondent: Callsign, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexScreen.kt deleted file mode 100644 index 91d2d36e..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexScreen.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.inkapplications.ack.android.capture.messages - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Inbox -import androidx.compose.material.icons.filled.Message -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.capture.messages.index.ConversationItemState -import com.inkapplications.ack.android.capture.messages.index.MessageIndexState -import com.inkapplications.ack.android.capture.messages.index.MessageIndexViewModel -import com.inkapplications.ack.android.capture.messages.index.MessagesScreenController -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun MessageIndexScreen( - controller: MessagesScreenController, - bottomProtection: Dp, - bottomContentProtection: Dp, - viewModel: MessageIndexViewModel = hiltViewModel(), -) = AckScreen { - val screenState = viewModel.indexState.collectAsState() - - when (val state = screenState.value) { - MessageIndexState.Initial -> {} - is MessageIndexState.ConversationList -> ConversationList(state, controller, bottomContentProtection) - is MessageIndexState.Empty -> EmptyPlaceholder() - } - Box( - modifier = Modifier - .fillMaxSize() - .padding(bottom = bottomProtection), - contentAlignment = Alignment.BottomEnd, - ) { - FloatingActionButton( - onClick = controller::onCreateMessageClick, - backgroundColor = AckTheme.colors.surface, - contentColor = contentColorFor(AckTheme.colors.surface), - modifier = Modifier.padding(AckTheme.spacing.gutter) - ) { - Icon(Icons.Default.Message, stringResource(R.string.messages_compose_action)) - } - } -} - -@Composable -private fun EmptyPlaceholder() = Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .padding(bottom = AckTheme.spacing.navigationProtection) - .fillMaxSize() -) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - imageVector = Icons.Default.Inbox, - contentDescription = null, - tint = AckTheme.colors.foregroundInactive, - modifier = Modifier.size(AckTheme.sizing.dispayIcon), - ) - Text("No messages received") - } -} - -@Composable -private fun ConversationList( - state: MessageIndexState.ConversationList, - controller: MessagesScreenController, - bottomProtection: Dp, -) { - LazyColumn( - contentPadding = PaddingValues(bottom = bottomProtection) - ) { - items(state.conversations) { conversation -> - ConversationItem(conversation, controller) - } - } -} - -@Composable -@OptIn(ExperimentalMaterialApi::class) -private fun ConversationItem(viewModel: ConversationItemState, controller: MessagesScreenController) { - Card( - onClick = { controller.onConversationClick(viewModel.correspondent) }, - modifier = Modifier.padding(horizontal = AckTheme.spacing.gutter, vertical = AckTheme.spacing.singleItem) - ) { - Column(modifier = Modifier - .padding(AckTheme.spacing.content) - .fillMaxWidth()) { - Text(viewModel.name, style = AckTheme.typography.h2) - Text(viewModel.messagePreview) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexState.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexState.kt deleted file mode 100644 index ae8c9b36..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexState.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.index - -/** - * State of the list of conversations screen. - */ -sealed interface MessageIndexState { - /** - * Indicates that no data has been loaded yet. - */ - object Initial: MessageIndexState - - /** - * State that indicates that data is loaded, but no messages have been - * sent or received. - */ - object Empty: MessageIndexState - - /** - * Loaded data to display a list of conversations. - */ - data class ConversationList( - /** - * Conversation rows to display. - */ - val conversations: List, - ): MessageIndexState -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexStateFactory.kt deleted file mode 100644 index b07e57a8..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexStateFactory.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.index - -import com.inkapplications.ack.android.capture.messages.MessageData -import com.inkapplications.ack.structures.PacketData -import dagger.Reusable -import javax.inject.Inject - -/** - * Create the state models for a list of message conversations. - */ -@Reusable -class MessageIndexStateFactory @Inject constructor() { - /** - * Create a state object for the conversation list. - * - * @param latestMessages A list of the latest messages in each conversation. - */ - fun createScreenState(latestMessages: List): MessageIndexState { - return when { - latestMessages.isEmpty() -> MessageIndexState.Empty - else -> MessageIndexState.ConversationList( - conversations = latestMessages.map(::createConversationItem), - ) - } - } - - /** - * Create a single conversation state model from its latest message. - * - * @param data The latest message in this conversation. - */ - private fun createConversationItem(data: MessageData): ConversationItemState { - val message = data.message.parsed.data as PacketData.Message - val correspondent = message.addressee.takeIf { data.isOutgoing } ?: data.message.parsed.route.source - return ConversationItemState( - name = correspondent.callsign.canonical, - messagePreview = message.message, - correspondent = correspondent.callsign, - ) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexViewModel.kt deleted file mode 100644 index c5069f35..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexViewModel.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.index - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.inkapplications.ack.android.capture.messages.MessageEvents -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -/** - * Android viewmodel to load state information for the conversation list. - */ -@HiltViewModel -class MessageIndexViewModel @Inject constructor( - private val messageEvents: MessageEvents, - private val messageIndexStateFactory: MessageIndexStateFactory, -): ViewModel() { - /** - * List of conversations to be displayed. - */ - val indexState = messageEvents.latestMessageByConversation - .map { messageIndexStateFactory.createScreenState(it) } - .stateIn(viewModelScope, SharingStarted.Eagerly, MessageIndexState.Initial) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessagesScreenController.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessagesScreenController.kt deleted file mode 100644 index fc934d22..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/messages/index/MessagesScreenController.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.index - -import com.inkapplications.ack.structures.station.Callsign - -/** - * Actions that can be taken on the messages screen. - */ -interface MessagesScreenController { - /** - * Invoked when the user clicks the new conversation button. - */ - fun onCreateMessageClick() - - /** - * Invoked when the user clicks on a conversation - * - * @param callsign The callsign as an identifier for the conversation that was clicked. - */ - fun onConversationClick(callsign: Callsign) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/service/BackgroundCaptureService.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/service/BackgroundCaptureService.kt deleted file mode 100644 index fe04f560..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/service/BackgroundCaptureService.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.inkapplications.ack.android.capture.service - -import android.app.Service -import android.content.Intent -import android.os.IBinder -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.capture.CaptureEvents -import dagger.hilt.android.AndroidEntryPoint -import kimchi.Kimchi -import kotlinx.coroutines.* -import javax.inject.Inject - -/** - * Service that runs in the background to collect and transmit packets. - */ -@AndroidEntryPoint -open class BackgroundCaptureService: Service() { - private lateinit var runScope: CoroutineScope - - @Inject - lateinit var captureEvents: CaptureEvents - - @Inject - lateinit var captureServiceNotifications: CaptureServiceNotifications - private val notificationId: Int = 89 - private val notificationTitle: String get() = getString(R.string.capture_service_notification_title) - - override fun onBind(intent: Intent?): IBinder? = null - - override fun onCreate() { - super.onCreate() - Kimchi.info("onCreate BackgroundCaptureService") - Kimchi.trackEvent("background_capture_start") - runScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - val notification = captureServiceNotifications.createServiceNotification(this, notificationTitle) - startForeground(notificationId, notification) - runScope.launch { - captureEvents.connectDriver() - captureEvents.locationTransmitLoop() - stopSelf() - } - - return super.onStartCommand(intent, flags, startId) - } - - override fun onDestroy() { - Kimchi.trackEvent("background_capture_destroy") - runScope.cancel() - super.onDestroy() - } -} - -class BackgroundCaptureServiceAudio: BackgroundCaptureService() diff --git a/android-application/src/main/java/com/inkapplications/ack/android/capture/service/CaptureServiceNotifications.kt b/android-application/src/main/java/com/inkapplications/ack/android/capture/service/CaptureServiceNotifications.kt deleted file mode 100644 index 141bcb16..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/capture/service/CaptureServiceNotifications.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.inkapplications.ack.android.capture.service - -import android.app.* -import android.app.PendingIntent.FLAG_IMMUTABLE -import android.content.Context -import android.content.Intent -import android.os.Build -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.capture.CaptureActivity -import com.inkapplications.ack.android.startup.ApplicationInitializer -import com.inkapplications.android.extensions.notificationBuilder -import kimchi.logger.KimchiLogger -import javax.inject.Inject - -private const val CHANNEL_ID = "ack.capture.services" - -/** - * Creates notification channels for use with the capture service at app start. - */ -class CaptureServiceNotifications @Inject constructor( - private val notificationManager: NotificationManager, - private val logger: KimchiLogger, -): ApplicationInitializer { - override suspend fun initialize(application: Application) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - logger.debug("Skipping notification channel setup") - return - } - logger.debug("Creating Capture Service Notification Channel") - val channel = NotificationChannel(CHANNEL_ID, application.getString(R.string.capture_service_channel_name), NotificationManager.IMPORTANCE_LOW) - channel.description = application.getString(R.string.capture_service_channel_description) - notificationManager.createNotificationChannel(channel) - } - - fun createServiceNotification(context: Context, title: String): Notification { - val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) FLAG_IMMUTABLE else 0 - val pendingIntent: PendingIntent = Intent(context, CaptureActivity::class.java).let { notificationIntent -> - PendingIntent.getActivity(context, 0, notificationIntent, flags) - } - - return context.notificationBuilder(CHANNEL_ID) - .setContentTitle(title) - .setSmallIcon(R.drawable.capture_service_notification_icon) - .setContentIntent(pendingIntent) - .build() - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/connection/ConnectionModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/connection/ConnectionModule.kt deleted file mode 100644 index c98dca9d..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/connection/ConnectionModule.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.inkapplications.ack.android.connection - -import com.inkapplications.ack.android.settings.SettingsProvider -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(SingletonComponent::class) -abstract class ConnectionModule { - @Binds - @IntoSet - abstract fun settings(settings: ConnectionSettings): SettingsProvider -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/connection/ConnectionSettings.kt b/android-application/src/main/java/com/inkapplications/ack/android/connection/ConnectionSettings.kt deleted file mode 100644 index b17d82ae..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/connection/ConnectionSettings.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.inkapplications.ack.android.connection - -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.input.IntegerValidator -import com.inkapplications.ack.android.input.RegexValidator -import com.inkapplications.ack.android.input.enumInputValidator -import com.inkapplications.ack.android.settings.* -import com.inkapplications.ack.android.settings.transformer.* -import com.inkapplications.ack.data.DEFAULT_CONNECTION_PORT -import com.inkapplications.ack.data.DEFAULT_CONNECTION_SERVER -import com.inkapplications.ack.data.DEFAULT_SEARCH_RADIUS_MILES -import com.inkapplications.android.extensions.StringResources -import dagger.Reusable -import inkapplications.spondee.measure.us.miles -import javax.inject.Inject - -@Reusable -class ConnectionSettings @Inject constructor( - resources: StringResources, -): SettingsProvider { - private val callsignRegex = Regex("^[0-9a-zA-Z]{1,3}\\d[0-9a-zA-Z]{0,4}[a-zA-Z](?:-[a-zA-Z0-9]{1,2})?\$") - private val hostnameRegex = Regex("^(?=.{1,255}\$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\\.?\$") - - val address = StringBackedSetting( - key = "connection.address", - name = resources.getString(R.string.connection_setting_address_name), - categoryName = resources.getString(R.string.connection_setting_category_name), - defaultData = null, - visibility = SettingVisibility.Dev, - storageTransformer = SentinelOptionalTransformer( - sentinelValue = "", - delegate = StationAddressTransformer + TrimmingTransformer, - ), - inputValidator = RegexValidator( - regex = callsignRegex, - error = resources.getString(R.string.connection_setting_address_invalid), - allowEmpty = true, - ), - ) - - val passcode = IntBackedSetting( - key = "connection.passcode", - name = resources.getString(R.string.connection_setting_passcode_name), - categoryName = resources.getString(R.string.connection_setting_category_name), - defaultData = null, - visibility = SettingVisibility.Dev, - storageTransformer = SentinelOptionalTransformer( - sentinelValue = -1, - delegate = object: Transformer { - override fun toStorage(data: Passcode): Int = data.value - override fun toData(storage: Int): Passcode = storage.let(::Passcode) - }, - ), - inputValidator = IntegerValidator( - error = resources.getString(R.string.connection_setting_passcode_invalid), - allowSentinel = -1, - ), - fieldTransformer = OptionalIntTransformer(), - ) - - val server = StringSetting( - key = "connection.server", - name = resources.getString(R.string.connection_setting_server_name), - categoryName = resources.getString(R.string.connection_setting_category_name), - defaultValue = DEFAULT_CONNECTION_SERVER, - visibility = SettingVisibility.Advanced, - validator = RegexValidator( - regex = hostnameRegex, - error = resources.getString(R.string.connection_setting_server_invalid), - ), - ) - val port = IntSetting( - key = "connection.port", - name = resources.getString(R.string.connection_setting_port_name), - categoryName = resources.getString(R.string.connection_setting_category_name), - defaultValue = DEFAULT_CONNECTION_PORT, - visibility = SettingVisibility.Advanced, - validator = IntegerValidator( - error = resources.getString(R.string.connection_setting_port_invalid), - zeroInclusive = false, - ), - ) - - val radius = IntBackedSetting( - key = "connection.radius", - name = resources.getString(R.string.connection_setting_radius_name), - categoryName = resources.getString(R.string.connection_setting_category_name), - defaultData = DEFAULT_SEARCH_RADIUS_MILES.miles, - storageTransformer = MileTransformer, - inputValidator = IntegerValidator( - error = resources.getString(R.string.connection_setting_radius_invalid), - zeroInclusive = false, - ), - ) - - val driver = StringBackedSetting( - key = "connection.driver", - categoryName = resources.getString(R.string.connection_setting_category_name), - defaultData = DriverSelection.Audio, - name = resources.getString(R.string.transmit_settings_driver), - visibility = SettingVisibility.Dev, - storageTransformer = enumTransformer(), - inputValidator = enumInputValidator( - error = resources.getString( - R.string.settings_validation_not_in_set, - DriverSelection.values().joinToString { it.name }, - ), - ), - ) - - override val settings: List = listOf( - address, - passcode, - server, - port, - radius, - driver, - ) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/connection/DriverSelection.kt b/android-application/src/main/java/com/inkapplications/ack/android/connection/DriverSelection.kt deleted file mode 100644 index b54b0e71..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/connection/DriverSelection.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.inkapplications.ack.android.connection - -import androidx.annotation.StringRes -import com.inkapplications.ack.android.R - -/** - * Represents the user-selected source of APRS data. - */ -enum class DriverSelection { - Audio, - Internet, - Tnc, -} - -/** - * Resource ID of the user-readable name for the selected driver. - */ -val DriverSelection.readableName get(): @StringRes Int = when (this) { - DriverSelection.Audio -> R.string.capture_driver_selection_audio - DriverSelection.Internet -> R.string.capture_driver_selection_internet - DriverSelection.Tnc -> R.string.capture_driver_selection_tnc -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/firebase/FirebaseInitializer.kt b/android-application/src/main/java/com/inkapplications/ack/android/firebase/FirebaseInitializer.kt deleted file mode 100644 index bdf5c6d6..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/firebase/FirebaseInitializer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.inkapplications.ack.android.firebase - -import android.app.Application -import com.google.firebase.ktx.Firebase -import com.google.firebase.remoteconfig.ktx.remoteConfig -import com.inkapplications.ack.android.settings.SettingsProvider -import com.inkapplications.ack.android.startup.ApplicationInitializer -import dagger.Reusable -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.suspendCancellableCoroutine -import javax.inject.Inject -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -@Reusable -class FirebaseInitializer @Inject constructor( - private val settingsProvider: SettingsProvider, - private val logger: KimchiLogger -): ApplicationInitializer { - override suspend fun initialize(application: Application) { - logger.trace("Initializing Firebase.") - suspendCancellableCoroutine { continuation -> - settingsProvider.settings - .map { it.key to it.defaultValue } - .toMap() - .onEach { logger.debug("Initializing <${it.key}> with default <${it.value}>") } - .run(Firebase.remoteConfig::setDefaultsAsync) - .addOnSuccessListener { continuation.resume(Unit) } - .addOnFailureListener { continuation.resumeWithException(it) } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/firebase/FirebaseModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/firebase/FirebaseModule.kt deleted file mode 100644 index bb0b7a56..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/firebase/FirebaseModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.inkapplications.ack.android.firebase - -import com.inkapplications.ack.android.BuildConfig -import com.inkapplications.ack.android.startup.ApplicationInitializer -import com.inkapplications.ack.android.startup.NoOpInitializer -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(SingletonComponent::class) -class FirebaseModule { - @Provides - @IntoSet - fun initializer(firebaseInitializer: FirebaseInitializer): ApplicationInitializer { - if (BuildConfig.USE_GOOGLE_SERVICES) { - return firebaseInitializer - } - return NoOpInitializer - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/firebase/FirebaseSettings.kt b/android-application/src/main/java/com/inkapplications/ack/android/firebase/FirebaseSettings.kt deleted file mode 100644 index 7ec04ad2..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/firebase/FirebaseSettings.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.inkapplications.ack.android.firebase - -import com.google.firebase.ktx.Firebase -import com.google.firebase.remoteconfig.ktx.remoteConfig -import com.inkapplications.ack.android.settings.BooleanSetting -import com.inkapplications.ack.android.settings.IntSetting -import com.inkapplications.ack.android.settings.SettingsReadAccess -import com.inkapplications.ack.android.settings.StringSetting -import dagger.Reusable -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import javax.inject.Inject - -@Reusable -class FirebaseSettings @Inject constructor(): SettingsReadAccess { - override fun observeStringState(setting: StringSetting): Flow { - return flowOf(Firebase.remoteConfig.getString(setting.key)) - } - - override fun observeIntState(setting: IntSetting): Flow { - return flowOf(Firebase.remoteConfig.getLong(setting.key).toInt()) - } - - override fun observeBooleanState(setting: BooleanSetting): Flow { - return flowOf(Firebase.remoteConfig.getBoolean(setting.key)) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/input/ConvertibleOptionalIntValidator.kt b/android-application/src/main/java/com/inkapplications/ack/android/input/ConvertibleOptionalIntValidator.kt deleted file mode 100644 index 90f635b6..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/input/ConvertibleOptionalIntValidator.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.inkapplications.ack.android.input - -import com.inkapplications.ack.android.R -import com.inkapplications.android.extensions.StringResources -import dagger.Reusable -import javax.inject.Inject - -/** - * Ensures that a string can be successfully converted to an integer, or is blank. - */ -@Reusable -class ConvertibleOptionalIntValidator @Inject constructor( - private val stringResources: StringResources, -) : Validator { - override fun validate(input: String): ValidationResult { - val result = runCatching { input.toInt() } - return when { - input.isBlank() -> ValidationResult.Valid - result.isSuccess -> ValidationResult.Valid - else -> ValidationResult.Error( - stringResources.getString(R.string.prompt_int_invalid_error) - ) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/input/EnumValidator.kt b/android-application/src/main/java/com/inkapplications/ack/android/input/EnumValidator.kt deleted file mode 100644 index 56c1beea..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/input/EnumValidator.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.inkapplications.ack.android.input - -/** - * Validates an input string, asserting that its value is a valid enum name. - */ -inline fun > enumInputValidator( - error: String, -): Validator { - return object: Validator { - override fun validate(input: String): ValidationResult { - return if (input in enumValues().map { it.name }) { - ValidationResult.Valid - } else { - ValidationResult.Error(error) - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/input/IntPrompt.kt b/android-application/src/main/java/com/inkapplications/ack/android/input/IntPrompt.kt deleted file mode 100644 index 4de129e7..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/input/IntPrompt.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.inkapplications.ack.android.input - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.window.Dialog -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.ui.theme.AckTheme - -/** - * Displays a dialog asking the user for a number input. - * - * @param title The dialog's title text. - * @param value The current value of the field. - * @param validator Asserts that the input value is valid. - * @param onDismiss Invoked when the user cancels or taps-out of the dialog. - * @param onSubmit Invoked with the entered validated value when the user saves. - */ -@Composable -fun IntPrompt( - title: String, - value: Int, - validator: Validator, - onDismiss: () -> Unit, - onSubmit: (Int) -> Unit, -) { - val inputState = remember { mutableStateOf(value.toString()) } - val errorState = remember { mutableStateOf(null) } - val inputInvalid = stringResource(R.string.prompt_int_invalid_error) - Dialog( - onDismissRequest = onDismiss, - ) { - Surface(shape = AckTheme.shapes.corners) { - Column( - modifier = Modifier.padding(AckTheme.spacing.gutter), - ) { - Text(title, style = AckTheme.typography.h2) - TextField( - value = inputState.value, - onValueChange = { inputState.value = it }, - isError = errorState.value != null, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.padding(top = AckTheme.spacing.item).fillMaxWidth(), - ) - Text( - text = errorState.value.orEmpty(), - style = AckTheme.typography.errorCaption, - ) - - Row( - horizontalArrangement = Arrangement.End, - modifier = Modifier.fillMaxWidth().padding(top = AckTheme.spacing.item), - ) { - TextButton( - onClick = onDismiss, - colors = ButtonDefaults.textButtonColors( - contentColor = AckTheme.colors.foreground, - ), - modifier = Modifier.padding(end = AckTheme.spacing.item), - ) { - Text(stringResource(R.string.prompt_cancel)) - } - TextButton( - onClick = { - val input = inputState.value.trim().toIntOrNull() - if (input == null) { - errorState.value = inputInvalid - } else { - when (val result = validator.validate(input)) { - is ValidationResult.Error -> errorState.value = result.message - ValidationResult.Valid -> onSubmit(input) - } - } - }, - ) { - Text(stringResource(R.string.prompt_save)) - } - } - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/input/IntegerValidator.kt b/android-application/src/main/java/com/inkapplications/ack/android/input/IntegerValidator.kt deleted file mode 100644 index 5abedf0f..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/input/IntegerValidator.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.inkapplications.ack.android.input - -/** - * Validates common constraints on positive integer values. - * - * @param error The error message to return if the regex does not match. - * @param requirePositive Whether the value must be greater than zero. - * @param zeroInclusive Whether the value can be zero. - * @param sentinelValue An arbitrary single value to consider valid, when using sentinel values. - */ -class IntegerValidator( - private val error: String, - private val zeroInclusive: Boolean = true, - private val allowSentinel: Int? = null, -): Validator { - override fun validate(input: Int): ValidationResult { - return when { - allowSentinel != null && input == allowSentinel -> ValidationResult.Valid - input < 0 -> ValidationResult.Error(error) - !zeroInclusive && input == 0 -> ValidationResult.Error(error) - else -> ValidationResult.Valid - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/input/MaxLengthValidator.kt b/android-application/src/main/java/com/inkapplications/ack/android/input/MaxLengthValidator.kt deleted file mode 100644 index 0bea37d4..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/input/MaxLengthValidator.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.inkapplications.ack.android.input - -class MaxLengthValidator( - private val error: String, - private val maxLength: Int, -): Validator { - override fun validate(input: String): ValidationResult { - return if (input.length <= maxLength) { - ValidationResult.Valid - } else { - ValidationResult.Error(error) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/input/MinLengthValidator.kt b/android-application/src/main/java/com/inkapplications/ack/android/input/MinLengthValidator.kt deleted file mode 100644 index 48f9ddc8..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/input/MinLengthValidator.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.inkapplications.ack.android.input - -class MinLengthValidator( - private val error: String, - private val minLength: Int, -): Validator { - override fun validate(input: String): ValidationResult { - return if (input.length >= minLength) { - ValidationResult.Valid - } else { - ValidationResult.Error(error) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/input/NoValidation.kt b/android-application/src/main/java/com/inkapplications/ack/android/input/NoValidation.kt deleted file mode 100644 index 06a2bd96..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/input/NoValidation.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.inkapplications.ack.android.input - -/** - * Validator that always passes regardless of the input - */ -object NoValidation: Validator { - override fun validate(input: Any?) = ValidationResult.Valid -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/input/RegexValidator.kt b/android-application/src/main/java/com/inkapplications/ack/android/input/RegexValidator.kt deleted file mode 100644 index 6999ed9d..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/input/RegexValidator.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.inkapplications.ack.android.input - -/** - * Validator that checks if a string matches a regex. - * - * @param regex The regex to consider valid input. - * @param error The error message to return if the regex does not match. - */ -class RegexValidator( - private val regex: Regex, - private val error: String, - private val trim: Boolean = true, - private val allowEmpty: Boolean = false, -): Validator { - override fun validate(input: String): ValidationResult { - if (input.isBlank() && allowEmpty) return ValidationResult.Valid - return if (regex.matches(input.let { if(trim) it.trim() else it })) { - ValidationResult.Valid - } else { - ValidationResult.Error(error) - } - } -} - diff --git a/android-application/src/main/java/com/inkapplications/ack/android/input/StringPrompt.kt b/android-application/src/main/java/com/inkapplications/ack/android/input/StringPrompt.kt deleted file mode 100644 index d7cbe164..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/input/StringPrompt.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.inkapplications.ack.android.input - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.window.Dialog -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.ui.theme.AckTheme - -/** - * Displays a dialog asking the user for an arbitrary single-line string input. - * - * @param title The dialog's title text. - * @param value The current value of the field. - * @param validator Asserts that the input value is valid. - * @param onDismiss Invoked when the user cancels or taps-out of the dialog. - * @param onSubmit Invoked with the entered validated value when the user saves. - */ -@Composable -fun StringPrompt( - title: String, - value: String, - validator: Validator, - onDismiss: () -> Unit, - onSubmit: (String) -> Unit, -) { - val inputState = remember { mutableStateOf(value) } - val errorState = remember { mutableStateOf(null) } - Dialog( - onDismissRequest = onDismiss, - ) { - Surface(shape = AckTheme.shapes.corners) { - Column( - modifier = Modifier.padding(AckTheme.spacing.gutter), - ) { - Text(title, style = AckTheme.typography.h2) - TextField( - value = inputState.value, - onValueChange = { inputState.value = it }, - isError = errorState.value != null, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii), - modifier = Modifier.padding(top = AckTheme.spacing.item).fillMaxWidth(), - ) - Text( - text = errorState.value.orEmpty(), - style = AckTheme.typography.errorCaption, - ) - - Row( - horizontalArrangement = Arrangement.End, - modifier = Modifier.fillMaxWidth().padding(top = AckTheme.spacing.item), - ) { - TextButton( - onClick = onDismiss, - colors = ButtonDefaults.textButtonColors( - contentColor = AckTheme.colors.foreground, - ), - modifier = Modifier.padding(end = AckTheme.spacing.item), - ) { - Text(stringResource(R.string.prompt_cancel)) - } - TextButton( - onClick = { - val input = inputState.value.trim() - when (val result = validator.validate(input)) { - is ValidationResult.Error -> errorState.value = result.message - ValidationResult.Valid -> onSubmit(input) - } - }, - ) { - Text(stringResource(R.string.prompt_save)) - } - } - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/input/ValidationResult.kt b/android-application/src/main/java/com/inkapplications/ack/android/input/ValidationResult.kt deleted file mode 100644 index 65efdbe6..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/input/ValidationResult.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.inkapplications.ack.android.input - -/** - * Results of a [Validator]'s validation check. - */ -sealed interface ValidationResult { - /** - * Indicates that the given input was valid. - */ - object Valid: ValidationResult - - /** - * Indicates that the given input was not valid, and explains why with a [message] - * - * @param message A user-readable message to explain why the input was not valid. - */ - data class Error(val message: String): ValidationResult -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/input/Validator.kt b/android-application/src/main/java/com/inkapplications/ack/android/input/Validator.kt deleted file mode 100644 index b97920f3..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/input/Validator.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.inkapplications.ack.android.input - -/** - * Checks if an input type is allowable. - */ -interface Validator { - /** - * Check if the input is allowed, returning a specific result. - */ - fun validate(input: T): ValidationResult -} - -private class CompositeValidator( - val validators: List>, -): Validator { - override fun validate(input: T): ValidationResult { - return validators - .map { it.validate(input) } - .firstOrNull { it is ValidationResult.Error } - ?: ValidationResult.Valid - } -} - -operator fun Validator.plus(other: Validator): Validator { - val validators = when (this) { - is CompositeValidator -> this.validators + other - else -> listOf(this, other) - } - - return CompositeValidator(validators) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/locale/LocaleModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/locale/LocaleModule.kt deleted file mode 100644 index a3545787..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/locale/LocaleModule.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.inkapplications.ack.android.locale - -import com.inkapplications.ack.android.settings.SettingsProvider -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(SingletonComponent::class) -abstract class LocaleModule { - @Binds - @IntoSet - abstract fun settings(settings: LocaleSettings): SettingsProvider -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/locale/LocaleSettings.kt b/android-application/src/main/java/com/inkapplications/ack/android/locale/LocaleSettings.kt deleted file mode 100644 index feb0472b..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/locale/LocaleSettings.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.inkapplications.ack.android.locale - -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.settings.BooleanSetting -import com.inkapplications.ack.android.settings.Setting -import com.inkapplications.ack.android.settings.SettingsProvider -import com.inkapplications.android.extensions.StringResources -import dagger.Reusable -import javax.inject.Inject - -@Reusable -class LocaleSettings @Inject constructor( - stringResources: StringResources -): SettingsProvider { - val preferMetric = BooleanSetting( - key = "locale.metric", - name = stringResources.getString(R.string.locale_setting_metric_name), - categoryName = stringResources.getString(R.string.locale_setting_category), - defaultValue = false - ) - - - override val settings: List = listOf(preferMetric) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/locale/Units.kt b/android-application/src/main/java/com/inkapplications/ack/android/locale/Units.kt deleted file mode 100644 index d1598c2b..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/locale/Units.kt +++ /dev/null @@ -1,37 +0,0 @@ -@file:OptIn(SimpleNumberFormats::class) - -package com.inkapplications.ack.android.locale - -import inkapplications.spondee.format.SimpleNumberFormats -import inkapplications.spondee.format.formatDecimal -import inkapplications.spondee.measure.Length -import inkapplications.spondee.measure.Speed -import inkapplications.spondee.measure.Temperature -import inkapplications.spondee.measure.metric.toCelsius -import inkapplications.spondee.measure.metric.toKilometersPerHourValue -import inkapplications.spondee.measure.metric.toMetersPerSecondValue -import inkapplications.spondee.measure.us.toFahrenheit -import inkapplications.spondee.measure.us.toFeet -import inkapplications.spondee.measure.us.toMiles -import inkapplications.spondee.measure.us.toMilesPerHourValue -import inkapplications.spondee.structure.Kilo -import inkapplications.spondee.structure.compareTo -import inkapplications.spondee.structure.format - -fun Length.format(metric: Boolean) = when { - metric && toMeters() > 1000.0 -> toMeters().format(Kilo) - metric -> toMeters().format() - toFeet() > 1000 -> toMiles().format() - else -> toFeet().format() -} - -fun Speed.format(metric: Boolean) = when { - metric && toMetersPerSecondValue() >= 1000 -> "${toKilometersPerHourValue().formatDecimal()}km/h" - metric -> "${toMetersPerSecondValue().formatDecimal()}m/s" - else -> "${toMilesPerHourValue().formatDecimal()}mph" -} - -fun Temperature.format(metric: Boolean) = when { - metric -> toCelsius().format(decimals = 1) - else -> toFahrenheit().format() -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/AprsLogItem.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/AprsLogItem.kt deleted file mode 100644 index 85c3df70..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/AprsLogItem.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.inkapplications.ack.android.log - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Card -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.inkapplications.ack.android.capture.log.AprsSymbol -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun AprsLogItem( - log: LogItemViewState, - onClick: (LogItemViewState) -> Unit, - border: Boolean = false, -) { - Card( - border = BorderStroke(1.dp, AckTheme.colors.accent).takeIf { border }, - modifier = Modifier - .padding( - vertical = AckTheme.spacing.singleItem, - horizontal = AckTheme.spacing.gutter - ) - .fillMaxWidth() - .clickable { onClick(log) } - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(AckTheme.spacing.content) - ) { - AprsSymbol(log.symbol) - - Column( - modifier = Modifier.padding(start = AckTheme.spacing.content) - ) { - Text( - text = log.origin, - style = AckTheme.typography.h3, - ) - Text( - text = log.comment, - style = AckTheme.typography.body, - ) - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/CombinedLogItemViewStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/CombinedLogItemViewStateFactory.kt deleted file mode 100644 index 25b99865..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/CombinedLogItemViewStateFactory.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.inkapplications.ack.android.log - -import com.inkapplications.ack.android.locale.format -import com.inkapplications.ack.android.symbol.SymbolFactory -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.structures.AprsPacket -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.capabilities.Mapable -import dagger.Reusable -import javax.inject.Inject - -@Reusable -class CombinedLogItemViewStateFactory @Inject constructor( - private val symbolFactory: SymbolFactory -): LogItemViewStateFactory { - override fun create(packets: List, metric: Boolean): List { - return packets.map { create(it.id, it.parsed, metric) } - } - - override fun create( - id: CaptureId, - packet: AprsPacket, - metric: Boolean, - ): LogItemViewState { - return LogItemViewState( - id = id, - source = packet.route.source.callsign, - origin = packet.route.source.toString(), - comment = when (val data = packet.data) { - is PacketData.Position -> "Position${data.comment.append()}" - is PacketData.Weather -> "Weather${data.temperature?.format(metric).append()}" - is PacketData.ObjectReport -> "Object: ${data.name}${data.comment.append(" - ")}" - is PacketData.ItemReport -> "Item: ${data.name}${data.comment.append(" - ")}" - is PacketData.Message -> "[${data.addressee}] ${data.message}${data.messageNumber?.let { " ($it)" }.orEmpty()}" - is PacketData.TelemetryReport -> "Telemetry${data.comment.append()}" - is PacketData.StatusReport -> "Status${data.status.append()}" - is PacketData.CapabilityReport -> "Capability Report" - is PacketData.Unknown -> "Unknown data" - }, - symbol = when (val data = packet.data) { - is Mapable -> data.symbol?.let(symbolFactory::createSymbol) - else -> symbolFactory.defaultSymbol - } - ) - } - - private fun String?.append(separator: String = ": ") = this.takeIfNotEmpty()?.let { "${separator}$it" }.orEmpty() - private fun String?.takeIfNotEmpty() = this?.takeIf { it.isNotBlank() } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/LogEvents.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/LogEvents.kt deleted file mode 100644 index c2b52916..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/LogEvents.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.inkapplications.ack.android.log - -import com.inkapplications.ack.android.locale.LocaleSettings -import com.inkapplications.ack.android.log.details.LogDetailData -import com.inkapplications.ack.android.log.index.LogIndexData -import com.inkapplications.ack.android.map.MarkerViewStateFactory -import com.inkapplications.ack.android.maps.CameraPositionDefaults -import com.inkapplications.ack.android.maps.MapCameraPosition -import com.inkapplications.ack.android.maps.MapViewModel -import com.inkapplications.ack.android.maps.ZoomLevels -import com.inkapplications.ack.android.settings.SettingsReadAccess -import com.inkapplications.ack.android.settings.observeBoolean -import com.inkapplications.ack.android.station.StationSettings -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.data.PacketStorage -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.capabilities.Mapable -import com.inkapplications.coroutines.combinePair -import com.inkapplications.coroutines.filterItems -import dagger.Reusable -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -@Reusable -class LogEvents @Inject constructor( - private val packetStorage: PacketStorage, - private val markerViewStateFactory: MarkerViewStateFactory, - private val settings: SettingsReadAccess, - private val localeSettings: LocaleSettings, - private val stationSettings: StationSettings, - logSettings: LogSettings, - private val logger: KimchiLogger, -) { - val logIndex = settings.observeBoolean(localeSettings.preferMetric) - .combinePair(settings.observeBoolean(logSettings.filterUnknown)) - .flatMapLatest { (metric, filterUnknown) -> - packetStorage.findRecent(500) - .filterItems { !filterUnknown || it.parsed.data !is PacketData.Unknown } - .map { LogIndexData(metric, it) } - } - - fun stateEvents(id: CaptureId): Flow { - logger.trace("Observing packet: $id") - return packetStorage.findById(id) - .filterNotNull() - .combine(settings.observeBoolean(localeSettings.preferMetric)) { packet, metric -> - LogDetailData( - packet = packet, - metric = metric, - ) - } - .combine(settings.observeBoolean(stationSettings.showDebugData)) { data, debugData -> - data.copy(debug = debugData) - } - } - - fun mapState(id: CaptureId): Flow { - return packetStorage.findById(id) - .filterNotNull() - .map { packet -> - MapViewModel( - markers = markerViewStateFactory.create(packet)?.let { listOf(it) }.orEmpty(), - cameraPosition = (packet.parsed.data as? Mapable)?.coordinates - ?.let { MapCameraPosition(it, ZoomLevels.ROADS) } - ?: CameraPositionDefaults.unknownLocation, - ) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/LogItemViewState.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/LogItemViewState.kt deleted file mode 100644 index 78ef1f46..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/LogItemViewState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.inkapplications.ack.android.log - -import android.graphics.Bitmap -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.structures.station.Callsign - -data class LogItemViewState( - val id: CaptureId, - val source: Callsign, - val origin: String, - val comment: String, - val symbol: Bitmap? -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/LogItemViewStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/LogItemViewStateFactory.kt deleted file mode 100644 index 13c5ab5f..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/LogItemViewStateFactory.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.inkapplications.ack.android.log - -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.structures.AprsPacket - -interface LogItemViewStateFactory { - fun create( - id: CaptureId, - packet: AprsPacket, - metric: Boolean, - ): LogItemViewState - - fun create( - packets: List, - metric: Boolean, - ): List -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/LogModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/LogModule.kt deleted file mode 100644 index 8ecb7986..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/LogModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.inkapplications.ack.android.log - -import com.inkapplications.ack.android.settings.SettingsProvider -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(SingletonComponent::class) -interface LogModule { - @Binds - @IntoSet - fun settings(settings: LogSettings): SettingsProvider - - @Binds - fun logFactory(factory: CombinedLogItemViewStateFactory): LogItemViewStateFactory -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/LogSettings.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/LogSettings.kt deleted file mode 100644 index 5f0bba02..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/LogSettings.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.inkapplications.ack.android.log - -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.settings.BooleanSetting -import com.inkapplications.ack.android.settings.Setting -import com.inkapplications.ack.android.settings.SettingsProvider -import com.inkapplications.android.extensions.StringResources -import dagger.Reusable -import javax.inject.Inject - -@Reusable -class LogSettings @Inject constructor( - resources: StringResources -): SettingsProvider { - val filterUnknown = BooleanSetting( - key = "log.filter.unknown", - name = resources.getString(R.string.log_setting_filter_unknown), - categoryName = resources.getString(R.string.log_setting_category), - defaultValue = false, - ) - - override val settings: List = listOf(filterUnknown) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/SummaryFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/SummaryFactory.kt deleted file mode 100644 index 5e924802..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/SummaryFactory.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.inkapplications.ack.android.log - -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.locale.format -import com.inkapplications.ack.structures.WindData -import com.inkapplications.android.extensions.StringResources -import inkapplications.spondee.spatial.toDegrees -import inkapplications.spondee.structure.format -import javax.inject.Inject - -/** - * Create readable sentence summaries from packet data. - */ -class SummaryFactory @Inject constructor( - private val stringResources: StringResources, -) { - /** - * Create a readable sentence out of various wind data, if available. - */ - fun createWindSummary(data: WindData, metric: Boolean): String? { - val direction = data.direction?.toDegrees()?.format() - val speed = data.speed?.format(metric) - val gust = data.gust?.format(metric) - - return when { - direction != null && speed != null && gust != null -> stringResources.getString(R.string.station_wind_format_full, direction, speed, gust) - direction != null && speed != null -> stringResources.getString(R.string.station_wind_format_wind_only, direction, speed) - direction != null -> stringResources.getString(R.string.station_wind_format_direction_only, direction) - speed != null -> stringResources.getString(R.string.station_wind_format_speed_only, speed) - else -> null - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailData.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailData.kt deleted file mode 100644 index b8f4474c..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailData.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.inkapplications.ack.android.log.details - -import com.inkapplications.ack.data.CapturedPacket - -/** - * Raw Data required to render the Log Details UI. - */ -data class LogDetailData( - /** - * The packet that is being displayed - */ - val packet: CapturedPacket, - - /** - * Whether to display stats in metric format. - */ - val metric: Boolean = false, - - /** - * Whether debug information should be visible. - */ - val debug: Boolean = false, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsActivity.kt deleted file mode 100644 index dcfd67f2..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsActivity.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.inkapplications.ack.android.log.details - -import android.app.Activity -import android.os.Bundle -import androidx.activity.compose.setContent -import com.inkapplications.ack.android.station.startStationActivity -import com.inkapplications.ack.android.trackNavigation -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.structures.station.Callsign -import com.inkapplications.android.extensions.ExtendedActivity -import com.inkapplications.android.startActivity -import dagger.hilt.android.AndroidEntryPoint -import kimchi.Kimchi - -const val EXTRA_LOG_ID = "aprs.station.extra.id" - -/** - * Shows information about a particular packet received. - */ -@AndroidEntryPoint -class LogDetailsActivity: ExtendedActivity(), LogDetailsController { - private val id get() = CaptureId(intent.getLongExtra(EXTRA_LOG_ID, -1)) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - Kimchi.trackScreen("log_details") - - setContent { - AckScreen { - LogDetailsScreen( - controller = this, - ) - } - } - } - - override fun onMapItemClicked(id: CaptureId?) { - Kimchi.trackEvent("log_details_map_item_click") - Kimchi.debug("Map Item Clicked: No-Op") - } - - override fun onViewStationDetails(station: Callsign) { - Kimchi.trackEvent("log_details_view_station") - startStationActivity(station) - } -} - -/** - * Start an Activity displaying the details for the specified packet. - */ -fun Activity.startLogInspectActivity(packetId: CaptureId) { - Kimchi.trackNavigation("log_inspect") - startActivity(LogDetailsActivity::class) { - putExtra(EXTRA_LOG_ID, packetId.value) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsController.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsController.kt deleted file mode 100644 index 33a3dcdd..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsController.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.inkapplications.ack.android.log.details - -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.structures.station.Callsign - -/** - * Actions available on the log details screen - */ -interface LogDetailsController { - /** - * Invoked when the user taps the up navigation at the top of the screen. - */ - fun onBackPressed() - - /** - * Invoked when the user clicks on the station details button. - */ - fun onViewStationDetails(station: Callsign) - - /** - * Invoked when the user clicks on a map marker. - * - * @param id The id of the map marker that was clicked. - */ - fun onMapItemClicked(id: CaptureId?) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsScreen.kt deleted file mode 100644 index 3bcf3304..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsScreen.kt +++ /dev/null @@ -1,163 +0,0 @@ -package com.inkapplications.ack.android.log.details - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Card -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.map.MarkerMap -import com.inkapplications.ack.android.ui.IconRow -import com.inkapplications.ack.android.ui.NavigationRow -import com.inkapplications.ack.android.ui.TelemetryTable -import com.inkapplications.ack.android.ui.theme.AckTheme - -/** - * Detailed display of the data for a single logged packet. - */ -@Composable -fun LogDetailsScreen( - viewModel: LogDetailsViewModel = hiltViewModel(), - controller: LogDetailsController, -) { - val viewState = viewModel.detailsState.collectAsState().value - - when (viewState) { - is LogDetailsState.Initial -> {} - is LogDetailsState.Loaded -> Details(viewState, controller) - } -} - -@Composable -private fun Details( - viewState: LogDetailsState.Loaded, - controller: LogDetailsController, -) { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - ) { - if (viewState.mapable) { - Column { - Box { - MarkerMap( - viewModel = viewState.mapViewState, - onMapItemClicked = controller::onMapItemClicked, - interactive = false, - modifier = Modifier.aspectRatio(16f / 9f), - ) - Box( - modifier = Modifier - .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) - ) { - IconButton( - onClick = controller::onBackPressed - ) { - Icon(Icons.Default.ArrowBack, stringResource(R.string.navigate_up)) - } - } - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = AckTheme.spacing.gutter) - ) { - Text( - viewState.name, - style = AckTheme.typography.h1, - modifier = Modifier.padding( - start = AckTheme.spacing.gutter, - ), - ) - IconButton( - onClick = { controller.onViewStationDetails(viewState.callsign) }, - ) { - Icon(Icons.Default.Info, stringResource(R.string.log_details_title)) - } - } - } - } else { - Box( - Modifier - .padding( - top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), - ) - .padding( - start = AckTheme.spacing.gutter, - top = AckTheme.spacing.gutter, - end = AckTheme.spacing.gutter, - ) - ) { - NavigationRow( - title = { - Text( - viewState.name, - style = AckTheme.typography.h1, - modifier = Modifier.padding( - start = AckTheme.spacing.gutter, - ), - ) - IconButton( - onClick = { controller.onViewStationDetails(viewState.callsign) }, - ) { - Icon(Icons.Default.Info, stringResource(R.string.log_details_title)) - } - }, - onBackPressed = controller::onBackPressed - ) - } - } - Column( - modifier = Modifier - .padding( - bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - ) - .padding( - top = AckTheme.spacing.content, - start = AckTheme.spacing.gutter, - end = AckTheme.spacing.gutter, - bottom = AckTheme.spacing.gutter, - ), - ) { - IconRow( - icon = viewState.receiveIcon, - viewState.timestamp, - ) - if (viewState.temperature != null) { - IconRow(Icons.Default.WbSunny, viewState.temperature) - } - if (viewState.wind != null) { - IconRow(Icons.Default.Air, viewState.wind) - } - if (viewState.altitude != null) { - IconRow(Icons.Default.Terrain, viewState.altitude) - } - if (viewState.comment != null) { - IconRow(Icons.Default.Comment, viewState.comment) - } - if (viewState.telemetryValues != null) { - TelemetryTable(viewState.telemetryValues, viewState.telemetrySequence) - } - if (viewState.rawSource != null) { - Card(modifier = Modifier - .fillMaxWidth() - .padding(vertical = AckTheme.spacing.content)) { - Column(modifier = Modifier.padding(AckTheme.spacing.content)) { - Text("Debug Info", style = AckTheme.typography.h2) - Text("Raw Data", style = AckTheme.typography.h3) - Text(viewState.rawSource, style = AckTheme.typography.caption) - Spacer(Modifier.height(AckTheme.spacing.content)) - } - } - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsState.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsState.kt deleted file mode 100644 index 8efcd062..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsState.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.inkapplications.ack.android.log.details - -import androidx.compose.ui.graphics.vector.ImageVector -import com.inkapplications.ack.android.maps.MapViewModel -import com.inkapplications.ack.structures.TelemetryValues -import com.inkapplications.ack.structures.station.Callsign - -/** - * Possible states for the Log Details Screen. - */ -sealed interface LogDetailsState { - /** - * Indicates that no data has been loaded yet. - */ - object Initial: LogDetailsState - - /** - * Log data for the packet. - */ - data class Loaded( - val callsign: Callsign, - val name: String, - val receiveIcon: ImageVector, - val receiveIconDescription: String, - val timestamp: String, - val mapable: Boolean, - val mapViewState: MapViewModel, - val comment: String? = null, - val temperature: String? = null, - val wind: String? = null, - val altitude: String? = null, - val rawSource: String? = null, - val telemetryValues: TelemetryValues? = null, - val telemetrySequence: String? = null, - ): LogDetailsState -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsViewModel.kt deleted file mode 100644 index 366576fe..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsViewModel.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.inkapplications.ack.android.log.details - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.inkapplications.ack.android.log.LogEvents -import com.inkapplications.ack.data.CaptureId -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -/** - * Android viewmodel to load state for the log-details screen. - */ -@HiltViewModel -class LogDetailsViewModel @Inject constructor( - logEvents: LogEvents, - savedState: SavedStateHandle, - logDetailsViewStateFactory: LogDetailsViewStateFactory, -): ViewModel() { - private val logId = CaptureId(savedState.get(EXTRA_LOG_ID)!!) - - /** - * Detail state about the packet data. - */ - val detailsState = logEvents.stateEvents(logId) - .map { logDetailsViewStateFactory.create(it) } - .stateIn(viewModelScope, SharingStarted.Eagerly, LogDetailsState.Initial) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactory.kt deleted file mode 100644 index d044baa7..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactory.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.inkapplications.ack.android.log.details - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Bluetooth -import androidx.compose.material.icons.filled.Cloud -import androidx.compose.material.icons.filled.SettingsInputAntenna -import androidx.compose.material.icons.filled.Storage -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.locale.format -import com.inkapplications.ack.android.log.SummaryFactory -import com.inkapplications.ack.android.maps.* -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.data.PacketOrigin -import com.inkapplications.ack.structures.PacketData.TelemetryReport -import com.inkapplications.ack.structures.PacketData.Weather -import com.inkapplications.ack.structures.capabilities.Commented -import com.inkapplications.ack.structures.capabilities.Mapable -import com.inkapplications.ack.structures.capabilities.Report -import com.inkapplications.android.extensions.StringResources -import com.inkapplications.android.extensions.ViewStateFactory -import com.inkapplications.android.extensions.format.DateTimeFormatter -import javax.inject.Inject - -/** - * Convert Log data into the model used to render the view for a specific packet. - */ -class LogDetailsViewStateFactory @Inject constructor( - private val summaryFactory: SummaryFactory, - private val markerViewStateFactory: ViewStateFactory, - private val timeFormatter: DateTimeFormatter, - private val stringResources: StringResources, -) { - fun create(data: LogDetailData): LogDetailsState.Loaded { - val packetData = data.packet.parsed.data - return LogDetailsState.Loaded( - callsign = data.packet.parsed.route.source.callsign, - name = data.packet.parsed.route.source.toString(), - timestamp = timeFormatter.formatTimestamp(data.packet.received) - .let { stringResources.getString(R.string.log_details_received_format, it) }, - comment = (packetData as? Commented)?.comment, - mapable = packetData is Mapable && packetData.coordinates != null, - mapViewState = MapViewModel( - markers = markerViewStateFactory.create(data.packet)?.let { listOf(it) }.orEmpty(), - cameraPosition = (data.packet.parsed.data as? Mapable)?.coordinates - ?.let { MapCameraPosition(it, ZoomLevels.ROADS) } - ?: CameraPositionDefaults.unknownLocation, - ), - temperature = (packetData as? Weather)?.temperature?.format(data.metric), - wind = (packetData as? Weather)?.let { summaryFactory.createWindSummary(it.windData, data.metric) }, - altitude = (packetData as? Report)?.altitude?.format(data.metric), - rawSource = data.packet.raw.decodeToString().takeIf { data.debug }, - telemetryValues = (packetData as? TelemetryReport)?.data, - telemetrySequence = (packetData as? TelemetryReport)?.sequenceId, - receiveIcon = when (data.packet.origin) { - PacketOrigin.AprsIs -> Icons.Default.Cloud - PacketOrigin.Ax25 -> Icons.Default.SettingsInputAntenna - PacketOrigin.Tnc -> Icons.Default.Bluetooth - PacketOrigin.Local -> Icons.Default.Storage - }, - receiveIconDescription = when (data.packet.origin) { - PacketOrigin.Ax25 -> stringResources.getString(R.string.log_icon_ax25) - PacketOrigin.AprsIs -> stringResources.getString(R.string.log_icon_aprs_is) - PacketOrigin.Tnc -> stringResources.getString(R.string.log_icon_tnc) - PacketOrigin.Local -> stringResources.getString(R.string.log_icon_local) - }, - ) - } -} - diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexController.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexController.kt deleted file mode 100644 index 3c46d1f3..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexController.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.inkapplications.ack.android.log.index - -import com.inkapplications.ack.android.log.LogItemViewState - -/** - * Actions invoked from the Log Index screen - */ -interface LogIndexController { - /** - * Invoked when the user clicks a log item in the log list. - */ - fun onLogListItemClick(item: LogItemViewState) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexData.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexData.kt deleted file mode 100644 index b87b0399..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexData.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.inkapplications.ack.android.log.index - -import com.inkapplications.ack.data.CapturedPacket - -/** - * Data required to render the Log Index's view state. - */ -data class LogIndexData( - /** - * Whether units should be displayed in metric - */ - val metric: Boolean, - - /** - * List of packet data to be displayed in the index. - */ - val packets: List, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexScreen.kt deleted file mode 100644 index bca7adcb..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexScreen.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.inkapplications.ack.android.log.index - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Summarize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.log.AprsLogItem -import com.inkapplications.ack.android.log.LogItemViewState -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.android.ui.theme.AckTheme - -/** - * Screen that displays a scrolling list of log items. - */ -@Composable -fun LogIndexScreen( - viewModel: LogIndexViewModel = hiltViewModel(), - controller: LogIndexController, -) = AckScreen { - val state = viewModel.indexState.collectAsState() - - when (val currentState = state.value) { - LogIndexState.Initial -> {} - is LogIndexState.LogList -> LogList(currentState, controller) - is LogIndexState.Empty -> EmptyState() - } -} - -@Composable -private fun EmptyState() { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier - .padding(bottom = AckTheme.spacing.navigationProtection) - .fillMaxSize() - ) { - Icon( - imageVector = Icons.Default.Summarize, - contentDescription = null, - tint = AckTheme.colors.foregroundInactive, - modifier = Modifier.size(AckTheme.sizing.dispayIcon), - ) - Text(stringResource(R.string.log_index_empty)) - } -} - -@Composable -private fun LogList( - state: LogIndexState.LogList, - controller: LogIndexController, -) { - LazyColumn( - contentPadding = PaddingValues(top = AckTheme.spacing.gutter, bottom = AckTheme.spacing.navigationProtection) - ) { - val logItems = state.logs.map { LogListItem.Log(it) } - items(listOf(LogListItem.Header) + logItems) { item -> - when (item) { - LogListItem.Header -> Text( - text = "Packet Log", - style = AckTheme.typography.h1, - modifier = Modifier.padding( - top = AckTheme.spacing.gutter, - start = AckTheme.spacing.gutter, - end = AckTheme.spacing.gutter, - bottom = AckTheme.spacing.content, - ) - ) - is LogListItem.Log -> AprsLogItem(item.log, controller::onLogListItemClick) - } - - } - } -} - -private sealed interface LogListItem { - object Header: LogListItem - data class Log(val log: LogItemViewState): LogListItem -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexState.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexState.kt deleted file mode 100644 index 0777fac4..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.inkapplications.ack.android.log.index - -import com.inkapplications.ack.android.log.LogItemViewState - -/** - * Screen-state representation of the Log item screen. - */ -sealed interface LogIndexState { - /** - * Used when nothing has loaded yet. - */ - object Initial: LogIndexState - - /** - * Used when the list has been loaded, but there are no items to display. - */ - object Empty: LogIndexState - - /** - * Loaded a list of log items to be displayed to the user. - */ - data class LogList(val logs: List): LogIndexState -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexStateFactory.kt deleted file mode 100644 index 3b477590..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexStateFactory.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.inkapplications.ack.android.log.index - -import com.inkapplications.ack.android.log.LogItemViewStateFactory -import dagger.Reusable -import javax.inject.Inject - -/** - * Create Index State objects from log data. - */ -@Reusable -class LogIndexStateFactory @Inject constructor( - private val stateFactory: LogItemViewStateFactory, -) { - fun create(data: LogIndexData): LogIndexState { - return data.packets - .ifEmpty { return LogIndexState.Empty } - .let { stateFactory.create(it, data.metric) } - .let { LogIndexState.LogList(it) } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexViewModel.kt deleted file mode 100644 index 3eb26fb8..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/log/index/LogIndexViewModel.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.inkapplications.ack.android.log.index - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.inkapplications.ack.android.log.LogEvents -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -/** - * Android viewmodel to create and hold the state of the Log Index view data. - */ -@HiltViewModel -class LogIndexViewModel @Inject constructor( - logEvents: LogEvents, - stateFactory: LogIndexStateFactory, -): ViewModel() { - val indexState = logEvents.logIndex - .map { stateFactory.create(it) } - .stateIn(viewModelScope, SharingStarted.Eagerly, LogIndexState.Initial) -} - diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapDataRepository.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapDataRepository.kt deleted file mode 100644 index bf21d8e8..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapDataRepository.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.inkapplications.ack.android.map - -import com.inkapplications.ack.android.locale.LocaleSettings -import com.inkapplications.ack.android.log.CombinedLogItemViewStateFactory -import com.inkapplications.ack.android.log.LogItemViewState -import com.inkapplications.ack.android.maps.MarkerViewState -import com.inkapplications.ack.android.settings.SettingsReadAccess -import com.inkapplications.ack.android.settings.observeBoolean -import com.inkapplications.ack.android.settings.observeInt -import com.inkapplications.ack.android.symbol.SymbolFactory -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.data.PacketStorage -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.coroutines.filterItemNotNull -import com.inkapplications.coroutines.mapItems -import dagger.Reusable -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import javax.inject.Inject - -/** - * Aggregate data needed to display the map. - */ -@Reusable -class MapDataRepository @Inject constructor( - private val logger: KimchiLogger, - private val aprs: PacketStorage, - private val symbolFactory: SymbolFactory, - private val logStateFactory: CombinedLogItemViewStateFactory, - private val settings: SettingsReadAccess, - private val mapSettings: MapSettings, - private val localeSettings: LocaleSettings, -) { - fun findMarkers(): Flow> { - return settings.observeInt(mapSettings.pinCount) - .flatMapLatest { pinCount -> - aprs.findRecent(pinCount.toLong()) - .map { it.distinctBy { it.parsed.route.source } } - .mapItems { packet -> - when (val data = packet.parsed.data) { - is PacketData.Position -> MarkerViewState(packet.id, data.coordinates, symbolFactory.createSymbol(data.symbol)) - else -> null - } - } - .filterItemNotNull() - .onEach { logger.info("New set of ${it.size} map markers.") } - } - } - - fun findLogItem(id: CaptureId): Flow { - return settings.observeBoolean(localeSettings.preferMetric).flatMapLatest { metric -> - aprs.findById(id).map { it?.let { logStateFactory.create(it.id, it.parsed, metric) } } - } - } -} - diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapEvents.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapEvents.kt deleted file mode 100644 index d0fa7a8b..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapEvents.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.inkapplications.ack.android.map - -import android.Manifest.permission.ACCESS_COARSE_LOCATION -import android.content.Context -import android.content.pm.PackageManager.PERMISSION_GRANTED -import androidx.core.content.ContextCompat -import com.inkapplications.ack.android.maps.CameraPositionDefaults -import com.inkapplications.ack.android.maps.MapCameraPosition -import com.inkapplications.ack.android.maps.MapViewModel -import com.inkapplications.ack.android.maps.ZoomLevels -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.android.extensions.location.LocationAccess -import kotlinx.coroutines.flow.* -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Provide access to map events. - */ -@Singleton -class MapEvents @Inject constructor( - private val mapData: MapDataRepository, - location: LocationAccess, - context: Context, -) { - val trackingEnabled = MutableStateFlow(false) - val selectedItemId = MutableStateFlow(null) - private val selectedItem = selectedItemId.flatMapLatest { - it?.let { mapData.findLogItem(it) } ?: flowOf(null) - } - private val lastLocation = if (ContextCompat.checkSelfPermission(context, ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED) { - location.lastKnownLocation?.location - } else null - - - val viewState = combine( - mapData.findMarkers(), - trackingEnabled, - selectedItem, - ) { markers, tracking, selected -> - MapViewState( - MapViewModel( - cameraPosition = (lastLocation ?: markers.firstOrNull()?.coordinates) - ?.let { MapCameraPosition(it, ZoomLevels.RIVERS) } - ?: CameraPositionDefaults.unknownLocation, - markers = markers, - enablePositionTracking = tracking - ), - selectedItem = selected, - ) - }.distinctUntilChanged() -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapModule.kt deleted file mode 100644 index 29b6aa38..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapModule.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.inkapplications.ack.android.map - -import com.inkapplications.ack.android.maps.MarkerViewState -import com.inkapplications.ack.android.settings.SettingsProvider -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.android.extensions.ViewStateFactory -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(SingletonComponent::class) -interface MapModule { - @Binds - @IntoSet - fun settings(mapSettings: MapSettings): SettingsProvider - - @Binds - fun markerViewStateFactory(factory: MarkerViewStateFactory): ViewStateFactory -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapScreen.kt deleted file mode 100644 index edde0b9b..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapScreen.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.inkapplications.ack.android.map - -import androidx.compose.foundation.layout.* -import androidx.compose.material.FloatingActionButton -import androidx.compose.material.Icon -import androidx.compose.material.contentColorFor -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.LocationDisabled -import androidx.compose.material.icons.filled.MyLocation -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.log.AprsLogItem -import com.inkapplications.ack.android.log.LogItemViewState -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.android.ui.theme.AckTheme -import com.inkapplications.ack.data.CaptureId - -@Composable -fun MapScreen( - state: MapViewState, - onMapItemClick: (CaptureId?) -> Unit, - onLogItemClick: (LogItemViewState) -> Unit, - onEnableLocation: () -> Unit, - onDisableLocation: () -> Unit, - bottomContentProtection: Dp, -) = AckScreen { - MarkerMap( - viewModel = state.mapViewModel, - onMapItemClicked = onMapItemClick, - bottomProtection = dimensionResource(R.dimen.mapbox_logo_padding_bottom), - modifier = Modifier.fillMaxSize() - ) - val logState = state.selectedItem - if (state.selectedItemVisible && logState != null) { - Row ( - modifier = Modifier - .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) - .padding(top = AckTheme.spacing.gutter) - ) { - AprsLogItem( - log = logState, - onClick = onLogItemClick, - border = true, - ) - } - } - Box( - modifier = Modifier - .fillMaxSize() - .padding(bottom = bottomContentProtection), - contentAlignment = Alignment.BottomEnd, - ) { - LocationStateButton(state.mapViewModel.enablePositionTracking, onEnableLocation, onDisableLocation) - } -} - -@Composable -fun LocationStateButton( - isTracking: Boolean, - onEnableTrackingClick: () -> Unit, - onDisableTrackingClick: () -> Unit -) { - if (isTracking) { - FloatingActionButton( - onClick = onDisableTrackingClick, - backgroundColor = AckTheme.colors.surface, - contentColor = AckTheme.colors.accent, - modifier = Modifier.padding(AckTheme.spacing.gutter) - ) { - Icon(Icons.Default.MyLocation, stringResource(R.string.map_location_tracking_stop_action)) - } - } else { - FloatingActionButton( - onClick = onEnableTrackingClick, - backgroundColor = AckTheme.colors.surface, - contentColor = contentColorFor(AckTheme.colors.surface), - modifier = Modifier.padding(AckTheme.spacing.gutter) - ) { - Icon(Icons.Default.LocationDisabled, stringResource(R.string.map_location_tracking_start_action)) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapSettings.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapSettings.kt deleted file mode 100644 index 9cbee874..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapSettings.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.inkapplications.ack.android.map - -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.input.IntegerValidator -import com.inkapplications.ack.android.settings.IntSetting -import com.inkapplications.ack.android.settings.Setting -import com.inkapplications.ack.android.settings.SettingsProvider -import com.inkapplications.android.extensions.StringResources -import dagger.Reusable -import javax.inject.Inject - -@Reusable -class MapSettings @Inject constructor( - resources: StringResources -): SettingsProvider { - val pinCount = IntSetting( - key = "map.pins", - name = resources.getString(R.string.map_settings_pins_title), - categoryName = resources.getString(R.string.map_settings_category), - defaultValue = 500, - validator = IntegerValidator( - error = resources.getString(R.string.input_validator_positive_integer_error), - ) - ) - - override val settings: List = listOf(pinCount) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MapViewState.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MapViewState.kt deleted file mode 100644 index 1d9a3a61..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MapViewState.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.inkapplications.ack.android.map - -import com.inkapplications.ack.android.log.LogItemViewState -import com.inkapplications.ack.android.maps.MapViewModel - -/** - * Model the state of the entire map page. - * - * This can't include events that are dependent on the map, which loads - * separately. - */ -data class MapViewState( - val mapViewModel: MapViewModel, - val selectedItem: LogItemViewState? = null, -) { - val selectedItemVisible = selectedItem != null -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerMap.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerMap.kt deleted file mode 100644 index af13da3c..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerMap.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.inkapplications.ack.android.map - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.inkapplications.ack.android.maps.MapViewModel -import com.inkapplications.ack.android.maps.MapsImplementation -import com.inkapplications.ack.data.CaptureId - - -@Composable -fun MarkerMap( - viewModel: MapViewModel, - onMapItemClicked: (CaptureId?) -> Unit, - bottomProtection: Dp = 0.dp, - interactive: Boolean = true, - modifier: Modifier, -) = MapsImplementation.renderMarkerMap( - viewModel = viewModel, - onMapItemClicked = onMapItemClicked, - bottomProtection = 0.dp, - interactive = interactive, - modifier = modifier, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerViewStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerViewStateFactory.kt deleted file mode 100644 index b4df2c53..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/map/MarkerViewStateFactory.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.inkapplications.ack.android.map - -import com.inkapplications.ack.android.maps.MarkerViewState -import com.inkapplications.ack.android.symbol.SymbolFactory -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.capabilities.Mapable -import com.inkapplications.ack.structures.symbolOf -import com.inkapplications.android.extensions.ViewStateFactory -import javax.inject.Inject - -/** - * Convert packet data into the model used to render the marker on a map. - * - * If the packet is not mapable for whatever reason, this will return a - * null marker. - */ -class MarkerViewStateFactory @Inject constructor( - private val symbolFactory: SymbolFactory, -): ViewStateFactory { - private val defaultWeatherSymbol = symbolOf('W', '/') - - override fun create(data: CapturedPacket): MarkerViewState? { - val mapable = data.parsed.data as? Mapable ?: return null - val coordinates = mapable.coordinates ?: return null - val symbol = mapable.symbol - ?: defaultWeatherSymbol.takeIf { data.parsed.data is PacketData.Weather } - ?: return null - val symbolBitmap = symbolFactory.createSymbol(symbol) ?: return null - - return MarkerViewState(data.id, coordinates, symbolBitmap) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardActivity.kt deleted file mode 100644 index 7a878b7f..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardActivity.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.inkapplications.ack.android.onboard - -import androidx.activity.compose.setContent -import androidx.compose.runtime.LaunchedEffect -import androidx.hilt.navigation.compose.hiltViewModel -import com.inkapplications.ack.android.capture.CaptureActivity -import com.inkapplications.ack.android.settings.SettingsAccess -import com.inkapplications.ack.android.settings.license.LicensePromptFieldValues -import com.inkapplications.ack.android.settings.license.LicensePromptValidator -import com.inkapplications.android.extensions.ExtendedActivity -import com.inkapplications.android.startActivity -import dagger.hilt.android.AndroidEntryPoint -import kimchi.Kimchi -import kimchi.analytics.Property -import kotlinx.coroutines.flow.first -import javax.inject.Inject - -/** - * Screen that shows required agreements and setup at first launch. - */ -@AndroidEntryPoint -class OnboardActivity: ExtendedActivity(), UserAgreementController { - @Inject - lateinit var stateAccess: OnboardingStateAccess - - @Inject - lateinit var settingsAccess: SettingsAccess - - @Inject - lateinit var licensePromptValidator: LicensePromptValidator - - override fun onCreate() { - super.onCreate() - Kimchi.trackScreen("onboard") - - setContent { - val viewModel: OnboardingViewModel = hiltViewModel() - - OnboardScreen( - viewModel = viewModel, - userAgreementController = this, - licenseValidator = licensePromptValidator, - onLicenseContinueClick = ::onLicenseContinueClick, - ) - - LaunchedEffect(null) { - viewModel.screenState.first { it is OnboardingState.Complete } - onCompleted() - } - } - } - - private fun onLicenseContinueClick(values: LicensePromptFieldValues) { - Kimchi.trackEvent("onboard_license", listOf( - Property.IntProperty("license_callsign_provided", if (values.callsign.isBlank()) 0 else 1), - Property.IntProperty("license_passcode_provided", if (values.passcode.isBlank()) 0 else 1), - )) - settingsAccess.setLicense(values) - stateAccess.setLicensePromptCompleted() - } - - private fun onCompleted() { - Kimchi.trackEvent("onboard_complete") - startActivity(CaptureActivity::class) - finish() - } - - override fun onTermsAgreeClick() { - Kimchi.trackEvent("onboard_terms_agreement") - stateAccess.setUserAgreed() - } - - override fun onTermsDeclineClick() { - // Intentionally no logging. - finish() - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardScreen.kt deleted file mode 100644 index f3de7c37..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardScreen.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.inkapplications.ack.android.onboard - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import com.inkapplications.ack.android.settings.license.LicensePromptFieldValues -import com.inkapplications.ack.android.settings.license.LicensePromptScreen -import com.inkapplications.ack.android.settings.license.LicensePromptValidator -import com.inkapplications.ack.android.ui.theme.AckScreen - -@Composable -fun OnboardScreen( - viewModel: OnboardingViewModel, - userAgreementController: UserAgreementController, - licenseValidator: LicensePromptValidator, - onLicenseContinueClick: (LicensePromptFieldValues) -> Unit, -) = AckScreen { - val state = viewModel.screenState.collectAsState() - when (val onboardingState = state.value) { - OnboardingState.Complete -> {} - OnboardingState.Initial -> {} - is OnboardingState.LicensePrompt -> LicensePromptScreen( - initialValues = onboardingState.initialValues, - validator = licenseValidator, - onContinue = onLicenseContinueClick, - ) - OnboardingState.UserAgreement -> UsageAgreementPrompt(userAgreementController) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardSettings.kt b/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardSettings.kt deleted file mode 100644 index 1bb74262..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardSettings.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.inkapplications.ack.android.onboard - -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.settings.* -import com.inkapplications.android.extensions.StringResources -import dagger.Reusable -import javax.inject.Inject - -@Reusable -class OnboardSettings @Inject constructor( - resources: StringResources -): SettingsProvider { - val agreementRevision = IntSetting( - key = "onboarding.agreement", - name = resources.getString(R.string.settings_onboard_license), - categoryName = resources.getString(R.string.settings_onboard_category), - defaultValue = 0, - visibility = SettingVisibility.Dev, - ) - val completedLicensePrompt = BooleanSetting( - key = "onboarding.license", - name = resources.getString(R.string.settings_onboard_license), - categoryName = resources.getString(R.string.settings_onboard_category), - defaultValue = false, - visibility = SettingVisibility.Dev, - ) - - override val settings: List = listOf( - agreementRevision, - completedLicensePrompt, - ) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingModule.kt deleted file mode 100644 index fafd8965..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingModule.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.inkapplications.ack.android.onboard - -import com.inkapplications.ack.android.settings.SettingsProvider -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(SingletonComponent::class) -abstract class OnboardingModule { - @Binds - @IntoSet - abstract fun settings(settings: OnboardSettings): SettingsProvider -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingState.kt b/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingState.kt deleted file mode 100644 index df1f5b5e..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingState.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.inkapplications.ack.android.onboard - -import com.inkapplications.ack.android.settings.license.LicensePromptFieldValues - -/** - * Possible states of the onboarding screen. - */ -sealed interface OnboardingState { - /** - * Initial state, before any data is loaded. - */ - object Initial: OnboardingState - - /** - * Screen prompting the user to agree to the usage agreement. - */ - object UserAgreement: OnboardingState - - /** - * Screen prompting the user to enter their callsign information. - */ - class LicensePrompt( - val initialValues: LicensePromptFieldValues, - ): OnboardingState - - /** - * Screen state after all data has been completed. - */ - object Complete: OnboardingState -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingStateAccess.kt b/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingStateAccess.kt deleted file mode 100644 index cdf670ee..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingStateAccess.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.inkapplications.ack.android.onboard - -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.connection.ConnectionSettings -import com.inkapplications.ack.android.settings.* -import com.inkapplications.android.extensions.IntegerResources -import dagger.Reusable -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import javax.inject.Inject - -/** - * Provides information access relevant to onboarding in the application. - */ -@Reusable -class OnboardingStateAccess @Inject constructor( - readSettings: SettingsReadAccess, - private val writeSettings: SettingsWriteAccess, - private val onboardingSettings: OnboardSettings, - connectionSettings: ConnectionSettings, - integers: IntegerResources, -) { - private val latestRevision = integers.getInteger(R.integer.usage_revision) - private val agreementRevision = readSettings.observeInt(onboardingSettings.agreementRevision) - private val completedLicensePrompt = readSettings.observeBoolean(onboardingSettings.completedLicensePrompt) - private val address = readSettings.observeData(connectionSettings.address) - private val passcode = readSettings.observeData(connectionSettings.passcode) - - /** - * Data needed to render the onboarding screens. - */ - val onboardingData = agreementRevision - .map { agreed -> - OnboardingStateFactory.OnboardingData( - latestRevision = latestRevision, - agreementRevision = agreed, - ) - } - .combine(completedLicensePrompt) { data, completedLicense -> - data.copy(completedLicense = completedLicense) - } - .combine(address) { data, address -> - data.copy(currentAddress = address) - } - .combine(passcode) { data, passcode -> - data.copy(currentPasscode = passcode) - } - - /** - * Whether onboarding is considered completed. - */ - val finished = combine(agreementRevision, completedLicensePrompt) { agreement, licenseComplete -> - agreement == latestRevision && licenseComplete - } - - /** - * Update the latest policy agreement to the current revision. - */ - fun setUserAgreed() { - writeSettings.setInt(onboardingSettings.agreementRevision, latestRevision) - } - - /** - * Set the license prompt flag as completed. - */ - fun setLicensePromptCompleted() { - writeSettings.setBoolean(onboardingSettings.completedLicensePrompt, true) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingStateFactory.kt deleted file mode 100644 index d5b2e3ca..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingStateFactory.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.inkapplications.ack.android.onboard - -import com.inkapplications.ack.android.settings.Passcode -import com.inkapplications.ack.android.settings.license.LicensePromptFieldValues -import com.inkapplications.ack.structures.station.StationAddress -import dagger.Reusable -import javax.inject.Inject - -/** - * Create onboarding state objects from the current data. - */ -@Reusable -class OnboardingStateFactory @Inject constructor() { - fun screenState(data: OnboardingData): OnboardingState { - return when { - data.agreementRevision != data.latestRevision -> OnboardingState.UserAgreement - !data.completedLicense -> OnboardingState.LicensePrompt( - initialValues = LicensePromptFieldValues( - callsign = data.currentAddress?.toString().orEmpty(), - passcode = data.currentPasscode?.value?.toString().orEmpty(), - ) - ) - else -> OnboardingState.Complete - } - } - - /** - * Data required to render the onboarding screens. - */ - data class OnboardingData( - val latestRevision: Int, - val agreementRevision: Int? = null, - val completedLicense: Boolean = false, - val currentAddress: StationAddress? = null, - val currentPasscode: Passcode? = null, - ) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingViewModel.kt deleted file mode 100644 index 81543470..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/onboard/OnboardingViewModel.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.inkapplications.ack.android.onboard - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -/** - * Android viewmodel containing the current state of an onboarding screen. - */ -@HiltViewModel -class OnboardingViewModel @Inject constructor( - stateAccess: OnboardingStateAccess, - onboardingStateFactory: OnboardingStateFactory, -): ViewModel() { - val screenState = stateAccess.onboardingData - .map { onboardingStateFactory.screenState(it) } - .stateIn(viewModelScope, SharingStarted.Eagerly, OnboardingState.Initial) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/onboard/UsageAgreementPrompt.kt b/android-application/src/main/java/com/inkapplications/ack/android/onboard/UsageAgreementPrompt.kt deleted file mode 100644 index dccef758..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/onboard/UsageAgreementPrompt.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.inkapplications.ack.android.onboard - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.settings.agreement.UsageAgreement -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun UsageAgreementPrompt( - controller: UserAgreementController, -) { - Column( - modifier = Modifier - .padding( - top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), - bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - ) - .padding(AckTheme.spacing.gutter) - .fillMaxHeight() - .verticalScroll(rememberScrollState()), - ) { - Text( - stringResource(R.string.usage_title), - style = AckTheme.typography.h1, - ) - Spacer(Modifier.height(AckTheme.spacing.content)) - UsageAgreement() - Spacer(Modifier.weight(1f).defaultMinSize(minHeight = AckTheme.spacing.content)) - Button( - onClick = controller::onTermsDeclineClick, - colors = ButtonDefaults.outlinedButtonColors(), - modifier = Modifier.fillMaxWidth().padding(top = AckTheme.spacing.content), - ) { - Text(stringResource(R.string.usage_decline_action)) - } - Spacer(Modifier.height(AckTheme.spacing.item)) - Button( - onClick = controller::onTermsAgreeClick, - modifier = Modifier.fillMaxWidth(), - ) { - Text(stringResource(R.string.usage_agree_action)) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/onboard/UserAgreementController.kt b/android-application/src/main/java/com/inkapplications/ack/android/onboard/UserAgreementController.kt deleted file mode 100644 index 0e7f6530..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/onboard/UserAgreementController.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.inkapplications.ack.android.onboard - -interface UserAgreementController { - fun onTermsAgreeClick() - fun onTermsDeclineClick() -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/CompositeSettingsProvider.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/CompositeSettingsProvider.kt deleted file mode 100644 index 15e49567..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/CompositeSettingsProvider.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.inkapplications.ack.android.settings - -import dagger.Reusable -import javax.inject.Inject - -/** - * Combines all the settings providers in the application into a single provider. - */ -@Reusable -class CompositeSettingsProvider @Inject constructor( - providers: @JvmSuppressWildcards Set -): SettingsProvider { - override val settings: List = providers.map { it.settings }.flatten() -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/LicenseData.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/LicenseData.kt deleted file mode 100644 index 9c8a039f..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/LicenseData.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.inkapplications.ack.android.settings - -import com.inkapplications.ack.structures.station.StationAddress - -/** - * Station Address + Passcode data pair. - */ -data class LicenseData( - val address: StationAddress? = null, - val passcode: Passcode? = null, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/LicenseViewState.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/LicenseViewState.kt deleted file mode 100644 index 99390a8f..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/LicenseViewState.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.inkapplications.ack.android.settings - -/** - * Models the state the user's license information displayed in settings. - */ -sealed interface LicenseViewState { - /** - * The initial state before data is loaded. - */ - object Initial: LicenseViewState - - /** - * Indicates that the user has not set a callsign or passcode. - */ - object Unconfigured: LicenseViewState - - /** - * Indicates that the user has a callsign for their license configured. - */ - sealed interface CallsignConfigured: LicenseViewState { - /** - * Readable Callsign to display on the screen. - */ - val callsign: String - } - - /** - * Indicates that the user has configured both a callsign, but not a passcode. - */ - data class Registered(override val callsign: String): CallsignConfigured - - /** - * Indicates that the user has configured both a callsign and passcode. - */ - data class Verified(override val callsign: String): CallsignConfigured -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/NullSettingsReadAccesss.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/NullSettingsReadAccesss.kt deleted file mode 100644 index 3802fc20..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/NullSettingsReadAccesss.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.inkapplications.ack.android.settings - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -/** - * Read access to settings that always returns a null result. - * - * This can be used for testing or as a stand-in for disabled services. - */ -object NullSettingsReadAccesss: SettingsReadAccess { - override fun observeStringState(setting: StringSetting): Flow = flowOf(null) - override fun observeIntState(setting: IntSetting): Flow = flowOf(null) - override fun observeBooleanState(setting: BooleanSetting): Flow = flowOf(null) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/Passcode.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/Passcode.kt deleted file mode 100644 index b526ff62..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/Passcode.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.inkapplications.ack.android.settings - -/** - * Wrapping class for the user's license passcode. - */ -@JvmInline -value class Passcode(val value: Int) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/PrioritySettingValues.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/PrioritySettingValues.kt deleted file mode 100644 index fe0ecb1f..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/PrioritySettingValues.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.inkapplications.ack.android.settings - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine - -/** - * Provides read access to a collection of settings provider by taking the first one with a - * successful result. - */ -class PrioritySettingValues(private vararg val delegates: SettingsReadAccess): SettingsReadAccess { - override fun observeStringState(setting: StringSetting): Flow { - return delegates.map { it.observeStringState(setting) }.flattenFirst { it != null } - } - - override fun observeIntState(setting: IntSetting): Flow { - return delegates.map { it.observeIntState(setting) }.flattenFirst { it != null } - } - - override fun observeBooleanState(setting: BooleanSetting): Flow { - return delegates.map { it.observeBooleanState(setting) }.flattenFirst { it != null } - } - - /** - * Merge a list of flows taking the first flow's latest result that matches a [predicate] - */ - private inline fun List>.flattenFirst(crossinline predicate: (T) -> Boolean): Flow = combine(*this.toTypedArray()) { - it.firstOrNull { predicate(it) } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/Setting.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/Setting.kt deleted file mode 100644 index 2a6cc003..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/Setting.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.inkapplications.ack.android.settings - -import com.inkapplications.ack.android.input.NoValidation -import com.inkapplications.ack.android.input.ValidationResult -import com.inkapplications.ack.android.input.Validator -import com.inkapplications.ack.android.settings.transformer.IntFieldTransformer -import com.inkapplications.ack.android.settings.transformer.Transformer - -/** - * Definition of an individual configuration aspect of the application. - */ -sealed interface Setting { - /** - * A Unique Identifier for the setting, used for storage. - */ - val key: String - - /** - * A user-readable name for the setting. - */ - val name: String - - /** - * A user-readable name for the category of the setting. - */ - val categoryName: String - - /** - * The value that should be used if no provider has overridden it. - */ - val defaultValue: Any - - /** - * Identifies a setting that should be hidden from the user. - */ - @Deprecated("Use visibility instead", ReplaceWith("visibility == Visibility.Advanced")) - val advanced: Boolean get() { return visibility == SettingVisibility.Advanced } - - /** - * An object to check whether an input value can be allowed for this setting. - */ - val validator: Validator<*> - - /** - * How/when to display the setting in the UI. - */ - val visibility: SettingVisibility -} - -/** - * Plain string setting. - */ -open class StringSetting( - override val key: String, - override val name: String, - override val categoryName: String, - override val defaultValue: String, - override val visibility: SettingVisibility = SettingVisibility.Visible, - override val validator: Validator = NoValidation, -): Setting - -/** - * A plain number setting. - */ -open class IntSetting( - override val key: String, - override val name: String, - override val categoryName: String, - override val defaultValue: Int, - override val visibility: SettingVisibility = SettingVisibility.Visible, - override val validator: Validator = NoValidation, -): Setting - -/** - * A plain boolean setting. - */ -data class BooleanSetting( - override val key: String, - override val name: String, - override val categoryName: String, - override val defaultValue: Boolean, - override val visibility: SettingVisibility = SettingVisibility.Visible, - override val validator: Validator = NoValidation, -): Setting - -/** - * Complex data type that is based on a [StringSetting] for storage. - * - * @param storageTransformer Used to convert to/from the data structure and a string. - * @param inputValidator Used to validate plain input from the user. - * @param dataValidator Used to validate input from the user, but after it has been transformed by the [storageTransformer]. - */ -class StringBackedSetting( - key: String, - name: String, - categoryName: String, - override val defaultData: T, - override val visibility: SettingVisibility = SettingVisibility.Visible, - override val storageTransformer: Transformer, - val inputValidator: Validator = NoValidation, - val dataValidator: Validator = NoValidation, -): StringSetting( - key = key, - name = name, - categoryName = categoryName, - defaultValue = storageTransformer.toStorage(defaultData), - visibility = visibility, - validator = object: Validator { - override fun validate(input: String): ValidationResult { - val inputResult = inputValidator.validate(input) - if (inputResult !is ValidationResult.Valid) return inputResult - return storageTransformer.toData(input).run(dataValidator::validate) - } - } -), TransformableSetting - -/** - * Complex data type that is based on a [IntSetting] for storage. - * - * @param storageTransformer Used to convert to/from the data structure and an integer. - * @param inputValidator Used to validate plain input from the user. - * @param dataValidator Used to validate input from the user, but after it has been transformed by the [storageTransformer]. - */ -class IntBackedSetting( - key: String, - name: String, - categoryName: String, - override val defaultData: T, - override val visibility: SettingVisibility = SettingVisibility.Visible, - override val storageTransformer: Transformer, - val inputValidator: Validator = NoValidation, - val dataValidator: Validator = NoValidation, - val fieldTransformer: Transformer = IntFieldTransformer, -): IntSetting( - key = key, - name = name, - categoryName = categoryName, - defaultValue = storageTransformer.toStorage(defaultData), - visibility = visibility, - validator = object: Validator { - override fun validate(input: Int): ValidationResult { - val inputResult = inputValidator.validate(input) - if (inputResult !is ValidationResult.Valid) return inputResult - return storageTransformer.toData(input).run(dataValidator::validate) - } - } -), TransformableSetting diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingRows.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingRows.kt deleted file mode 100644 index e8630aaa..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingRows.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.inkapplications.ack.android.settings - -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.Switch -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.symbol.SymbolSelectorViewModel -import com.inkapplications.ack.android.ui.theme.AckTheme -import com.inkapplications.ack.structures.Symbol -import com.inkapplications.ack.structures.symbolOf - -@Composable -fun IntStateRow( - state: SettingState.IntState, - onClick: () -> Unit -) = Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable(onClick = onClick) - .padding(vertical = AckTheme.spacing.clickSafety, horizontal = AckTheme.spacing.gutter) -) { - Column { - WarningLabel(state.setting) - Text(state.setting.name, fontWeight = FontWeight.Bold) - } - Spacer(Modifier.weight(1f)) - Text(state.value.toString(), style = AckTheme.typography.caption) -} - -@Composable -fun StringStateRow( - state: SettingState.StringState, - onClick: () -> Unit -) = Column( - modifier = Modifier - .clickable(onClick = onClick) - .fillMaxWidth() - .padding(vertical = AckTheme.spacing.clickSafety, horizontal = AckTheme.spacing.gutter) -) { - WarningLabel(state.setting) - Text(state.setting.name, fontWeight = FontWeight.Bold) - Text(state.value, style = AckTheme.typography.caption) -} - -@Composable -fun BooleanStateRow( - state: SettingState.BooleanState, - onChange: (Boolean) -> Unit, -) = Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable { onChange(!state.value) } - .padding(vertical = AckTheme.spacing.clickSafety, horizontal = AckTheme.spacing.gutter) -) { - Column { - WarningLabel(state.setting) - Text(state.setting.name, fontWeight = FontWeight.Bold) - } - Spacer(Modifier.weight(1f)) - Switch( - checked = state.value, - onCheckedChange = { checked -> - onChange(checked) - } - ) -} - -/** - * APRS Symbol selection setting state. - */ -@Composable -fun SymbolStateRow( - state: SettingState.StringState, - setting: StringBackedSetting, - symbolViewModel: SymbolSelectorViewModel = hiltViewModel(), - onClick: () -> Unit, -) = Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable(onClick = onClick) - .padding(vertical = AckTheme.spacing.clickSafety, horizontal = AckTheme.spacing.gutter) -) { - Column { - WarningLabel(setting) - Text(setting.name, fontWeight = FontWeight.Bold) - } - Spacer(Modifier.weight(1f)) - - val bitmap = symbolViewModel.createBitmap(symbolOf(state.value)) - if (bitmap != null) { - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = null, - modifier = Modifier - .width(24.dp) - .height(24.dp), - ) - } -} - -/** - * Warning label for advanced or dev settings. - */ -@Composable -private fun WarningLabel(setting: Setting) { - when (setting.visibility) { - SettingVisibility.Advanced -> Text( - text = stringResource(R.string.settings_advanced_label), - color = AckTheme.colors.warnForeground, - style = AckTheme.typography.caption, - ) - SettingVisibility.Dev -> Text( - text = stringResource(R.string.settings_dev_label), - color = AckTheme.colors.dangerForeground, - style = AckTheme.typography.caption, - ) - else -> {} - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingState.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingState.kt deleted file mode 100644 index adad311a..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingState.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.inkapplications.ack.android.settings - -sealed interface SettingState { - val setting: Setting - - data class IntState( - override val setting: IntSetting, - val value: Int, - ): SettingState - - data class StringState( - override val setting: StringSetting, - val value: String, - ): SettingState - - data class BooleanState( - override val setting: BooleanSetting, - val value: Boolean, - ): SettingState -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingVisibility.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingVisibility.kt deleted file mode 100644 index 400eed85..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingVisibility.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.inkapplications.ack.android.settings - -/** - * How/when to display a setting. - */ -enum class SettingVisibility { - /** - * Displayed to the user normally in the UI. - */ - Visible, - - /** - * Displayed only if the user chooses to display advanced settings. - */ - Advanced, - - /** - * Displayed only as a partially hidden developer setting. - */ - Dev, -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsAccess.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsAccess.kt deleted file mode 100644 index c50d496d..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsAccess.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.inkapplications.ack.android.settings - -import com.inkapplications.ack.android.connection.ConnectionSettings -import com.inkapplications.ack.android.settings.license.LicensePromptFieldValues -import com.inkapplications.ack.structures.station.toStationAddress -import com.inkapplications.coroutines.mapItems -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map -import javax.inject.Inject - -/** - * Provide the settings screen with application-wide settings info. - */ -class SettingsAccess @Inject constructor( - private val settingsProvider: SettingsProvider, - private val settingValues: SettingsReadAccess, - private val settingsStorage: SettingsWriteAccess, - private val connectionSettings: ConnectionSettings, -) { - fun settingsGroups(visibility: SettingVisibility): Flow { - return settingsProvider.settings - .filter { it.visibility <= visibility } - .groupBy { it.categoryName } - .map { (key, settings) -> - settings.map { setting -> - when (setting) { - is StringSetting -> settingValues.observeString(setting) - .map { SettingState.StringState(setting, it) } - is IntSetting -> settingValues.observeInt(setting) - .map { SettingState.IntState(setting, it) } - is BooleanSetting -> settingValues.observeBoolean(setting) - .map { SettingState.BooleanState(setting, it) } - } - }.let { - combine(*it.toTypedArray()) { it.toList() } - } .map { - SettingsGroup(key, it) - } - } - .let { - combine(*it.toTypedArray()) { it.toList() } - } - .mapItems { - it.copy(settings = it.settings.sortedBy { it.setting.name }) - } - .map { - it.sortedBy { it.name } - } - .map { - SettingsListData().apply { - this.settings = it - this.visibility = visibility - } - } - } - - val licenseData = settingValues.observeData(connectionSettings.address) - .combine(settingValues.observeData(connectionSettings.passcode)) { callsign, passcode -> - LicenseData(callsign, passcode) - } - - val licensePromptFieldValues: Flow = settingValues.observeData(connectionSettings.address) - .combine(settingValues.observeData(connectionSettings.passcode)) { callsign, passcode -> - LicensePromptFieldValues(callsign?.toString().orEmpty(), passcode?.value?.toString().orEmpty()) - } - - fun setLicense(values: LicensePromptFieldValues) { - settingsStorage.setData(connectionSettings.address, values.callsign.trim().takeIf { it.isNotEmpty() }?.toStationAddress()) - settingsStorage.setData(connectionSettings.passcode, Passcode(values.passcode.trim().toIntOrNull() ?: -1)) - } - - fun updateInt(key: String, value: Int) { - val setting = settingsProvider.settings.find { it.key == key } as? IntSetting - ?: throw IllegalArgumentException("Unknown Int Setting: <$key>") - settingsStorage.setInt(setting, value) - } - - fun updateString(key: String, value: String) { - val setting = settingsProvider.settings.find { it.key == key } as? StringSetting - ?: throw IllegalArgumentException("Unknown String Setting: <$key>") - settingsStorage.setString(setting, value) - } - - fun updateBoolean(key: String, value: Boolean) { - val setting = settingsProvider.settings.find { it.key == key } as? BooleanSetting - ?: throw IllegalArgumentException("Unknown Boolean Setting: <$key>") - settingsStorage.setBoolean(setting, value) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsActivity.kt deleted file mode 100644 index 198d18b5..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsActivity.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.inkapplications.ack.android.settings - -import androidx.activity.compose.setContent -import androidx.activity.viewModels -import com.google.android.gms.oss.licenses.OssLicensesMenuActivity -import com.inkapplications.ack.android.settings.agreement.UserAgreementActivity -import com.inkapplications.ack.android.settings.license.LicenseEditActivity -import com.inkapplications.android.extensions.ExtendedActivity -import com.inkapplications.android.startActivity -import dagger.hilt.android.AndroidEntryPoint -import kimchi.Kimchi -import kimchi.analytics.intProperty -import kimchi.analytics.stringProperty -import javax.inject.Inject - -@AndroidEntryPoint -class SettingsActivity: ExtendedActivity(), SettingsController { - @Inject - lateinit var settingsAccess: SettingsAccess - - private val viewModel: SettingsViewModel by viewModels() - - override fun onCreate() { - super.onCreate() - Kimchi.trackScreen("settings") - - setContent { - SettingsScreen( - viewModel = viewModel, - controller = this, - ) - } - } - - override fun onVersionLongPress() { - Kimchi.trackEvent("settings_show_advanced") - viewModel.showDev() - } - - override fun onCallsignEditClick() { - Kimchi.trackEvent("settings_license_edit") - startActivity(LicenseEditActivity::class) - } - - override fun onIntSettingChanged(state: SettingState.IntState, newValue: Int) { - Kimchi.trackEvent("settings_change", listOf( - stringProperty("setting", state.setting.key), - intProperty("value", newValue) - )) - settingsAccess.updateInt(state.setting.key, newValue) - } - - override fun onStringSettingChanged(state: SettingState.StringState, newValue: String) { - Kimchi.trackEvent("settings_change", listOf( - stringProperty("setting", state.setting.key), - stringProperty("value", newValue) - )) - settingsAccess.updateString(state.setting.key, newValue) - } - - override fun onSwitchSettingChanged(state: SettingState.BooleanState, newState: Boolean) { - Kimchi.trackEvent("settings_change", listOf( - stringProperty("setting", state.setting.key), - stringProperty("value", newState.toString()) - )) - settingsAccess.updateBoolean(state.setting.key, newState) - } - - override fun onAckLicenseClick() { - Kimchi.trackEvent("settings_self_license") - startActivity(UserAgreementActivity::class) - } - - override fun onLicensesClick() { - Kimchi.trackEvent("settings_show_licenses") - startActivity(OssLicensesMenuActivity::class) - } - - override fun onAdvancedChanged(checked: Boolean) { - Kimchi.trackEvent("settings_advanced", listOf( - intProperty("enabled", if (checked) 1 else 0) - )) - viewModel.showAdvanced(checked) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsController.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsController.kt deleted file mode 100644 index 353c1f78..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsController.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.inkapplications.ack.android.settings - -interface SettingsController { - fun onIntSettingChanged(state: SettingState.IntState, newValue: Int) - fun onStringSettingChanged(state: SettingState.StringState, newValue: String) - fun onSwitchSettingChanged(state: SettingState.BooleanState, newState: Boolean) - fun onVersionLongPress() - fun onBackPressed() - fun onCallsignEditClick() - fun onAckLicenseClick() - fun onLicensesClick() - fun onAdvancedChanged(checked: Boolean) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsGroup.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsGroup.kt deleted file mode 100644 index ef933cf1..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsGroup.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.inkapplications.ack.android.settings - -data class SettingsGroup(val name: String, val settings: List) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsListData.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsListData.kt deleted file mode 100644 index 15cac1a9..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsListData.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.inkapplications.ack.android.settings - -/** - * Data required to display the list of settings on the settings screen. - */ -class SettingsListData { - lateinit var settings: List - lateinit var visibility: SettingVisibility -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsListViewState.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsListViewState.kt deleted file mode 100644 index c6efff89..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsListViewState.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.inkapplications.ack.android.settings - -/** - * Models the state of the list of settings within the settings screen. - */ -sealed interface SettingsListViewState { - /** - * The initial state of the settings screen before data is loaded. - */ - object Initial: SettingsListViewState - - /** - * The state of the settings screen after data is loaded. - */ - data class Loaded( - val settingsList: List, - val advancedVisible: Boolean, - ): SettingsListViewState -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsModule.kt deleted file mode 100644 index 1a0c56a7..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsModule.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.inkapplications.ack.android.settings - -import com.inkapplications.ack.android.BuildConfig -import com.inkapplications.ack.android.firebase.FirebaseSettings -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.Reusable -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.Multibinds - -@Module(includes = [StaticSettingsModule::class]) -@InstallIn(SingletonComponent::class) -class SettingsModule { - @Provides - @Reusable - fun settingsProvider( - sharedPreferences: SharedPreferenceSettings, - firebaseSettings: FirebaseSettings - ): SettingsReadAccess { - val readProviders = if (BuildConfig.USE_GOOGLE_SERVICES) { - arrayOf(sharedPreferences, firebaseSettings) - } else { - arrayOf(sharedPreferences) - } - - return PrioritySettingValues(*readProviders) - } -} - -@Module -abstract class StaticSettingsModule { - @Binds - @Reusable - abstract fun settingsStorage(preferences: SharedPreferenceSettings): SettingsWriteAccess - - @Multibinds - abstract fun settingsProviders(): @JvmSuppressWildcards Set - - @Binds - @Reusable - abstract fun settingsProvider(provider: CompositeSettingsProvider): SettingsProvider -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsProvider.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsProvider.kt deleted file mode 100644 index 5daeb577..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsProvider.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.inkapplications.ack.android.settings - -/** - * Contributor to the list of application-wide available settings. - */ -interface SettingsProvider { - val settings: List -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsReadAccess.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsReadAccess.kt deleted file mode 100644 index 67e9b7a4..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsReadAccess.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.inkapplications.ack.android.settings - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -/** - * Provides access to key/value primitive preferences for the application. - */ -interface SettingsReadAccess { - fun observeStringState(setting: StringSetting): Flow - fun observeIntState(setting: IntSetting): Flow - fun observeBooleanState(setting: BooleanSetting): Flow -} - -fun SettingsReadAccess.observeString(setting: StringSetting): Flow { - return observeStringState(setting).map { it ?: setting.defaultValue } -} - -fun SettingsReadAccess.observeInt(setting: IntSetting): Flow { - return observeIntState(setting).map { it ?: setting.defaultValue } -} - -fun SettingsReadAccess.observeBoolean(setting: BooleanSetting): Flow { - return observeBooleanState(setting).map { it ?: setting.defaultValue } -} - -/** - * Observes the value of a setting after being transformed into its data type. - */ -fun SettingsReadAccess.observeData(setting: TransformableSetting): Flow { - return when (setting) { - is IntBackedSetting -> observeInt(setting).map(setting.storageTransformer::toData) - is StringBackedSetting -> observeString(setting).map(setting.storageTransformer::toData) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsScreen.kt deleted file mode 100644 index 4e3bcf7c..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsScreen.kt +++ /dev/null @@ -1,197 +0,0 @@ -package com.inkapplications.ack.android.settings - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.input.IntPrompt -import com.inkapplications.ack.android.input.StringPrompt -import com.inkapplications.ack.android.settings.buildinfo.BuildInfo -import com.inkapplications.ack.android.symbol.SymbolPrompt -import com.inkapplications.ack.android.ui.CallsignChip -import com.inkapplications.ack.android.ui.NavigationRow -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.android.ui.theme.AckTheme -import com.inkapplications.ack.structures.Symbol -import com.inkapplications.ack.structures.code - -@Composable -fun SettingsScreen( - viewModel: SettingsViewModel, - controller: SettingsController, -) = AckScreen { - Column( - modifier = Modifier - .fillMaxSize() - .padding( - top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), - bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - ) - .verticalScroll(rememberScrollState()), - ) { - NavigationRow( - title = stringResource(R.string.settings_title), - onBackPressed = controller::onBackPressed, - ) - LicenseRow(viewModel.licenseState.collectAsState(), controller) - val promptSetting = remember { mutableStateOf(null) } - val settingState = promptSetting.value - when { - settingState is SettingState.StringState && settingState.setting is StringBackedSetting<*> && settingState.setting.defaultData is Symbol -> { - val symbolSetting = settingState.setting as StringBackedSetting - SymbolPrompt( - title = symbolSetting.name, - value = symbolSetting.storageTransformer.toData(settingState.value), - onDismiss = { promptSetting.value = null }, - onSubmit = { - if (it != null) { - controller.onStringSettingChanged(settingState, it.code) - } - promptSetting.value = null - } - ) - } - settingState is SettingState.IntState -> IntPrompt( - title = settingState.setting.name, - value = settingState.value, - validator = settingState.setting.validator, - onDismiss = { promptSetting.value = null }, - onSubmit = { - controller.onIntSettingChanged(settingState, it) - promptSetting.value = null - } - ) - settingState is SettingState.StringState -> StringPrompt( - title = settingState.setting.name, - value = settingState.value, - validator = settingState.setting.validator, - onDismiss = { promptSetting.value = null }, - onSubmit = { - controller.onStringSettingChanged(settingState, it) - promptSetting.value = null - } - ) - else -> {} - } - - when (val listState = viewModel.settingsList.collectAsState().value) { - SettingsListViewState.Initial -> {} - is SettingsListViewState.Loaded -> { - listState.settingsList.forEach { - SettingsCard(group = it, controller = controller, promptSetting = promptSetting) - } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = stringResource(R.string.settings_advanced_toggle_label), - style = AckTheme.typography.caption, - modifier = Modifier.padding(end = AckTheme.spacing.item) - ) - Switch( - checked = listState.advancedVisible, - onCheckedChange = controller::onAdvancedChanged, - ) - } - } - } - - Spacer(Modifier - .weight(1f) - .defaultMinSize(minHeight = AckTheme.spacing.content)) - - BuildInfo( - buildInfo = viewModel.buildInfoState.collectAsState().value, - onVersionLongPress = controller::onVersionLongPress, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) - - Text( - text = stringResource(R.string.settings_author_line), - style = AckTheme.typography.caption, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - Spacer(Modifier.height(AckTheme.spacing.content)) - TextButton(controller::onAckLicenseClick, modifier = Modifier.align(Alignment.CenterHorizontally)) { - Text(stringResource(R.string.settings_ack_license)) - } - TextButton(controller::onLicensesClick, modifier = Modifier.align(Alignment.CenterHorizontally)) { - Text(stringResource(R.string.settings_licenses)) - } - } -} - -@Composable -private fun LicenseRow( - state: State, - controller: SettingsController, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = AckTheme.spacing.gutter, vertical = AckTheme.spacing.content), - ) { - when (val licenseState = state.value) { - LicenseViewState.Initial -> {} - LicenseViewState.Unconfigured -> Button(onClick = controller::onCallsignEditClick) { - Text("Add Callsign") - } - is LicenseViewState.CallsignConfigured -> CallsignChip( - licenseState.callsign, - licenseState is LicenseViewState.Verified, - controller::onCallsignEditClick - ) - } - } -} - -@Composable -private fun SettingsCard( - group: SettingsGroup, - controller: SettingsController, - promptSetting: MutableState, -) { - Card(modifier = Modifier.padding(vertical = AckTheme.spacing.item)) { - Column { - SettingsCategoryRow(group.name) - group.settings.forEach { item -> - val setting = item.setting - when { - item is SettingState.StringState && setting is StringBackedSetting<*> && setting.defaultData is Symbol -> { - val symbolSetting = setting as StringBackedSetting - SymbolStateRow(item, symbolSetting) { - promptSetting.value = item - } - } - item is SettingState.BooleanState -> BooleanStateRow(item) { - controller.onSwitchSettingChanged(item, it) - } - item is SettingState.IntState -> IntStateRow(item) { - promptSetting.value = item - } - item is SettingState.StringState -> StringStateRow(item) { - promptSetting.value = item - } - } - } - } - } -} - -@Composable -fun SettingsCategoryRow(name: String) = Row( - Modifier.padding(horizontal = AckTheme.spacing.gutter, vertical = AckTheme.spacing.item) -) { - Text(name, style = AckTheme.typography.h2, modifier = Modifier.padding(vertical = AckTheme.spacing.item)) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsViewModel.kt deleted file mode 100644 index 32398e5f..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsViewModel.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.inkapplications.ack.android.settings - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.inkapplications.ack.android.settings.buildinfo.BuildDataAccess -import com.inkapplications.ack.android.settings.buildinfo.BuildInfoFactory -import com.inkapplications.ack.android.settings.buildinfo.BuildInfoState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -/** - * Android Viewmodel for storing the state of the settings screen. - */ -@OptIn(ExperimentalCoroutinesApi::class) -@HiltViewModel -class SettingsViewModel @Inject constructor( - settingsAccess: SettingsAccess, - settingsViewStateFactory: SettingsViewStateFactory, - buildDataAccess: BuildDataAccess, - buildInfoFactory: BuildInfoFactory, -): ViewModel() { - private val visibility = MutableStateFlow(SettingVisibility.Visible) - - val settingsList = visibility.flatMapLatest { visibility -> - settingsAccess.settingsGroups(visibility) - }.map { - settingsViewStateFactory.viewState(it) - }.stateIn(viewModelScope, SharingStarted.Eagerly, SettingsListViewState.Initial) - - val licenseState = settingsAccess.licenseData - .map { settingsViewStateFactory.licenseState(it) } - .stateIn(viewModelScope, SharingStarted.Eagerly, LicenseViewState.Initial) - - val buildInfoState: StateFlow = buildDataAccess.buildData - .map { buildInfoFactory.buildInfo(it) } - .stateIn(viewModelScope, SharingStarted.Eagerly, BuildInfoState.Initial) - - fun showAdvanced(value: Boolean) { - visibility.value = if (value) { - SettingVisibility.Advanced - } else { - SettingVisibility.Visible - } - } - - fun showDev() { - visibility.getAndUpdate { - if (it == SettingVisibility.Advanced) SettingVisibility.Dev else it - } - } -} - diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsViewStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsViewStateFactory.kt deleted file mode 100644 index 56f166a1..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsViewStateFactory.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.inkapplications.ack.android.settings - -import dagger.Reusable -import javax.inject.Inject - -/** - * Creates view state objects based on settings data. - */ -@Reusable -class SettingsViewStateFactory @Inject constructor() { - /** - * Create a view state object based on the user's current license data. - */ - fun licenseState(data: LicenseData): LicenseViewState { - return when { - data.address == null -> LicenseViewState.Unconfigured - data.passcode == null -> LicenseViewState.Registered(data.address.toString()) - else -> LicenseViewState.Verified(data.address.toString()) - } - } - - /** - * Create a list state from the current settings data. - */ - fun viewState(data: SettingsListData): SettingsListViewState { - return SettingsListViewState.Loaded( - settingsList = data.settings, - advancedVisible = data.visibility >= SettingVisibility.Advanced, - ) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsWriteAccess.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsWriteAccess.kt deleted file mode 100644 index bec02968..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SettingsWriteAccess.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.inkapplications.ack.android.settings - -/** - * Provides access to storing key/value preferences for the application. - */ -interface SettingsWriteAccess { - fun setString(setting: StringSetting, value: String) - fun setInt(setting: IntSetting, value: Int) - fun setBoolean(setting: BooleanSetting, value: Boolean) -} - -fun SettingsWriteAccess.setData(setting: TransformableSetting, value: DATA) { - return when (setting) { - is IntBackedSetting -> setInt(setting, setting.storageTransformer.toStorage(value)) - is StringBackedSetting -> setString(setting, setting.storageTransformer.toStorage(value)) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/SharedPreferenceSettings.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/SharedPreferenceSettings.kt deleted file mode 100644 index d00e473a..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/SharedPreferenceSettings.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.inkapplications.ack.android.settings - -import android.content.SharedPreferences -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.trySendBlocking -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -/** - * Provides read + write access to settings via Android's SharedPreferences. - */ -class SharedPreferenceSettings @Inject constructor( - private val preferences: SharedPreferences -): SettingsReadAccess, SettingsWriteAccess { - private val updates = callbackFlow { - val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - trySendBlocking(key) - } - - preferences.registerOnSharedPreferenceChangeListener(listener) - - awaitClose { - preferences.unregisterOnSharedPreferenceChangeListener(listener) - } - } - - override fun observeStringState(setting: StringSetting): Flow { - return updates - .filter { it == setting.key || it == null } - .onStart { emit(setting.key) } - .map { preferences.getString(setting.key, null) } - } - - override fun setString(setting: StringSetting, value: String) = preferences.apply { putString(setting.key, value) } - - override fun observeIntState(setting: IntSetting): Flow { - return updates - .filter { it == setting.key || it == null } - .onStart { emit(setting.key) } - .map { preferences.getOptionalInt(setting.key) } - } - - override fun observeBooleanState(setting: BooleanSetting): Flow { - return updates - .filter { it == setting.key || it == null } - .onStart { emit(setting.key) } - .map { preferences.getOptionalBoolean(setting.key) } - } - - private fun SharedPreferences.getOptionalInt(key: String): Int? { - return ifContains(key) { getInt(key, -1) } - } - - private fun SharedPreferences.getOptionalBoolean(key: String): Boolean? { - return ifContains(key) { getBoolean(key, false) } - } - - private fun SharedPreferences.ifContains(key: String, action: () -> T): T? { - return if (contains(key)) action() else null - } - - override fun setInt(setting: IntSetting, value: Int) = preferences.apply { putInt(setting.key, value) } - - override fun setBoolean(setting: BooleanSetting, value: Boolean) = preferences.apply { putBoolean(setting.key, value) } - - private fun Flow.requireKey(): Flow> = map { key -> - if (preferences.contains(key)) Result.success(key) - else Result.failure(IllegalArgumentException("No preference contained for: $key")) - } - - private inline fun SharedPreferences.apply(modifications: SharedPreferences.Editor.() -> Unit) { - edit().apply { modifications() }.apply() - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/TransformableSetting.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/TransformableSetting.kt deleted file mode 100644 index 2cd4d6fb..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/TransformableSetting.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.inkapplications.ack.android.settings - -import com.inkapplications.ack.android.settings.transformer.Transformer - -/** - * A setting with a transformer to use for going between primitives and complex types. - */ -sealed interface TransformableSetting { - /** - * Transformer used to store the typed data structure into settings. - */ - val storageTransformer: Transformer - - /** - * A default value for the setting, in its data format. - */ - val defaultData: DATA -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/agreement/UsageAgreement.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/agreement/UsageAgreement.kt deleted file mode 100644 index d4648e7f..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/agreement/UsageAgreement.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.inkapplications.ack.android.settings.agreement - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun UsageAgreement() { - Column { - Text( - text = stringResource(R.string.usage_legal_services_title), - style = AckTheme.typography.h2, - ) - Spacer(Modifier.height(AckTheme.spacing.item)) - Text( - text = stringResource(R.string.usage_legal_services_section1), - ) - Text( - text = stringResource(R.string.usage_legal_services_section2), - ) - - Spacer(Modifier.height(AckTheme.spacing.content)) - - Text( - text = stringResource(R.string.usage_data_privacy_title), - style = AckTheme.typography.h2, - ) - Spacer(Modifier.height(AckTheme.spacing.item)) - Text( - text = stringResource(R.string.usage_data_privacy_section1), - ) - Spacer(Modifier.height(AckTheme.spacing.item)) - Text( - text = stringResource(R.string.usage_data_privacy_section2), - ) - - Spacer(Modifier.height(AckTheme.spacing.content)) - - Text( - text = stringResource(R.string.usage_warranty_title), - style = AckTheme.typography.h2, - ) - Spacer(Modifier.height(AckTheme.spacing.item)) - Text( - text = stringResource(R.string.usage_warranty_section1), - ) - - Spacer(Modifier.height(AckTheme.spacing.content)) - - Text( - text = stringResource(R.string.usage_rights_title), - style = AckTheme.typography.h2, - ) - Spacer(Modifier.height(AckTheme.spacing.item)) - Text( - text = stringResource(R.string.usage_rights_section1), - ) - Text( - text = stringResource(R.string.usage_rights_section2), - ) - Text( - text = stringResource(R.string.usage_rights_section3), - ) - Text( - text = stringResource(R.string.usage_rights_section4), - ) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/agreement/UsageAgreementReviewScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/agreement/UsageAgreementReviewScreen.kt deleted file mode 100644 index 22507578..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/agreement/UsageAgreementReviewScreen.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.inkapplications.ack.android.settings.agreement - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.ui.NavigationRow -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun UsageAgreementReviewScreen( - onBackPressed: () -> Unit, -) = AckScreen { - Column { - NavigationRow( - title = stringResource(R.string.usage_title), - onBackPressed = onBackPressed, - ) - Column(Modifier.verticalScroll(rememberScrollState()).fillMaxHeight().padding(AckTheme.spacing.gutter)) { - UsageAgreement() - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/agreement/UserAgreementActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/agreement/UserAgreementActivity.kt deleted file mode 100644 index eec5dd2a..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/agreement/UserAgreementActivity.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.inkapplications.ack.android.settings.agreement - -import androidx.activity.compose.setContent -import com.inkapplications.android.extensions.ExtendedActivity -import kimchi.Kimchi - -/** - * Displays the user-agreement for the application. - * - * Note that this does not have a prompt to agree/disagree like seen in - * the onboarding screens. It is simply for informational purposes. - */ -class UserAgreementActivity: ExtendedActivity() { - override fun onCreate() { - super.onCreate() - Kimchi.trackScreen("user_agreement") - - setContent { - UsageAgreementReviewScreen(onBackPressedDispatcher::onBackPressed) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildData.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildData.kt deleted file mode 100644 index c22ad40c..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildData.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.inkapplications.ack.android.settings.buildinfo - -/** - * Raw data about the current app build and its environment. - */ -data class BuildData( - /** - * Android build type, ie. debug/release. - */ - val buildType: String, - - /** - * Readable version name. - */ - val versionName: String, - - /** - * Canonical version code. - */ - val versionCode: Int, - - /** - * Git commit hash, if specified. - */ - val commit: String?, - - /** - * Whether or not the build was configured to use play services. - */ - val usePlayServices: Boolean, - - /** - * Whether or not play services is available on the device. - */ - val playServicesAvailable: Boolean, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildDataAccess.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildDataAccess.kt deleted file mode 100644 index 66418657..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildDataAccess.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.inkapplications.ack.android.settings.buildinfo - -import android.content.Context -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability -import com.inkapplications.ack.android.BuildConfig -import dagger.Reusable -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import javax.inject.Inject - -/** - * Provides access to build and application runtime information. - */ -@Reusable -class BuildDataAccess @Inject constructor( - googleApiAvailability: GoogleApiAvailability, - context: Context, -) { - val buildData: Flow = flowOf(BuildData( - buildType = BuildConfig.BUILD_TYPE, - versionName = BuildConfig.VERSION_NAME, - versionCode = BuildConfig.VERSION_CODE, - commit = BuildConfig.COMMIT, - usePlayServices = BuildConfig.USE_GOOGLE_SERVICES, - playServicesAvailable = googleApiAvailability.isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS, - )) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildInfo.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildInfo.kt deleted file mode 100644 index 73aecb40..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildInfo.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.inkapplications.ack.android.settings.buildinfo - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.ui.theme.AckTheme -import com.inkapplications.android.extensions.compose.longClickable - -/** - * A column of information text about the build. - */ -@Composable -fun BuildInfo( - buildInfo: BuildInfoState, - modifier: Modifier = Modifier, - onVersionLongPress: () -> Unit = {}, -) = Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) { - when (buildInfo) { - BuildInfoState.Initial -> BuildString( - text = stringResource(R.string.app_name_short), - onLongPress = onVersionLongPress, - ) - is BuildInfoState.Debug -> { - BuildString(stringResource(R.string.application_version_debug), onVersionLongPress) - } - is BuildInfoState.Release -> { - BuildString( - text = buildInfo.versionStatment, - onLongPress = onVersionLongPress, - ) - } - } - if (buildInfo is BuildInfoState.ServiceAvailabilityInfo) { - when (buildInfo.playServices) { - BuildInfoState.ServiceState.Unavailable -> Text(stringResource(R.string.application_version_qualifier_play_unavailable)) - BuildInfoState.ServiceState.Unconfigured -> Text(stringResource(R.string.application_version_qualifier_play_unconfigured)) - BuildInfoState.ServiceState.Available -> {} - } - } -} - -@Composable -private fun BuildString( - text: String, - onLongPress: (() -> Unit)? = null, -) { - Text( - text = text, - style = AckTheme.typography.caption, - modifier = Modifier - .let { if (onLongPress != null) it.longClickable(onLongPress) else it } - .padding(AckTheme.spacing.clickSafety) - ) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildInfoFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildInfoFactory.kt deleted file mode 100644 index 40620f97..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildInfoFactory.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.inkapplications.ack.android.settings.buildinfo - -import com.inkapplications.ack.android.R -import com.inkapplications.android.extensions.StringResources -import dagger.Reusable -import kimchi.logger.KimchiLogger -import javax.inject.Inject - -/** - * Create a view state based on current build data. - */ -@Reusable -class BuildInfoFactory @Inject constructor( - private val stringResources: StringResources, - private val logger: KimchiLogger, -) { - fun buildInfo(data: BuildData): BuildInfoState { - val playServices = when { - !data.usePlayServices -> BuildInfoState.ServiceState.Unconfigured - !data.playServicesAvailable -> BuildInfoState.ServiceState.Unavailable - else -> BuildInfoState.ServiceState.Available - } - if (data.buildType == "debug") { - return BuildInfoState.Debug(playServices) - } - - return BuildInfoState.Release( - versionStatment = stringResources.getString( - key = R.string.application_version_full, - arguments = arrayOf( - data.versionName, - data.versionCode.toString(), - data.commit?.take(7) ?: stringResources.getString(R.string.application_version_commit_missing).also { - logger.error("Release Commit without Commit Hash") - }, - ) - ), - playServices = playServices - ) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildInfoState.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildInfoState.kt deleted file mode 100644 index 787fb110..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/buildinfo/BuildInfoState.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.inkapplications.ack.android.settings.buildinfo - -/** - * View state for the build info composable - */ -sealed interface BuildInfoState { - /** - * Initial state before data has been loaded. - */ - object Initial: BuildInfoState - - /** - * Debug build info, limited useful information. - */ - data class Debug( - override val playServices: ServiceState, - ): BuildInfoState, ServiceAvailabilityInfo - - /** - * Release build info with full details. - */ - data class Release( - val versionStatment: String, - override val playServices: ServiceState - ): BuildInfoState, ServiceAvailabilityInfo - - /** - * Availability descriptor for services on the device. - */ - sealed interface ServiceAvailabilityInfo { - /** - * Availability of Google Play Services - */ - val playServices: ServiceState - } - - /** - * Availability states for a service on the device. - */ - enum class ServiceState { - /** - * Indicates that the service is available and usable. - */ - Available, - - /** - * Indicates that the device does not support this feature. - */ - Unavailable, - - /** - * Indicates that the feature was not configured or disabled at build time. - */ - Unconfigured, - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicenseEditActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicenseEditActivity.kt deleted file mode 100644 index 7c768454..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicenseEditActivity.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.inkapplications.ack.android.settings.license - -import androidx.activity.compose.setContent -import androidx.compose.runtime.collectAsState -import androidx.hilt.navigation.compose.hiltViewModel -import com.inkapplications.ack.android.settings.SettingsAccess -import com.inkapplications.android.extensions.ExtendedActivity -import dagger.hilt.android.AndroidEntryPoint -import kimchi.Kimchi -import kimchi.analytics.Property -import javax.inject.Inject - -/** - * Displays an edit-form for editing the user's licens after onboarding. - */ -@AndroidEntryPoint -class LicenseEditActivity: ExtendedActivity() { - @Inject - lateinit var settingsAccess: SettingsAccess - - @Inject - lateinit var validator: LicensePromptValidator - - override fun onCreate() { - super.onCreate() - Kimchi.trackScreen("license_edit") - - setContent { - val viewModel: LicenseEditViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - - when (state) { - LicenseEditState.Initial -> {} - is LicenseEditState.Editable -> LicensePromptScreen( - initialValues = state.initialValues, - validator = validator, - onContinue = ::onContinue - ) - } - - } - } - - private fun onContinue(values: LicensePromptFieldValues) { - Kimchi.trackEvent("license_update", listOf( - Property.IntProperty("license_callsign_provided", if (values.callsign.isBlank()) 0 else 1), - Property.IntProperty("license_passcode_provided", if (values.passcode.isBlank()) 0 else 1), - )) - settingsAccess.setLicense(values) - finish() - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicenseEditState.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicenseEditState.kt deleted file mode 100644 index 6f6e0dda..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicenseEditState.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.inkapplications.ack.android.settings.license - -/** - * State of the license edit screen. - */ -sealed interface LicenseEditState { - /** - * Initial state of the screen before data has been loaded. - */ - object Initial: LicenseEditState - - /** - * State of the screen when the user's existing license is loaded. - */ - data class Editable( - val initialValues: LicensePromptFieldValues, - ): LicenseEditState -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicenseEditViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicenseEditViewModel.kt deleted file mode 100644 index 3d2f5528..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicenseEditViewModel.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.inkapplications.ack.android.settings.license - -import androidx.lifecycle.viewModelScope -import com.inkapplications.ack.android.settings.SettingsAccess -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -/** - * Android viewmodel containing the current state of the license edit screen. - */ -@HiltViewModel -class LicenseEditViewModel @Inject constructor( - settingsAccess: SettingsAccess, -): androidx.lifecycle.ViewModel() { - val state = settingsAccess.licensePromptFieldValues - .map { LicenseEditState.Editable(it) } - .stateIn(viewModelScope, SharingStarted.Eagerly, LicenseEditState.Initial) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePrompt.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePrompt.kt deleted file mode 100644 index cb1c3b8a..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePrompt.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.inkapplications.ack.android.settings.license - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Button -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.input.KeyboardType -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun LicensePromptScreen( - initialValues: LicensePromptFieldValues, - validator: LicensePromptValidator, - onContinue: (LicensePromptFieldValues) -> Unit, -) = AckScreen { - Column ( - modifier = Modifier - .padding( - top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), - bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - ) - .padding(AckTheme.spacing.gutter), - ) { - Text( - "License Info", - style = AckTheme.typography.h1, - ) - Spacer(Modifier.height(AckTheme.spacing.item)) - Text("If you have a radio license, enter the information below to enable transmit and internet services.") - Spacer(Modifier.height(AckTheme.spacing.content)) - - val callsign = rememberSaveable { mutableStateOf(initialValues.callsign) } - val passcode = rememberSaveable { mutableStateOf(initialValues.passcode) } - val callsignError = rememberSaveable { mutableStateOf(null) } - val passcodeError = rememberSaveable { mutableStateOf(null) } - - TextField( - value = callsign.value, - label = { Text("Callsign") }, - onValueChange = { callsign.value = it; passcode.value = "" }, - isError = callsignError.value != null, - modifier = Modifier.fillMaxWidth(), - ) - if (callsignError.value != null) Text(callsignError.value.orEmpty(), style = AckTheme.typography.errorCaption) - Spacer(Modifier.height(AckTheme.spacing.item)) - TextField( - value = passcode.value, - label = { Text("APRS-IS Passcode") }, - onValueChange = { passcode.value = it }, - enabled = callsign.value.isNotBlank(), - isError = passcodeError.value != null, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth(), - ) - if (passcodeError.value != null) Text(passcodeError.value.orEmpty(), style = AckTheme.typography.errorCaption) - Spacer(Modifier.weight(1f)) - Button( - onClick = { - callsignError.value = validator.getLicenseError(callsign.value) - passcodeError.value = validator.getPasscodeError(callsign.value, passcode.value) - if (validator.isValid(callsign.value, passcode.value)) { - onContinue(LicensePromptFieldValues(callsign.value, passcode.value)) - } - }, - modifier = Modifier.fillMaxWidth().padding(top = AckTheme.spacing.item), - ) { - if (callsign.value.isBlank() && passcode.value.isBlank()) { - Text("Skip") - } else { - Text("Continue") - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePromptFieldValues.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePromptFieldValues.kt deleted file mode 100644 index 27b94d54..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePromptFieldValues.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.inkapplications.ack.android.settings.license - -/** - * Form data for the license prompt fields. - */ -data class LicensePromptFieldValues( - val callsign: String, - val passcode: String, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePromptValidator.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePromptValidator.kt deleted file mode 100644 index 7a7b5f77..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/license/LicensePromptValidator.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.inkapplications.ack.android.settings.license - -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.connection.ConnectionSettings -import com.inkapplications.ack.android.input.ConvertibleOptionalIntValidator -import com.inkapplications.ack.android.input.ValidationResult -import com.inkapplications.ack.client.generatePasscode -import com.inkapplications.android.extensions.StringResources -import dagger.Reusable -import kimchi.logger.KimchiLogger -import javax.inject.Inject - -@Reusable -class LicensePromptValidator @Inject constructor( - private val connectionSettings: ConnectionSettings, - private val intValidator: ConvertibleOptionalIntValidator, - private val stringResources: StringResources, - private val logger: KimchiLogger, -) { - fun getLicenseError(license: String): String? { - return connectionSettings.address.validator - .validate(license) - .let { (it as? ValidationResult.Error)?.message } - } - - fun getPasscodeError(license: String, passcode: String): String? { - val fieldValidation = intValidator.validate(passcode) - if (fieldValidation is ValidationResult.Error) return fieldValidation.message - - val converted = connectionSettings.passcode.fieldTransformer.toStorage(passcode) - val inputValidation = connectionSettings.passcode.inputValidator.validate(converted) - val data = connectionSettings.passcode.storageTransformer.toData(converted) - val actual = generatePasscode(license) - - return when { - inputValidation is ValidationResult.Error -> inputValidation.message.also { - logger.debug("Passcode input validation failed") - } - data == null -> null - converted != actual -> stringResources.getString(R.string.connection_setting_passcode_incorrect) - else -> null - } - } - - fun isValid(license: String, passcode: String): Boolean { - return getLicenseError(license) == null && getPasscodeError(license, passcode) == null - } -} - diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/EnumTransformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/EnumTransformer.kt deleted file mode 100644 index 3907e03c..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/EnumTransformer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -/** - * Creates a transformer for an enum type, stored by enm name. - */ -inline fun > enumTransformer(): Transformer { - return object: Transformer { - override fun toStorage(data: T): String = data.name - override fun toData(storage: String): T = enumValueOf(storage) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/IntFieldTransformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/IntFieldTransformer.kt deleted file mode 100644 index a1aef5bc..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/IntFieldTransformer.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -/** - * Transform a string to an integer. - */ -object IntFieldTransformer: Transformer { - override fun toStorage(data: String): Int = data.toInt() - override fun toData(storage: Int): String = storage.toString() -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/MileTransformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/MileTransformer.kt deleted file mode 100644 index 0550dbe6..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/MileTransformer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -import inkapplications.spondee.measure.Length -import inkapplications.spondee.measure.us.miles -import inkapplications.spondee.measure.us.toMiles -import inkapplications.spondee.structure.roundToInt - -/** - * Transform a length to a primitive of Miles. - */ -object MileTransformer: Transformer { - override fun toStorage(data: Length): Int = data.toMiles().roundToInt() - override fun toData(storage: Int): Length = storage.miles -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/MillisecondTransformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/MillisecondTransformer.kt deleted file mode 100644 index 4b439749..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/MillisecondTransformer.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds - -/** - * Transform a duration to a primitive stored in milliseconds. - * - * Note: This is stored as an Int, so this should not be used for large - * duration values. - */ -object MillisecondTransformer: Transformer { - override fun toStorage(data: Duration): Int = data.inWholeMilliseconds.toInt() - override fun toData(storage: Int): Duration = storage.milliseconds -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/MinuteTransformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/MinuteTransformer.kt deleted file mode 100644 index 2f7536cb..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/MinuteTransformer.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes - -/** - * Transform a duration to a primitive stored in minutes. - */ -object MinuteTransformer: Transformer { - override fun toStorage(data: Duration): Int = data.inWholeMinutes.toInt() - override fun toData(storage: Int): Duration = storage.minutes -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/OptionalIntTransformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/OptionalIntTransformer.kt deleted file mode 100644 index b6a24efd..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/OptionalIntTransformer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -/** - * Convert a string to an integer, using a default value if the string is blank. - * - * @param nullValue The value to use if the string is blank. - */ -class OptionalIntTransformer(val nullValue: Int = -1): Transformer { - override fun toStorage(data: String): Int = data.takeIf { it.isNotBlank() }?.toInt() ?: nullValue - override fun toData(storage: Int): String = storage.takeIf { it != nullValue }?.toString().orEmpty() -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/PercentageTransformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/PercentageTransformer.kt deleted file mode 100644 index 4eaa07bd..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/PercentageTransformer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -import inkapplications.spondee.scalar.Percentage -import inkapplications.spondee.scalar.percent -import inkapplications.spondee.scalar.toWholePercentage -import inkapplications.spondee.structure.roundToInt - -/** - * Transform a percentage object to a whole integer value. - */ -object PercentageTransformer: Transformer { - override fun toStorage(data: Percentage): Int = data.toWholePercentage().roundToInt() - override fun toData(storage: Int): Percentage = storage.percent -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/SentinelOptionalTransformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/SentinelOptionalTransformer.kt deleted file mode 100644 index 704c5247..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/SentinelOptionalTransformer.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -/** - * Wrap a transformer, making it optional if the stored value matches a key. - */ -class SentinelOptionalTransformer( - private val sentinelValue: STORAGE, - private val delegate: Transformer, -): Transformer { - override fun toStorage(data: DATA?): STORAGE { - return data?.let(delegate::toStorage) ?: sentinelValue - } - - override fun toData(storage: STORAGE): DATA? { - return storage?.takeIf { it != sentinelValue }?.let(delegate::toData) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/StationAddressTransformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/StationAddressTransformer.kt deleted file mode 100644 index 78764ea3..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/StationAddressTransformer.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -import com.inkapplications.ack.structures.station.StationAddress -import com.inkapplications.ack.structures.station.toStationAddress - -/** - * Transform a station callsign and ssid to a string primitive. - */ -object StationAddressTransformer: Transformer { - override fun toStorage(data: StationAddress): String = data.toString() - override fun toData(storage: String): StationAddress = storage.toStationAddress() -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/Transformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/Transformer.kt deleted file mode 100644 index fcc18d73..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/Transformer.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -/** - * Transformer to change settings to/from their storage type. - * - * Note: Data is expected to be validated before being passed to the - * transformer. - */ -interface Transformer { - /** - * Change a data structure into a type appropriate for storing. - */ - fun toStorage(data: DATA): STORAGE - - /** - * Change a data structure into a type appropriate for application use. - */ - fun toData(storage: STORAGE): DATA -} - -/** - * Combines two transformers, allowing chaining. - */ -private class CompositeTransformer( - val first: Transformer, - val second: Transformer -): Transformer { - override fun toStorage(data: ORIGIN): END { - return second.toStorage(first.toStorage(data)) - } - - override fun toData(storage: END): ORIGIN { - return first.toData(second.toData(storage)) - } -} - -/** - * Chain two transformers together. - */ -fun Transformer.then(second: Transformer): Transformer { - return CompositeTransformer(this, second) -} - -/** - * Chain two transformers together. - */ -operator fun Transformer.plus(second: Transformer): Transformer { - return this.then(second) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/TrimmingTransformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/TrimmingTransformer.kt deleted file mode 100644 index b86cd32c..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/settings/transformer/TrimmingTransformer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -/** - * Trim whitespace from a string in both transformation directions. - */ -object TrimmingTransformer: Transformer { - override fun toStorage(data: String): String { - return data.trim() - } - - override fun toData(storage: String): String { - return storage.trim() - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/startup/ApplicationInitializer.kt b/android-application/src/main/java/com/inkapplications/ack/android/startup/ApplicationInitializer.kt deleted file mode 100644 index c99d1d6b..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/startup/ApplicationInitializer.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.inkapplications.ack.android.startup - -import android.app.Application - -interface ApplicationInitializer { - val priority: Priority get() = Priority.NORMAL - - suspend fun initialize(application: Application) - - enum class Priority { - HIGH, - NORMAL, - LOW, - } -} - -/** - * Create a static initializer from a lambda. - */ -fun ApplicationInitializer( - priority: ApplicationInitializer.Priority = ApplicationInitializer.Priority.NORMAL, - init: Application.() -> Unit -): ApplicationInitializer { - return object: ApplicationInitializer { - override suspend fun initialize(application: Application) { - application.init() - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/startup/CompositeApplicationInitializer.kt b/android-application/src/main/java/com/inkapplications/ack/android/startup/CompositeApplicationInitializer.kt deleted file mode 100644 index 62388d6c..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/startup/CompositeApplicationInitializer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.inkapplications.ack.android.startup - -import android.app.Application -import dagger.Reusable -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import javax.inject.Inject - -@Reusable -class CompositeApplicationInitializer @Inject constructor( - private val initializers: @JvmSuppressWildcards Set, - private val logger: KimchiLogger -): ApplicationInitializer { - override suspend fun initialize(application: Application) { - logger.trace("Initializing Application with ${initializers.size} initializers.") - coroutineScope { - val jobs = initializers - .sortedBy { it.priority } - .mapIndexed { index, initializer -> - async { - initializer.initialize(application) - logger.trace("#$index: <${initializer.javaClass.simpleName}> complete.") - } - } - jobs.awaitAll() - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/startup/InitJob.kt b/android-application/src/main/java/com/inkapplications/ack/android/startup/InitJob.kt deleted file mode 100644 index 01500312..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/startup/InitJob.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.inkapplications.ack.android.startup - -import android.app.Application -import kotlinx.coroutines.* - -/** - * Contains and executes the Initialization Job for the application. - */ -class InitJob private constructor( - private val initializer: ApplicationInitializer, - private val job: CompletableJob, -): Job by job { - constructor(initializer: ApplicationInitializer): this(initializer, Job()) - - private val scope = CoroutineScope(this + Dispatchers.Main) - - fun init(application: Application) { - if (job.isCompleted) return - scope.launch { - initializer.initialize(application) - job.complete() - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/startup/KimchiInitializer.kt b/android-application/src/main/java/com/inkapplications/ack/android/startup/KimchiInitializer.kt deleted file mode 100644 index 7cdc6b46..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/startup/KimchiInitializer.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.inkapplications.ack.android.startup - -import com.inkapplications.ack.android.BuildConfig -import com.inkapplications.android.extensions.LifecycleLogger -import kimchi.Kimchi -import kimchi.KimchiLoggerAnalytics -import kimchi.bridge.firebase.analytics.FirebaseAnalyticsAdapter -import kimchi.bridge.firebase.crashlytics.FirebaseCrashlyticsExceptionAdapter -import kimchi.bridge.firebase.crashlytics.FirebaseCrashlyticsLogMessageAdapter -import kimchi.logger.LogLevel -import kimchi.logger.defaultWriter -import kimchi.logger.withThreshold - -/** - * Initializes the Kimchi Logging Framework. - */ -val KimchiInitializer = ApplicationInitializer(ApplicationInitializer.Priority.HIGH) { - Kimchi.addLog(defaultWriter) - if (BuildConfig.USE_GOOGLE_SERVICES) { - Kimchi.addLog(FirebaseCrashlyticsLogMessageAdapter().withThreshold(LogLevel.INFO)) - Kimchi.addLog(FirebaseCrashlyticsExceptionAdapter()) - Kimchi.addAnalytics(FirebaseAnalyticsAdapter()) - } - Kimchi.addAnalytics(KimchiLoggerAnalytics) - - registerActivityLifecycleCallbacks(LifecycleLogger { - Kimchi.trace(it) - }) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/startup/NoOpInitializer.kt b/android-application/src/main/java/com/inkapplications/ack/android/startup/NoOpInitializer.kt deleted file mode 100644 index 1952a00e..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/startup/NoOpInitializer.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.inkapplications.ack.android.startup - -/** - * An Initializer implementation that does nothing. - * - * This can be used for testing or as a stand in for disabled services. - */ -val NoOpInitializer = ApplicationInitializer(ApplicationInitializer.Priority.LOW) {} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupActivity.kt deleted file mode 100644 index d1609931..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupActivity.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.inkapplications.ack.android.startup - -import android.os.Bundle -import androidx.activity.compose.setContent -import com.inkapplications.ack.android.capture.CaptureActivity -import com.inkapplications.ack.android.onboard.OnboardActivity -import com.inkapplications.ack.android.onboard.OnboardingStateAccess -import com.inkapplications.android.extensions.ExtendedActivity -import com.inkapplications.android.startActivity -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds - -/** - * A screen for initializing application settings. - */ -@AndroidEntryPoint -class StartupActivity: ExtendedActivity() { - /** - * Delay time for the splash screen. - * - * This is so the splash screen doesn't flash for too short of a time and - * appear as jank. Since the initializers are run asynchronously, this will - * not increase the load time, but set a minimum load time. - */ - private val delayTime = .5.seconds - - @Inject - lateinit var initJob: InitJob - - @Inject - lateinit var onboardingStateAccess: OnboardingStateAccess - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContent { - StartupScreen() - } - } - - override fun onStart() { - super.onStart() - foregroundScope.launch { - delay(delayTime) - initJob.join() - - if (onboardingStateAccess.finished.first()) { - startActivity(CaptureActivity::class) - } else { - startActivity(OnboardActivity::class) - } - finish() - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupModule.kt deleted file mode 100644 index 1cb92e51..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupModule.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.inkapplications.ack.android.startup - -import android.app.Application -import com.inkapplications.ack.android.capture.service.CaptureServiceNotifications -import com.inkapplications.ack.android.maps.MapsImplementation -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.ElementsIntoSet -import dagger.multibindings.Multibinds -import javax.inject.Singleton - -@Module(includes = [StartupModuleBindings::class]) -@InstallIn(SingletonComponent::class) -class StartupModule { - @Singleton - @Provides - fun initJob(initializer: ApplicationInitializer) = InitJob(initializer) - - @Provides - @ElementsIntoSet - fun initializers( - captureService: CaptureServiceNotifications - ): Set = setOf( - object: ApplicationInitializer { - override suspend fun initialize(application: Application) { - MapsImplementation.initialize(application) - } - }, - KimchiInitializer, - captureService, - ) -} - -@Module -private abstract class StartupModuleBindings { - @Multibinds - abstract fun initializerBinding(): @JvmSuppressWildcards Set - - @Binds - abstract fun initializer(composite: CompositeApplicationInitializer): ApplicationInitializer -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupScreen.kt deleted file mode 100644 index 988f3164..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupScreen.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.inkapplications.ack.android.startup - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.fadeIn -import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.hilt.navigation.compose.hiltViewModel -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.settings.buildinfo.BuildInfo -import com.inkapplications.ack.android.settings.buildinfo.BuildInfoState -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun StartupScreen( - viewModel: StartupViewModel = hiltViewModel() -) = AckScreen { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .padding(AckTheme.spacing.gutter) - .fillMaxHeight() - .fillMaxWidth() - ) { - val buildInfoState = viewModel.buildInfoState.collectAsState() - val visible = remember { MutableTransitionState(false) } - - if (buildInfoState.value !is BuildInfoState.Initial) { - visible.targetState = true - } - - AnimatedVisibility( - visibleState = visible, - enter = fadeIn(), - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - painterResource(R.drawable.wave), - contentDescription = null, - tint = AckTheme.colors.accent, - modifier = Modifier.padding(bottom = AckTheme.spacing.content) - ) - BuildInfo(buildInfoState.value) - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupViewModel.kt deleted file mode 100644 index e46abcbb..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/startup/StartupViewModel.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.inkapplications.ack.android.startup - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.inkapplications.ack.android.settings.buildinfo.BuildDataAccess -import com.inkapplications.ack.android.settings.buildinfo.BuildInfoFactory -import com.inkapplications.ack.android.settings.buildinfo.BuildInfoState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -/** - * Android viewmodel for storing the state loaded on the startup screen. - */ -@HiltViewModel -class StartupViewModel @Inject constructor( - buildDataAccess: BuildDataAccess, - buildInfoFactory: BuildInfoFactory, -): ViewModel() { - val buildInfoState: StateFlow = buildDataAccess.buildData - .map { buildInfoFactory.buildInfo(it) } - .stateIn(viewModelScope, SharingStarted.Eagerly, BuildInfoState.Initial) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/InsightViewState.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/InsightViewState.kt deleted file mode 100644 index 2c717b88..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/InsightViewState.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.inkapplications.ack.android.station - -import com.inkapplications.ack.structures.TelemetryValues - -/** - * Summary data for a particular station to display to the user - */ -data class InsightViewState( - val name: String = "", - val comment: String? = null, - val temperature: String? = null, - val wind: String? = null, - val altitude: String? = null, - val telemetryValues: TelemetryValues? = null, - val telemetrySequence: String? = null, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationActivity.kt deleted file mode 100644 index af967d04..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationActivity.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.inkapplications.ack.android.station - -import android.app.Activity -import android.os.Bundle -import androidx.activity.compose.setContent -import com.inkapplications.ack.android.log.LogItemViewState -import com.inkapplications.ack.android.log.details.startLogInspectActivity -import com.inkapplications.ack.android.trackNavigation -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.structures.station.Callsign -import com.inkapplications.android.extensions.ExtendedActivity -import com.inkapplications.android.startActivity -import dagger.hilt.android.AndroidEntryPoint -import kimchi.Kimchi - -const val EXTRA_CALLSIGN = "aprs.station.extra.callsign" - -/** - * Displays information and recent packets for a particular station. - */ -@AndroidEntryPoint -class StationActivity: ExtendedActivity(), StationScreenController { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - Kimchi.trackScreen("station") - - setContent { - AckScreen { - StationScreen( - controller = this, - ) - } - } - } - - override fun onMapItemClicked(id: CaptureId?) { - Kimchi.trackEvent("station_map_item_click") - Kimchi.debug("Map Item Clicked: No-Op") - } - - override fun onLogItemClicked(item: LogItemViewState) { - Kimchi.trackEvent("station_log_item_click") - startLogInspectActivity(item.id) - } -} - -/** - * Start the station activity for the station with the given callsign. - * - * @param callsign The callsign of the station to display. - */ -fun Activity.startStationActivity(callsign: Callsign) { - Kimchi.trackNavigation("station") - startActivity(StationActivity::class) { - putExtra(EXTRA_CALLSIGN, callsign.canonical) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationData.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationData.kt deleted file mode 100644 index dd5f0441..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationData.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.inkapplications.ack.android.station - -import com.inkapplications.ack.data.CapturedPacket - -/** - * Data required to render the station screen. - */ -data class StationData( - val packets: List = emptyList(), - val metric: Boolean = false, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationEvents.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationEvents.kt deleted file mode 100644 index 4d5349a2..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationEvents.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.inkapplications.ack.android.station - -import com.inkapplications.ack.android.locale.LocaleSettings -import com.inkapplications.ack.android.settings.SettingsReadAccess -import com.inkapplications.ack.android.settings.observeBoolean -import com.inkapplications.ack.android.settings.observeInt -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.data.PacketStorage -import com.inkapplications.ack.structures.station.Callsign -import dagger.Reusable -import kimchi.logger.EmptyLogger -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest -import javax.inject.Inject - -@Reusable -class StationEvents @Inject constructor( - private val aprs: PacketStorage, - private val settings: SettingsReadAccess, - private val localeSettings: LocaleSettings, - private val stationSettings: StationSettings, - private val logger: KimchiLogger = EmptyLogger, -) { - suspend fun packet(id: CaptureId) = aprs.findById(id).first() - - fun stationData(callsign: Callsign): Flow { - logger.trace("Observing Station Data for: $callsign") - return settings.observeInt(stationSettings.recentStationEvents) - .flatMapLatest { aprs.findBySource(callsign, it.toLong()) } - .combine(settings.observeBoolean(localeSettings.preferMetric)) { packets, metric -> - StationData( - packets = packets, - metric = metric, - ) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationInsightViewStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationInsightViewStateFactory.kt deleted file mode 100644 index ca25391c..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationInsightViewStateFactory.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.inkapplications.ack.android.station - -import com.inkapplications.ack.android.locale.format -import com.inkapplications.ack.android.log.SummaryFactory -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.capabilities.Commented -import com.inkapplications.ack.structures.capabilities.Report -import com.inkapplications.android.extensions.ViewStateFactory -import dagger.Reusable -import javax.inject.Inject - -/** - * Convert raw station data into data needed to render the insight view for a station. - */ -@Reusable -class StationInsightViewStateFactory @Inject constructor( - private val summaryFactory: SummaryFactory, -): ViewStateFactory { - override fun create(data: StationData): InsightViewState { - val firstWeatherData = data.packets.firstNotNullOfOrNull { it.parsed.data as? PacketData.Weather } - - return InsightViewState( - name = data.packets.first().parsed.route.source.callsign.canonical, - comment = data.packets.firstNotNullOfOrNull { (it.parsed.data as? Commented)?.comment }, - temperature = firstWeatherData?.temperature - ?.format(data.metric), - wind = firstWeatherData?.let { summaryFactory.createWindSummary(it.windData, data.metric) }, - altitude = data.packets.firstNotNullOfOrNull { it.parsed.data as? Report } - ?.altitude - ?.format(data.metric), - telemetryValues = data.packets.firstNotNullOfOrNull { it.parsed.data as? PacketData.TelemetryReport } - ?.data, - telemetrySequence = data.packets.firstNotNullOfOrNull { it.parsed.data as? PacketData.TelemetryReport } - ?.sequenceId, - ) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationModule.kt deleted file mode 100644 index 0d429eb0..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationModule.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.inkapplications.ack.android.station - -import com.inkapplications.ack.android.settings.SettingsProvider -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(SingletonComponent::class) -abstract class StationModule { - @Binds - @IntoSet - abstract fun settings(settings: StationSettings): SettingsProvider -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreen.kt deleted file mode 100644 index 69fac28b..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreen.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.inkapplications.ack.android.station - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.log.AprsLogItem -import com.inkapplications.ack.android.map.MarkerMap -import com.inkapplications.ack.android.ui.IconRow -import com.inkapplications.ack.android.ui.NavigationRow -import com.inkapplications.ack.android.ui.TelemetryTable -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun StationScreen( - viewModel: StationViewModel = hiltViewModel(), - controller: StationScreenController, -) { - val stationState = viewModel.stationState.collectAsState().value - if (stationState is StationViewState.Loaded) { - StationDetails(stationState, controller) - } -} - -@Composable -private fun StationDetails( - viewState: StationViewState.Loaded, - controller: StationScreenController, -) { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - ) { - if (viewState.mapState.markers.isNotEmpty()) { - Column { - Box { - MarkerMap( - viewModel = viewState.mapState, - onMapItemClicked = controller::onMapItemClicked, - interactive = false, - modifier = Modifier.aspectRatio(16f / 9f), - ) - Box( - modifier = Modifier - .padding(top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()) - ) { - IconButton( - onClick = controller::onBackPressed - ) { - Icon(Icons.Default.ArrowBack, stringResource(R.string.navigate_up)) - } - } - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = AckTheme.spacing.gutter) - ) { - Text( - viewState.insight.name, - style = AckTheme.typography.h1, - modifier = Modifier.padding( - start = AckTheme.spacing.gutter, - end = AckTheme.spacing.gutter, - ), - ) - } - } - } else { - Box( - Modifier - .padding( - top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), - ) - .padding( - start = AckTheme.spacing.gutter, - top = AckTheme.spacing.gutter, - end = AckTheme.spacing.gutter, - ) - ) { - NavigationRow( - title = { - Text( - viewState.insight.name, - style = AckTheme.typography.h1, - modifier = Modifier.padding( - start = AckTheme.spacing.gutter, - end = AckTheme.spacing.gutter, - ), - ) - }, - onBackPressed = controller::onBackPressed - ) - } - } - Column( - modifier = Modifier - .padding( - bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - ) - .padding( - top = AckTheme.spacing.content, - start = AckTheme.spacing.gutter, - end = AckTheme.spacing.gutter, - bottom = AckTheme.spacing.gutter, - ), - ) { - if (viewState.insight.temperature != null) { - IconRow(Icons.Default.WbSunny, viewState.insight.temperature) - } - if (viewState.insight.wind != null) { - IconRow(Icons.Default.Air, viewState.insight.wind) - } - if (viewState.insight.altitude != null) { - IconRow(Icons.Default.Terrain, viewState.insight.altitude) - } - if (viewState.insight.comment != null) { - IconRow(Icons.Default.Comment, viewState.insight.comment) - } - if (viewState.insight.telemetryValues != null) { - TelemetryTable(viewState.insight.telemetryValues, viewState.insight.telemetrySequence) - } - Text( - text = stringResource(R.string.station_packet_list_title), - style = AckTheme.typography.h2, - modifier = Modifier.padding(vertical = AckTheme.spacing.content), - ) - viewState.packets.forEach { log -> - AprsLogItem(log, controller::onLogItemClicked) - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreenController.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreenController.kt deleted file mode 100644 index 77482763..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationScreenController.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.inkapplications.ack.android.station - -import com.inkapplications.ack.android.log.LogItemViewState -import com.inkapplications.ack.data.CaptureId - -/** - * Actions invoked on the station screen - */ -interface StationScreenController { - /** - * Invoked when the user presses the back button at the top of the page. - */ - fun onBackPressed() - - /** - * Invoked when the user clicks on a log item in the list of packets - * received for this station. - */ - fun onLogItemClicked(item: LogItemViewState) - - /** - * Invoked when the user clicks on a map marker. - * - * @param id The id of the map marker that was clicked. - */ - fun onMapItemClicked(id: CaptureId?) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationSettings.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationSettings.kt deleted file mode 100644 index bc2ed994..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationSettings.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.inkapplications.ack.android.station - -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.input.IntegerValidator -import com.inkapplications.ack.android.settings.* -import com.inkapplications.android.extensions.StringResources -import dagger.Reusable -import javax.inject.Inject - -@Reusable -class StationSettings @Inject constructor( - stringResources: StringResources, -): SettingsProvider { - private val categoryName = stringResources.getString(R.string.station_settings_category) - - val showDebugData = BooleanSetting( - key = "station.data.debug", - name = stringResources.getString(R.string.station_settings_debug), - categoryName = categoryName, - defaultValue = false, - visibility = SettingVisibility.Dev, - ) - - val recentStationEvents = IntSetting( - key = "station.data.recent.limit", - name = stringResources.getString(R.string.station_settings_recent_limit), - categoryName = categoryName, - defaultValue = 10, - visibility = SettingVisibility.Advanced, - validator = IntegerValidator( - error = stringResources.getString(R.string.input_validator_positive_integer_error), - zeroInclusive = false, - ), - ) - - override val settings: List = listOf( - showDebugData, recentStationEvents - ) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewModel.kt deleted file mode 100644 index 149fcd05..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewModel.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.inkapplications.ack.android.station - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope -import com.inkapplications.ack.android.log.LogItemViewStateFactory -import com.inkapplications.ack.android.maps.* -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.structures.capabilities.Mapable -import com.inkapplications.ack.structures.station.Callsign -import com.inkapplications.android.extensions.ViewStateFactory -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -/** - * Android viewmodel to load state for the station screen. - */ -@HiltViewModel -class StationViewModel @Inject constructor( - savedState: SavedStateHandle, - stationEvents: StationEvents, - stationInsightViewStateFactory: StationInsightViewStateFactory, - logItemViewStateFactory: LogItemViewStateFactory, - markerViewStateFactory: ViewStateFactory, -): androidx.lifecycle.ViewModel() { - private val stationCallsign = savedState.get(EXTRA_CALLSIGN)!!.let(::Callsign) - val stationState = stationEvents.stationData(stationCallsign) - .map { data -> - StationViewState.Loaded( - insight = stationInsightViewStateFactory.create(data), - packets = data.packets.map { packet -> - logItemViewStateFactory.create(packet.id, packet.parsed, data.metric) - }, - mapState = data.packets.firstOrNull { it.parsed.data is Mapable } - .let { firstMapable -> - MapViewModel( - markers = firstMapable?.let { markerViewStateFactory.create(it) } - ?.let { listOf(it) } - .orEmpty(), - cameraPosition = (firstMapable?.parsed?.data as? Mapable)?.coordinates - ?.let { MapCameraPosition(it, ZoomLevels.ROADS) } - ?: CameraPositionDefaults.unknownLocation, - ) - } - ) - } - .stateIn(viewModelScope, SharingStarted.Eagerly, StationViewState.Initial) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewState.kt b/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewState.kt deleted file mode 100644 index 7b821662..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/station/StationViewState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.inkapplications.ack.android.station - -import com.inkapplications.ack.android.log.LogItemViewState -import com.inkapplications.ack.android.maps.MapViewModel - -sealed interface StationViewState { - object Initial: StationViewState - data class Loaded( - val insight: InsightViewState, - val packets: List, - val mapState: MapViewModel, - ): StationViewState -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/symbol/AndroidSymbolFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/symbol/AndroidSymbolFactory.kt deleted file mode 100644 index 76a5a0eb..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/symbol/AndroidSymbolFactory.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.inkapplications.ack.android.symbol - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Matrix -import com.inkapplications.ack.structures.Symbol -import com.inkapplications.ack.structures.symbolOf -import dagger.Reusable -import javax.inject.Inject - -/** - * Create a bitmap icon for an APRS symbol character. - */ -@Reusable -class AndroidSymbolFactory @Inject constructor( - private val context: Context, - private val locator: SymbolResourceLocator -): SymbolFactory { - private val resources = context.resources - - override val defaultSymbol by lazy { - createSymbol(symbolOf('"', '/')) - } - - override fun createSymbol(symbol: Symbol): Bitmap? { - val base = locator.getBaseResourceName(symbol).let(::findBitmap) - val overlay = locator.getOverlayResourceName(symbol)?.let(::findBitmap) - - return if (overlay == null) base else base?.withOverlay(overlay) - } - - private fun findBitmap(name: String): Bitmap? { - return resources.getIdentifier(name, "drawable", context.packageName) - .let { BitmapFactory.decodeResource(resources, it) } - } - - private fun Bitmap.withOverlay(other: Bitmap): Bitmap { - val bmOverlay = Bitmap.createBitmap(width, height, config ?: Bitmap.Config.ARGB_8888) - val canvas = Canvas(bmOverlay) - canvas.drawBitmap(this, Matrix(), null) - canvas.drawBitmap(other, 0f, 0f, null) - return bmOverlay - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolFactory.kt deleted file mode 100644 index 9022b2b7..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolFactory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.inkapplications.ack.android.symbol - -import android.graphics.Bitmap -import com.inkapplications.ack.structures.Symbol - -/** - * Create bitmaps for APRS Symbols. - */ -interface SymbolFactory { - val defaultSymbol: Bitmap? - fun createSymbol(symbol: Symbol): Bitmap? -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolModule.kt deleted file mode 100644 index 7f118242..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolModule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.inkapplications.ack.android.symbol - -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -abstract class SymbolModule { - @Binds - abstract fun symbolFactory(symbolFactory: AndroidSymbolFactory): SymbolFactory -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolPrompt.kt b/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolPrompt.kt deleted file mode 100644 index bd8fe6e6..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolPrompt.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.inkapplications.ack.android.symbol - -import androidx.compose.foundation.layout.* -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.ui.theme.AckTheme -import com.inkapplications.ack.structures.Symbol - -/** - * Input prompt allowing the user to select a symbol. - */ -@Composable -fun SymbolPrompt( - title: String, - value: Symbol?, - onDismiss: () -> Unit, - onSubmit: (Symbol?) -> Unit, -) { - val currentSymbol = remember { mutableStateOf(value) } - - Dialog( - onDismissRequest = onDismiss, - ) { - Surface( - shape = AckTheme.shapes.corners, - ) { - Column( - modifier = Modifier.padding(AckTheme.spacing.gutter), - ) { - Text(title, style = AckTheme.typography.h2) - SymbolSelector( - onChange = { - currentSymbol.value = it - }, - default = currentSymbol.value, - modifier = Modifier.padding(vertical = AckTheme.spacing.item) - .align(Alignment.CenterHorizontally) - .sizeIn(maxWidth = 400.dp, maxHeight = 200.dp) - ) - - Row( - horizontalArrangement = Arrangement.End, - modifier = Modifier.fillMaxWidth().padding(top = AckTheme.spacing.item), - ) { - TextButton( - onClick = onDismiss, - colors = ButtonDefaults.textButtonColors( - contentColor = AckTheme.colors.foreground, - ), - modifier = Modifier.padding(end = AckTheme.spacing.item), - ) { - Text(stringResource(R.string.prompt_cancel)) - } - TextButton( - onClick = { - onSubmit(currentSymbol.value) - }, - ) { - Text(stringResource(R.string.prompt_save)) - } - } - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolResourceLocator.kt b/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolResourceLocator.kt deleted file mode 100644 index 10aa72e3..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolResourceLocator.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.inkapplications.ack.android.symbol - -import com.inkapplications.ack.structures.Symbol -import dagger.Reusable -import kimchi.logger.KimchiLogger -import javax.inject.Inject - -private const val ALT_START = 96 -private const val OVERLAY_START = 207 -private const val PREFIX = "symbol_" -private const val CHAR_RANGE_START = '!' -private const val CHAR_RANGE_END = '~' -private const val ASC_0 = '0' -private const val ASC_9 = '9' -private const val ASC_A = 'A' -private const val ASC_Z = 'Z' -private const val FAULT_SYMBOL = 127.toChar() - -/** - * Translate a pair of APRS Symbol Chars to symbol resource names. - * - * The resource names are arbitrary and sequential as produced by an image - * slicer. They start at 0 and go up, with some empty symbols having been - * removed. - * The three tables (primary, offset and alphanumeric overlays) are offset - * by [ALT_START] and [OVERLAY_START]. i.e. the alternate symbols should start - * at the image number corresponding to [ALT_START] and so on. - * - * APRS Symbol format is defined here: http://www.aprs.org/symbols.html - * - * TL;DR – the first char is typically '/' or '\' designating the primary or - * secondary set of symbols, and the symbol is looked up by the second - * character's ASCII table position. If the first character is something other - * than '/' or '\' then it is indicating an overlay. - * - * This supports alphanumeric overlays, and will fall-back to just the alternate - * symbol if an unknown overlay is used. - */ -@Reusable -class SymbolResourceLocator @Inject constructor( - private val logger: KimchiLogger -) { - fun getBaseResourceName(symbol: Symbol): String { - if (symbol.id !in CHAR_RANGE_START..CHAR_RANGE_END) { - logger.warn("Out-of-bounds symbol requested: ${symbol.id}") - return format(FAULT_SYMBOL - CHAR_RANGE_START) - } - return when(symbol) { - is Symbol.Primary -> format(symbol.id - CHAR_RANGE_START) - is Symbol.Alternate -> format(symbol.id - CHAR_RANGE_START + ALT_START) - } - } - - fun getOverlayResourceName(symbol: Symbol): String? { - if (symbol !is Symbol.Alternate) return null - val overlay = symbol.overlay ?: return null - if (!symbol.alphaNumeric) return null - return when(overlay) { - in ASC_0..ASC_9, in ASC_A..ASC_Z -> format(overlay - ASC_0 + OVERLAY_START) - else -> null.also { - logger.warn("Out-of-bounds overlay requested: ${overlay.toInt()}") - } - } - } - - private fun format(number: Int): String = PREFIX + number.toString() -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolSelector.kt b/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolSelector.kt deleted file mode 100644 index 93e53474..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolSelector.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.inkapplications.ack.android.symbol - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items -import androidx.compose.material.IconButton -import androidx.compose.material.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.inkapplications.ack.android.ui.theme.AckTheme -import com.inkapplications.ack.structures.Symbol -import com.inkapplications.ack.structures.code -import com.inkapplications.ack.structures.symbolOf - -/** - * Selection UI showing a list of APRS symbols to select. - * - * This invokes the [onChange] callback any time a symbol is clicked on - * or a valid symbol is typed into the text field. - */ -@Composable -fun SymbolSelector( - onChange: (Symbol?) -> Unit, - modifier: Modifier = Modifier, - default: Symbol? = null, - viewModel: SymbolSelectorViewModel = hiltViewModel(), -) { - val current = remember { mutableStateOf(default?.code) } - - Column(modifier = modifier) { - TextField( - value = current.value.orEmpty(), - leadingIcon = { - val icon = current.value - .takeIf { it?.length == 2 } - ?.let { - try { symbolOf(it[0], it[1]) } - catch (e: IllegalArgumentException) { null } - } - ?.let { viewModel.createBitmap(it) } - ?: return@TextField - Image( - bitmap = icon.asImageBitmap(), - contentDescription = null, - modifier = Modifier - .width(24.dp + AckTheme.spacing.icon) - .height(24.dp + AckTheme.spacing.icon) - .padding(AckTheme.spacing.icon), - ) - }, - onValueChange = { new -> - if (new.length > 2) return@TextField - current.value = new - runCatching { symbolOf(new) }.getOrNull()?.run(onChange) - }, - shape = RectangleShape, - modifier = Modifier.fillMaxWidth() - ) - LazyVerticalGrid( - columns = GridCells.Adaptive(minSize = 32.dp), - ) { - items(viewModel.symbols) { symbol -> - val bitmap = viewModel.createBitmap(symbol) - IconButton(onClick = { - current.value = symbol.code - onChange(symbol) - }) { - if (bitmap == null) { - return@IconButton - } - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = null, - modifier = Modifier - .width(24.dp + AckTheme.spacing.icon) - .height(24.dp + AckTheme.spacing.icon) - .padding(AckTheme.spacing.icon), - ) - } - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolSelectorViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolSelectorViewModel.kt deleted file mode 100644 index f94744de..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/symbol/SymbolSelectorViewModel.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.inkapplications.ack.android.symbol - -import android.graphics.Bitmap -import androidx.lifecycle.ViewModel -import com.inkapplications.ack.structures.Symbol -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -/** - * Viewmodel used to in UIs that require symbol rendering. - * - * This provides access to the symbol factory to create bitmaps in Compose - * as well as data access to the available list of symbols in APRS. - */ -@HiltViewModel -class SymbolSelectorViewModel @Inject constructor( - private val symbolFactory: SymbolFactory, -): ViewModel() { - val symbols = Symbol.Primary.ALL + Symbol.Alternate.BASE - fun createBitmap(symbol: Symbol): Bitmap? { - return symbolFactory.createSymbol(symbol) ?: symbolFactory.defaultSymbol - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncActivity.kt b/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncActivity.kt deleted file mode 100644 index ce755510..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncActivity.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.inkapplications.ack.android.tnc - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Intent -import androidx.activity.compose.setContent -import androidx.lifecycle.lifecycleScope -import com.inkapplications.ack.android.trackNavigation -import com.inkapplications.ack.data.drivers.DriverConnectionState -import com.inkapplications.ack.data.drivers.PacketDrivers -import com.inkapplications.android.extensions.ExtendedActivity -import com.inkapplications.android.startActivity -import dagger.hilt.android.AndroidEntryPoint -import kimchi.Kimchi -import kimchi.analytics.stringProperty -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import javax.inject.Inject - -private const val ARG_BACKGROUND_INTENT = "background_intent" - -/** - * Activity that displays bluetooth TNC devices and manages connections. - */ -@AndroidEntryPoint -class ConnectTncActivity: ExtendedActivity(), DeviceListController { - @Inject - lateinit var drivers: PacketDrivers - - private lateinit var backgroundService: Intent - - override fun onCreate() { - super.onCreate() - - Kimchi.trackScreen("connect_tnc") - - backgroundService = intent.getParcelableExtra(ARG_BACKGROUND_INTENT)!! - - setContent { - ConnectTncScreen(this) - } - } - - @SuppressLint("MissingPermission") - override fun onDeviceConnectClick(device: DeviceItem) { - Kimchi.trackEvent("connect_tnc_connect", listOf( - stringProperty("device_name", device.name) - )) - lifecycleScope.launch { - drivers.tncDriver.selectDevice(device.data) - startService(backgroundService) - drivers.tncDriver.connectionState.filter { it == DriverConnectionState.Connected }.first() - finish() - } - } - - override fun onCloseClick() { - Kimchi.trackEvent("connect_tnc_close") - finish() - } -} - -fun Activity.startConnectTncActivity( - backgroundService: Intent, -) { - Kimchi.trackNavigation("connect_tnc") - startActivity(ConnectTncActivity::class) { - putExtra(ARG_BACKGROUND_INTENT, backgroundService) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncScreen.kt deleted file mode 100644 index cbd5acc7..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncScreen.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.inkapplications.ack.android.tnc - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.BluetoothSearching -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.hilt.navigation.compose.hiltViewModel -import com.inkapplications.ack.android.ui.NavigationRow -import com.inkapplications.ack.android.ui.theme.AckScreen -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun ConnectTncScreen( - controller: DeviceListController, -) = AckScreen { - DeviceList(controller = controller) -} - -@Composable -fun DeviceList( - viewModel: ConnectTncViewModel = hiltViewModel(), - controller: DeviceListController, -) { - val state = viewModel.state.collectAsState() - when (val stateValue = state.value) { - ConnectTncState.Initial, - is ConnectTncState.Discovering.Empty -> { - Box( - modifier = Modifier.fillMaxSize() - .padding( - top = WindowInsets.statusBars.asPaddingValues().calculateTopPadding(), - bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - ), - ) { - DeviceHeader(controller::onCloseClick) - Column( - modifier = Modifier.fillMaxSize().padding( - horizontal = AckTheme.spacing.gutter, - ), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - imageVector = Icons.Default.BluetoothSearching, - contentDescription = null, - tint = AckTheme.colors.foregroundInactive, - modifier = Modifier.size(AckTheme.sizing.dispayIcon), - ) - Text("Searching for devices...") - LinearProgressIndicator( - modifier = Modifier.padding(vertical = AckTheme.spacing.item), - ) - } - } - } - is ConnectTncState.Discovering.DeviceList -> { - LazyColumn( - contentPadding = PaddingValues( - bottom = AckTheme.spacing.gutter, - ), - ) { - val rows = listOf(RowType.Header) + stateValue.devices.map { RowType.Device(it) } - items(rows) { row -> - when (row) { - RowType.Header -> DeviceHeader(controller::onCloseClick) - is RowType.Device -> Device( - device = row.device, - onConnectClick = { controller.onDeviceConnectClick(row.device) }, - ) - } - } - } - } - } -} - -@Composable -private fun DeviceHeader( - onBackPress: () -> Unit, -) { - NavigationRow( - title = "Devices", - onBackPressed = onBackPress, - ) -} - -private sealed interface RowType { - object Header: RowType - data class Device(val device: DeviceItem): RowType -} - -@Composable -@OptIn(ExperimentalMaterialApi::class) -private fun Device( - device: DeviceItem, - onConnectClick: () -> Unit, -) { - Card( - onClick = onConnectClick, - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = AckTheme.spacing.gutter, - vertical = AckTheme.spacing.singleItem, - ), - ) { - Box( - modifier = Modifier.padding(AckTheme.spacing.item), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - Column { - Text( - text = device.name, - style = AckTheme.typography.h3, - ) - } - DeviceConnectionOptions( - device = device, - ) - } - } - } -} - -@Composable -private fun DeviceConnectionOptions( - device: DeviceItem, -) = Row( - verticalAlignment = Alignment.CenterVertically, -) { - when (device) { - is DeviceItem.Connecting -> { - Box(Modifier.padding(AckTheme.spacing.clickSafety)) { - CircularProgressIndicator( - modifier = Modifier.size(AckTheme.sizing.iconButton), - ) - } - } - else -> { - Box(Modifier.padding(AckTheme.spacing.clickSafety)) { - Spacer(modifier = Modifier.size(AckTheme.sizing.iconButton)) - } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncState.kt b/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncState.kt deleted file mode 100644 index b27a71ab..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncState.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.inkapplications.ack.android.tnc - -/** - * View data for the TNC connection screen. - */ -sealed interface ConnectTncState { - /** - * Initial state of the screen, before any data is loaded. - */ - object Initial: ConnectTncState - - /** - * State indicating that bluetooth discovery is running. - */ - sealed interface Discovering: ConnectTncState { - /** - * Bluetooth discovery is running, but no compatible devices have been found. - */ - object Empty: Discovering - - /** - * A list of compatible TNC devices and their states. - */ - data class DeviceList(val devices: List): Discovering - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncStateFactory.kt b/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncStateFactory.kt deleted file mode 100644 index 1d9bc804..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncStateFactory.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.inkapplications.ack.android.tnc - -import android.bluetooth.BluetoothDevice -import com.inkapplications.ack.data.drivers.ConnectTncData -import dagger.Reusable -import javax.inject.Inject - -/** - * Create view model data from the current connection state. - */ -@Reusable -class ConnectTncStateFactory @Inject constructor() { - fun create(data: ConnectTncData): ConnectTncState { - val connectedDevice = data.connectedDevice - val connectingDevice = data.connectingDevice - - val devices = data.discoveredDevices.map { - when { - it == connectedDevice -> DeviceItem.Connected( - name = it.name, - data = it, - reconnect = false, - ) - it == connectingDevice -> DeviceItem.Connecting( - name = it.name, - data = it, - ) - it.bondState == BluetoothDevice.BOND_BONDED -> DeviceItem.NotConnected( - name = it.name, - data = it, - reconnect = false, - ) - else -> DeviceItem.Unpaired( - name = it.name, - data = it, - ) - } - } - - return when { - data.discoveredDevices.isEmpty() -> ConnectTncState.Discovering.Empty - else -> ConnectTncState.Discovering.DeviceList(devices) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncViewModel.kt b/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncViewModel.kt deleted file mode 100644 index 48706f76..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/tnc/ConnectTncViewModel.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.inkapplications.ack.android.tnc - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.inkapplications.ack.data.drivers.PacketDrivers -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import javax.inject.Inject - -/** - * Android view model for holding data in the TNC connection screen. - */ -@HiltViewModel -class ConnectTncViewModel @Inject constructor( - drivers: PacketDrivers, - stateFactory: ConnectTncStateFactory, -): ViewModel() { - val state: StateFlow = drivers.tncDriver.deviceData - .map { stateFactory.create(it) } - .stateIn(viewModelScope, SharingStarted.Eagerly, ConnectTncState.Initial) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/tnc/DeviceItem.kt b/android-application/src/main/java/com/inkapplications/ack/android/tnc/DeviceItem.kt deleted file mode 100644 index b3e29ebb..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/tnc/DeviceItem.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.inkapplications.ack.android.tnc - -import com.inkapplications.android.extensions.bluetooth.BluetoothDeviceData - -/** - * Represents a bluetooth device and its connection state. - */ -sealed interface DeviceItem { - /** - * Technical data about the bluetooth device. - */ - val data: BluetoothDeviceData - - /** - * User-friendly name for the device. - */ - val name: String - - /** - * An unpaired bluetooth device. - */ - data class Unpaired( - override val name: String, - override val data: BluetoothDeviceData, - ): DeviceItem - - /** - * A device that is paired, but not connected. - */ - data class NotConnected( - override val name: String, - override val data: BluetoothDeviceData, - /** - * Whether automatic reconnection is enabled for this device. - */ - val reconnect: Boolean, - ): DeviceItem - - /** - * A device that is currently being connected to. - */ - data class Connecting( - override val name: String, - override val data: BluetoothDeviceData, - ): DeviceItem - - /** - * A connected bluetooth device. - */ - data class Connected( - override val name: String, - override val data: BluetoothDeviceData, - /** - * Whether automatic reconnection is enabled for this device. - */ - val reconnect: Boolean, - ): DeviceItem -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/tnc/DeviceListController.kt b/android-application/src/main/java/com/inkapplications/ack/android/tnc/DeviceListController.kt deleted file mode 100644 index d35e3f12..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/tnc/DeviceListController.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.inkapplications.ack.android.tnc - -/** - * Actions that can be taken from the bluetooth device list. - */ -interface DeviceListController { - /** - * Invoked when the user clicks the connect icon for a device. - */ - fun onDeviceConnectClick(device: DeviceItem) - - /** - * Invoked when the user clicks on the close icon for the screen. - */ - fun onCloseClick() -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/transmit/PathTransformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/transmit/PathTransformer.kt deleted file mode 100644 index 560d3d32..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/transmit/PathTransformer.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.inkapplications.ack.android.transmit - -import com.inkapplications.ack.android.settings.transformer.Transformer -import com.inkapplications.ack.structures.Digipeater -import com.inkapplications.ack.structures.station.toStationAddress - -/** - * Transforms a path of digipeaters into a comma-separated string. - */ -object PathTransformer: Transformer, String> { - override fun toStorage(data: List): String { - return data.map { it.address.toString() }.joinToString(",") - } - - override fun toData(storage: String): List { - return storage.split(',') - .map { it.trim() } - .map { it.toStationAddress() } - .map(::Digipeater) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/transmit/PreferenceDriverSettingsProvider.kt b/android-application/src/main/java/com/inkapplications/ack/android/transmit/PreferenceDriverSettingsProvider.kt deleted file mode 100644 index 4b46dac5..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/transmit/PreferenceDriverSettingsProvider.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.inkapplications.ack.android.transmit - -import com.inkapplications.ack.android.connection.ConnectionSettings -import com.inkapplications.ack.android.settings.SettingsReadAccess -import com.inkapplications.ack.android.settings.observeData -import com.inkapplications.ack.android.settings.observeInt -import com.inkapplications.ack.android.settings.observeString -import com.inkapplications.ack.data.AfskModulationConfiguration -import com.inkapplications.ack.data.ConnectionConfiguration -import com.inkapplications.ack.data.drivers.DriverSettingsProvider -import dagger.Reusable -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -@Reusable -class PreferenceDriversSettingsProvider @Inject constructor( - settings: SettingsReadAccess, - connectionSettings: ConnectionSettings, - transmitSettings: TransmitSettings, - logger: KimchiLogger, -): DriverSettingsProvider { - override val internetServiceConfiguration: Flow = settings.observeData(connectionSettings.address) - .filterNotNull() - .map { ConnectionConfiguration(it) } - .combine(settings.observeData(connectionSettings.passcode)) { settings, passcode -> - settings.copy(passcode = passcode?.value ?: -1) - } - .combine(settings.observeString(connectionSettings.server)) { settings, server -> - settings.copy(host = server) - } - .combine(settings.observeInt(connectionSettings.port)) { settings, port -> - settings.copy(port = port) - } - .combine(settings.observeData(connectionSettings.radius)) { settings, radius -> - settings.copy(searchRadius = radius) - } - .onEach { logger.debug("New Internet Service Configuration Received: $it") } - - override val afskConfiguration: Flow = settings.observeData(transmitSettings.preamble) - .combine(settings.observeData(transmitSettings.volume)) { preamble, volume -> - AfskModulationConfiguration( - preamble = preamble, - volume = volume, - ) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/transmit/SymbolTransformer.kt b/android-application/src/main/java/com/inkapplications/ack/android/transmit/SymbolTransformer.kt deleted file mode 100644 index c4caa58c..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/transmit/SymbolTransformer.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.inkapplications.ack.android.transmit - -import com.inkapplications.ack.android.settings.transformer.Transformer -import com.inkapplications.ack.structures.Symbol -import com.inkapplications.ack.structures.code -import com.inkapplications.ack.structures.symbolOf - -/** - * Transform an APRS symbol into its 2-char ASCII code. - */ -object SymbolTransformer: Transformer { - override fun toStorage(data: Symbol): String = data.code - - override fun toData(storage: String): Symbol = symbolOf(storage) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/transmit/TransmitModule.kt b/android-application/src/main/java/com/inkapplications/ack/android/transmit/TransmitModule.kt deleted file mode 100644 index c9941c16..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/transmit/TransmitModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.inkapplications.ack.android.transmit - -import com.inkapplications.ack.android.settings.SettingsProvider -import com.inkapplications.ack.data.drivers.DriverSettingsProvider -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import dagger.multibindings.IntoSet - -@Module -@InstallIn(SingletonComponent::class) -abstract class TransmitModule { - @Binds - @IntoSet - abstract fun settings(settings: TransmitSettings): SettingsProvider - - @Binds - abstract fun driverSettings(settings: PreferenceDriversSettingsProvider): DriverSettingsProvider -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/transmit/TransmitSettings.kt b/android-application/src/main/java/com/inkapplications/ack/android/transmit/TransmitSettings.kt deleted file mode 100644 index 6c33d912..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/transmit/TransmitSettings.kt +++ /dev/null @@ -1,146 +0,0 @@ -package com.inkapplications.ack.android.transmit - -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.input.* -import com.inkapplications.ack.android.settings.* -import com.inkapplications.ack.android.settings.transformer.* -import com.inkapplications.ack.structures.Digipeater -import com.inkapplications.ack.structures.Symbol -import com.inkapplications.ack.structures.station.StationAddress -import com.inkapplications.android.extensions.StringResources -import inkapplications.spondee.measure.us.miles -import inkapplications.spondee.scalar.Percentage -import inkapplications.spondee.scalar.percent -import inkapplications.spondee.scalar.toWholePercentage -import inkapplications.spondee.structure.roundToInt -import javax.inject.Inject -import kotlin.time.Duration.Companion.minutes -import kotlin.time.Duration.Companion.seconds - -class TransmitSettings @Inject constructor( - resources: StringResources, -): SettingsProvider { - val minRate = IntBackedSetting( - key = "transmit.rate.min", - name = resources.getString(R.string.transmit_settings_rate_min), - defaultData = 10.minutes, - categoryName = resources.getString(R.string.transmit_settings_category), - storageTransformer = MinuteTransformer, - inputValidator = IntegerValidator( - error = resources.getString(R.string.input_validator_positive_integer_error), - zeroInclusive = false, - ), - ) - - val maxRate = IntBackedSetting( - key = "transmit.rate.max", - name = resources.getString(R.string.transmit_settings_rate_max), - defaultData = 5.minutes, - categoryName = resources.getString(R.string.transmit_settings_category), - storageTransformer = MinuteTransformer, - inputValidator = IntegerValidator( - error = resources.getString(R.string.input_validator_positive_integer_error), - zeroInclusive = false, - ), - ) - - val distance = IntBackedSetting( - key = "transmit.distance", - name = resources.getString(R.string.transmit_settings_distance), - defaultData = 5.miles, - categoryName = resources.getString(R.string.transmit_settings_category), - storageTransformer = MileTransformer, - inputValidator = IntegerValidator( - error = resources.getString(R.string.input_validator_positive_integer_error), - zeroInclusive = false, - ), - ) - - val preamble = IntBackedSetting( - key = "transmit.preamble", - name = resources.getString(R.string.transmit_settings_preamble), - defaultData = 1.seconds, - categoryName = resources.getString(R.string.transmit_settings_category), - visibility = SettingVisibility.Advanced, - storageTransformer = MillisecondTransformer, - inputValidator = IntegerValidator( - error = resources.getString(R.string.input_validator_positive_integer_error), - zeroInclusive = false, - ), - ) - - val digipath = StringBackedSetting( - key = "transmit.digipath", - name = resources.getString(R.string.transmit_settings_digipath), - defaultData = StationAddress("WIDE1", "1").let(::Digipeater).let(::listOf), - visibility = SettingVisibility.Advanced, - categoryName = resources.getString(R.string.transmit_settings_category), - storageTransformer = PathTransformer, - ) - - val symbol = StringBackedSetting( - key = "transmit.symbol.normalized", - name = resources.getString(R.string.transmit_settings_symbol), - defaultData = Symbol.Primary('$'), - categoryName = resources.getString(R.string.transmit_settings_category), - storageTransformer = SymbolTransformer, - inputValidator = MinLengthValidator( - minLength = 2, - error = resources.getString(R.string.transmit_settings_symbol_length), - ) + MaxLengthValidator( - maxLength = 2, - error = resources.getString(R.string.transmit_settings_symbol_length), - ), - ) - - val destination = StringBackedSetting( - key = "transmit.destination", - name = resources.getString(R.string.transmit_settings_destination), - defaultData = StationAddress("APZ022"), - categoryName = resources.getString(R.string.transmit_settings_category), - visibility = SettingVisibility.Advanced, - storageTransformer = StationAddressTransformer, - ) - - val comment = StringSetting( - key = "transmit.comment", - name = resources.getString(R.string.transmit_settings_comment), - defaultValue = "Hello!", - categoryName = resources.getString(R.string.transmit_settings_category), - validator = MaxLengthValidator( - maxLength = 43, - error = resources.getString(R.string.transmit_settings_comment_length), - ), - ) - - val volume = IntBackedSetting( - key = "transmit.volume", - name = resources.getString(R.string.transmit_settings_volume), - defaultData = 50.percent, - categoryName = resources.getString(R.string.transmit_settings_category), - storageTransformer = PercentageTransformer, - dataValidator = object: Validator { - override fun validate(input: Percentage): ValidationResult { - return if (input.toWholePercentage().roundToInt() in (0..100)) { - ValidationResult.Valid - } else { - ValidationResult.Error( - message = resources.getString(R.string.transmit_settings_volume_range_error), - ) - } - } - } - ) - - override val settings: List = listOf( - minRate, - maxRate, - distance, - preamble, - digipath, - symbol, - destination, - comment, - volume, - ) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/AckChip.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/AckChip.kt deleted file mode 100644 index 47493441..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/AckChip.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.inkapplications.ack.android.ui - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.inkapplications.ack.android.ui.theme.AckTheme - -/** - * Rounded UI element, similar to a button, but intended to also display state. - */ -@Composable -@OptIn(ExperimentalMaterialApi::class) -fun AckChip( - modifier: Modifier = Modifier, - onClick: (() -> Unit)? = null, - border: BorderStroke? = BorderStroke(1.dp, AckTheme.colors.foregroundInactive), - content: @Composable () -> Unit, -) { - if (onClick != null) { - Surface( - shape = RoundedCornerShape(24.dp), - elevation = 1.dp, - border = border, - onClick = onClick, - modifier = modifier, - ) { - Row( - verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, - modifier = Modifier.padding(AckTheme.spacing.content), - ) { content() } - } - } else { - Surface( - shape = RoundedCornerShape(24.dp), - elevation = 1.dp, - border = border, - modifier = modifier, - ) { - Row( - verticalAlignment = androidx.compose.ui.Alignment.CenterVertically, - modifier = Modifier.padding(AckTheme.spacing.content), - ) { content() } - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/AprsSymbol.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/AprsSymbol.kt deleted file mode 100644 index fb25022e..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/AprsSymbol.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.inkapplications.ack.android.capture.log - -import android.graphics.Bitmap -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.unit.dp -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun AprsSymbol(symbol: Bitmap?) { - if (symbol != null) { - Image( - bitmap = symbol.asImageBitmap(), - contentDescription = null, - modifier = Modifier - .width(24.dp + AckTheme.spacing.icon) - .height(24.dp + AckTheme.spacing.icon) - .padding(AckTheme.spacing.icon) - ) - } else { - Box( - modifier = Modifier.width(24.dp).height(24.dp).padding(AckTheme.spacing.icon) - ) {} - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/CallsignChip.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/CallsignChip.kt deleted file mode 100644 index b81b8dad..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/CallsignChip.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.inkapplications.ack.android.ui - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Verified -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun CallsignChip( - callsign: String, - verified: Boolean, - onClick: (() -> Unit)? = null, - border: BorderStroke? = null, -) { - if (onClick != null) { - AckChip( - border = border, - onClick = onClick, - ) { - CallsignChipContent(callsign, verified) - } - } else { - AckChip( - border = border, - ) { - CallsignChipContent(callsign, verified) - } - } -} - -@Composable -private fun CallsignChipContent( - callsign: String, - verified: Boolean, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - if (verified) Icon( - Icons.Default.Verified, - stringResource(R.string.settings_callsign_verified_description), - modifier = Modifier.padding(end = AckTheme.spacing.icon) - ) - Text(callsign, style = AckTheme.typography.h2) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/EmptyBox.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/EmptyBox.kt deleted file mode 100644 index 62c67ab6..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/EmptyBox.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.inkapplications.ack.android.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import com.inkapplications.ack.android.ui.theme.AckTheme - -/** - * Placeholder with an icon and caption to explain that there is no data loaded - * for this section of the app yet. - * - * @param icon A large icon to place in the middle of the screen - * @param caption A snippet of text to explain what data is missing. - */ -@Composable -fun EmptyBox( - icon: ImageVector, - caption: String, - modifier: Modifier = Modifier, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxSize() - .then(modifier) - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = AckTheme.colors.foregroundInactive, - modifier = Modifier.size(AckTheme.sizing.dispayIcon), - ) - Text(caption) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/IconRow.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/IconRow.kt deleted file mode 100644 index fc2b6be8..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/IconRow.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.inkapplications.ack.android.ui - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.vector.ImageVector -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun IconRow( - icon: ImageVector, - text: String, - modifier: Modifier = Modifier, -) = Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { - Icon(icon, null, tint = AckTheme.colors.accent, modifier = Modifier.padding(AckTheme.spacing.icon)) - Text(text) -} - -@Composable -fun IconRow( - icon: Painter, - text: String, - modifier: Modifier = Modifier, -) = Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { - Icon(icon, null, tint = AckTheme.colors.accent, modifier = Modifier.padding(AckTheme.spacing.icon)) - Text(text) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/KeyValueRow.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/KeyValueRow.kt deleted file mode 100644 index 1a8026a4..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/KeyValueRow.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.inkapplications.ack.android.ui - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun KeyValueRow( - label: String, - value: String, - modifier: Modifier = Modifier, -) = Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { - Text(label, style = AckTheme.typography.caption, color = AckTheme.colors.accent, modifier = Modifier.padding(end = AckTheme.spacing.icon)) - Text(value, style = AckTheme.typography.body) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/LabelledIconButton.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/LabelledIconButton.kt deleted file mode 100644 index 3f15cc12..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/LabelledIconButton.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.inkapplications.ack.android.ui - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun LabelledIconButton( - icon: ImageVector, - title: String, - onClick: (() -> Unit) = {}, - backgroundColor: Color = AckTheme.colors.accent, - iconColor: Color = AckTheme.colors.onAccent, - border: BorderStroke? = null, - enabled: Boolean = true, - modifier: Modifier = Modifier, -) = Box(modifier) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .padding(vertical = AckTheme.spacing.clickSafety, horizontal = AckTheme.spacing.gutter) - ) { - Button( - onClick = onClick, - shape = CircleShape, - colors = ButtonDefaults.buttonColors( - backgroundColor = backgroundColor, - contentColor = iconColor, - ), - border = border, - enabled = enabled, - contentPadding = PaddingValues(8.dp), - modifier = Modifier.size(60.dp), - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = iconColor, - modifier = Modifier.padding(AckTheme.spacing.icon), - ) - } - Text( - text = title, - style = AckTheme.typography.caption, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = AckTheme.spacing.item).width(70.dp) - ) - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/NavigationRow.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/NavigationRow.kt deleted file mode 100644 index e698c9c3..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/NavigationRow.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.inkapplications.ack.android.ui - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.inkapplications.ack.android.ui.theme.AckTheme - -@Composable -fun NavigationRow( - title: String, - onBackPressed: (() -> Unit)? = null, -) = NavigationRow( - title = { - Text( - text = title, - style = AckTheme.typography.h1, - modifier = Modifier.padding( - start = AckTheme.spacing.item, - ) - ) - }, - onBackPressed = onBackPressed, -) - -@Composable -fun NavigationRow( - title: @Composable () -> Unit, - onBackPressed: (() -> Unit)? = null, -) = Row ( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding( - start = 0.dp, - top = AckTheme.spacing.gutter, - end = AckTheme.spacing.gutter, - bottom = AckTheme.spacing.content, - ) -) { - if (onBackPressed != null) { - IconButton( - onClick = { onBackPressed() }, - Modifier.padding(start = 0.dp) - ) { - Icon(Icons.Default.ArrowBack, "") - } - } else { - Spacer(Modifier.width(AckTheme.spacing.item)) - } - title() -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/SelectionRow.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/SelectionRow.kt deleted file mode 100644 index ca114391..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/SelectionRow.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.inkapplications.ack.android.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.inkapplications.ack.android.R -import com.inkapplications.ack.android.ui.theme.AckTheme - -/** - * Clickable row with a selection state. - * - * This is designed to be used in a selection screen or pop up, to indicate - * a list of options with one or more items in a "selected" state. - * - * @param name User-readable string displayed in this row. - * @param selected Whether to indicate the row as currently selected/enabled. - * @param onClick Invoked when the user clicks the row. - */ -@Composable -fun SelectionRow( - name: String, - selected: Boolean, - onClick: () -> Unit, -) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .clickable { onClick() } - .fillMaxWidth() - .padding(vertical = AckTheme.spacing.clickSafety, horizontal = AckTheme.spacing.gutter), - ) { - Text(name, style = AckTheme.typography.h2) - - if (selected) { - Icon( - Icons.Default.Check, - contentDescription = stringResource(id = R.string.state_selected_description), - ) - } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/StateLabelledIconButton.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/StateLabelledIconButton.kt deleted file mode 100644 index 16d3311c..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/StateLabelledIconButton.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.inkapplications.ack.android.ui - -import androidx.compose.foundation.BorderStroke -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.unit.dp -import com.inkapplications.ack.android.ui.theme.AckTheme -import com.inkapplications.android.extensions.control.ControlState - -@Composable -fun StateLabelledIconButton( - icon: ImageVector, - title: String, - onClick: ((ControlState) -> Unit) = {}, - state: ControlState, - modifier: Modifier = Modifier, -) = when (state) { - ControlState.On -> LabelledIconButton( - title = title, - icon = icon, - backgroundColor = AckTheme.colors.accent, - iconColor = AckTheme.colors.onAccent, - onClick = { onClick(state) }, - modifier = modifier, - ) - ControlState.Off -> LabelledIconButton( - title = title, - icon = icon, - onClick = { onClick(state) }, - modifier = modifier, - backgroundColor = AckTheme.colors.surface, - border = BorderStroke(1.dp, AckTheme.colors.accent), - iconColor = AckTheme.colors.foregroundInactive, - ) - ControlState.Disabled -> LabelledIconButton( - title = title, - icon = icon, - onClick = { onClick(state) }, - modifier = modifier.alpha(.6f), - enabled = false, - backgroundColor = AckTheme.colors.surface, - border = BorderStroke(1.dp, AckTheme.colors.foregroundInactive), - iconColor = AckTheme.colors.foregroundInactive, - ) - ControlState.Hidden -> {} -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/TelemetryTable.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/TelemetryTable.kt deleted file mode 100644 index 1fd596ef..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/TelemetryTable.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.inkapplications.ack.android.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Card -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.inkapplications.ack.android.ui.theme.AckTheme -import com.inkapplications.ack.structures.TelemetryValues - -@Composable -fun TelemetryTable(values: TelemetryValues, sequence: String?) { - Card(modifier = Modifier.fillMaxWidth().padding(vertical = AckTheme.spacing.content)) { - Column(modifier = Modifier.padding(AckTheme.spacing.content)) { - Text("Telemetry Data", style = AckTheme.typography.h2) - TelemetryValueRow("a1", values.analog1.toString()) - TelemetryValueRow("a2", values.analog2.toString()) - TelemetryValueRow("a3", values.analog3.toString()) - TelemetryValueRow("a4", values.analog4.toString()) - TelemetryValueRow("a5", values.analog5.toString()) - TelemetryValueRow("d1", values.digital.toString()) - TelemetryValueRow("sq", sequence.orEmpty()) - } - } -} - -@Composable -private fun TelemetryValueRow( - label: String, - value: String, -) = KeyValueRow(label, value, Modifier.padding(vertical = AckTheme.spacing.singleItem)) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/AckScreen.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/AckScreen.kt deleted file mode 100644 index 7d5c0892..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/AckScreen.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.inkapplications.ack.android.ui.theme - -import androidx.compose.material.LocalContentColor -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider - -@Composable -fun AckScreen(content: @Composable () -> Unit) { - MaterialTheme( - colors = AckTheme.colors.materialColors, - typography = AckTheme.typography.material, - ) { - CompositionLocalProvider( - LocalContentColor provides AckTheme.colors.foreground, - ) { content() } - } -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/AckTheme.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/AckTheme.kt deleted file mode 100644 index 65057055..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/AckTheme.kt +++ /dev/null @@ -1,131 +0,0 @@ -package com.inkapplications.ack.android.ui.theme - -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp - -object AckTheme { - private val primaryColor - @Composable - @ReadOnlyComposable - get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - colorResource(android.R.color.system_accent1_400) - } else { - ColorPalette.brand - } - - val darkColors - @Composable - @ReadOnlyComposable - get() = ColorVariant( - accent = primaryColor, - onAccent = ColorPalette.darkStroke, - foreground = ColorPalette.lightStroke, - background = ColorPalette.darkStroke, - surface = ColorPalette.darkStrokeSoftened, - error = ColorPalette.red, - onError = ColorPalette.lightStroke, - light = false, - warnForeground = ColorPalette.lightWarning, - dangerForeground = ColorPalette.red, - ) - - val lightColors - @Composable - @ReadOnlyComposable - get() = darkColors.copy( - foreground = ColorPalette.darkStroke, - background = ColorPalette.lightStroke, - surface = ColorPalette.lightStrokeSoftened, - warnForeground = ColorPalette.darkWarning, - dangerForeground = ColorPalette.red, - ) - - val colors - @Composable - @ReadOnlyComposable - get() = if (isSystemInDarkTheme() || Build.VERSION.SDK_INT < Build.VERSION_CODES.N) darkColors else lightColors - - val spacing - @Composable - @ReadOnlyComposable - get() = SpacingVariant( - gutter = 16.dp, - content = 8.dp, - item = 8.dp, - icon = 8.dp, - clickSafety = 16.dp, - ) - - val sizing - @Composable - @ReadOnlyComposable - get() = SizingVariant( - dispayIcon = 120.dp, - displayDecorationIcon = 80.dp, - iconButton = 16.dp, - ) - - val shapes - @Composable - @ReadOnlyComposable - get() = ShapeVariant( - corners = RoundedCornerShape(8.dp), - ) - - val typography - @Composable - @ReadOnlyComposable - get() = TypographyVariant( - display = TextStyle( - fontFamily = AprsFonts.titleFont, - fontWeight = FontWeight.Normal, - fontSize = 48.sp, - letterSpacing = 0.25.sp - ), - h1 = TextStyle( - fontFamily = AprsFonts.titleFont, - fontWeight = FontWeight.Normal, - fontSize = 34.sp, - letterSpacing = 0.25.sp - ), - h2 = TextStyle( - fontFamily = AprsFonts.titleFont, - fontWeight = FontWeight.Normal, - fontSize = 24.sp, - letterSpacing = 0.sp - ), - h3 = TextStyle( - fontFamily = AprsFonts.titleFont, - fontWeight = FontWeight.Medium, - fontSize = 20.sp, - letterSpacing = 0.15.sp - ), - body = TextStyle( - fontFamily = AprsFonts.contentFont, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - letterSpacing = 0.5.sp - ), - caption = TextStyle( - fontFamily = AprsFonts.contentFont, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - letterSpacing = 0.4.sp - ), - errorCaption = TextStyle( - fontFamily = AprsFonts.contentFont, - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - letterSpacing = 0.4.sp, - color = colors.error - ) - ) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/AprsFonts.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/AprsFonts.kt deleted file mode 100644 index 19c812de..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/AprsFonts.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.inkapplications.ack.android.ui.theme - -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import com.inkapplications.ack.android.R - -object AprsFonts { - val titleFont = FontFamily( - Font(R.font.anonymous_pro, FontWeight.Normal), - Font(R.font.anonymous_pro_bold, FontWeight.Bold), - ) - val contentFont = FontFamily( - Font(R.font.lato_regular, FontWeight.Normal), - ) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/ColorPalette.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/ColorPalette.kt deleted file mode 100644 index f4cf30bb..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/ColorPalette.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.inkapplications.ack.android.ui.theme - -import androidx.compose.ui.graphics.Color - -object ColorPalette { - val brand = Color(0xFFFFa72B) - val brandLight = Color(0xFFFCB652) - val brandDark = Color(0xFFB16D0D) - - val darkStroke = Color(0xFF212121) - val darkStroke70 = Color(0x70212121) - val darkStrokeSoftened = Color(0xFF292929) - - val lightStroke = Color(0xFFFFFFFF) - val lightStroke70 = Color(0x70FFFFFF) - val lightStrokeSoftened = Color(0xFFF2F2F2) - - val darkWarning = Color(0xFFB16D0D) - val lightWarning = Color(0xFFFCB652) - - val red = Color(0xFFFF432B) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/ColorVariant.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/ColorVariant.kt deleted file mode 100644 index d4cb2657..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/ColorVariant.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.inkapplications.ack.android.ui.theme - -import androidx.compose.material.Colors -import androidx.compose.ui.graphics.Color - -data class ColorVariant( - val accent: Color, - val onAccent: Color, - val foreground: Color, - val background: Color, - val surface: Color, - val error: Color, - val onError: Color, - val light: Boolean, - val warnForeground: Color, - val dangerForeground: Color, -) { - val foregroundInactive = foreground.copy(alpha = .5f) - val surfaceInactive = surface.copy(alpha = .5f) - - val materialColors = Colors( - primary = accent, - primaryVariant = accent, - secondary = accent, - secondaryVariant = accent, - background = background, - surface = background, - error = error, - onPrimary = onAccent, - onSecondary = onAccent, - onBackground = foreground, - onSurface = foreground, - onError = onError, - isLight = light, - ) -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/ShapeVariant.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/ShapeVariant.kt deleted file mode 100644 index 4fe4304e..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/ShapeVariant.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.inkapplications.ack.android.ui.theme - -import androidx.compose.ui.graphics.Shape - -/** - * Defines the shapes used in the application. - * - * @param corners Corner radii used for dialogs and other surfaces. - */ -data class ShapeVariant( - val corners: Shape, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/SizingVariant.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/SizingVariant.kt deleted file mode 100644 index 3bca0555..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/SizingVariant.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.inkapplications.ack.android.ui.theme - -import androidx.compose.ui.unit.Dp - -data class SizingVariant( - val dispayIcon: Dp, - val displayDecorationIcon: Dp, - val iconButton: Dp, -) diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/SpacingVariant.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/SpacingVariant.kt deleted file mode 100644 index 5947a58a..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/SpacingVariant.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.inkapplications.ack.android.ui.theme - -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -data class SpacingVariant( - val gutter: Dp, - val content: Dp, - val item: Dp, - val icon: Dp, - val clickSafety: Dp, - val bottomBarHeight: Dp = 60.dp, - val fabSize: Dp = 40.dp, -) { - val navigationProtection = bottomBarHeight + (fabSize / 2) + content - val singleItem: Dp = item / 2 -} diff --git a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/TypographyVariant.kt b/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/TypographyVariant.kt deleted file mode 100644 index af654ab2..00000000 --- a/android-application/src/main/java/com/inkapplications/ack/android/ui/theme/TypographyVariant.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.inkapplications.ack.android.ui.theme - -import androidx.compose.material.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight - -data class TypographyVariant( - val display: TextStyle, - val h1: TextStyle, - val h2: TextStyle, - val h3: TextStyle, - val body: TextStyle, - val caption: TextStyle, - val errorCaption: TextStyle, -) { - val material = Typography( - defaultFontFamily = AprsFonts.contentFont, - h1 = h1, - h2 = h2, - h3 = h3, - h4 = h1, - h5 = h2, - h6 = h3, - subtitle1 = caption, - subtitle2 = caption, - body1 = body, - body2 = body.copy(fontWeight = FontWeight.Bold), - button = body, - caption = caption, - overline = body, - ) -} diff --git a/android-application/src/main/res/drawable-nodpi/symbol_0.png b/android-application/src/main/res/drawable-nodpi/symbol_0.png deleted file mode 100644 index 210ea853..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_0.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_1.png b/android-application/src/main/res/drawable-nodpi/symbol_1.png deleted file mode 100644 index 26e8bd7f..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_1.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_10.png b/android-application/src/main/res/drawable-nodpi/symbol_10.png deleted file mode 100644 index 62be5e34..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_10.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_100.png b/android-application/src/main/res/drawable-nodpi/symbol_100.png deleted file mode 100644 index a1b79390..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_100.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_101.png b/android-application/src/main/res/drawable-nodpi/symbol_101.png deleted file mode 100644 index fdaffe38..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_101.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_102.png b/android-application/src/main/res/drawable-nodpi/symbol_102.png deleted file mode 100644 index 96dafa84..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_102.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_103.png b/android-application/src/main/res/drawable-nodpi/symbol_103.png deleted file mode 100644 index 8f97077b..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_103.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_104.png b/android-application/src/main/res/drawable-nodpi/symbol_104.png deleted file mode 100644 index 8b4473d8..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_104.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_105.png b/android-application/src/main/res/drawable-nodpi/symbol_105.png deleted file mode 100644 index 7eec96f3..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_105.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_106.png b/android-application/src/main/res/drawable-nodpi/symbol_106.png deleted file mode 100644 index 8b373175..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_106.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_107.png b/android-application/src/main/res/drawable-nodpi/symbol_107.png deleted file mode 100644 index 0427f9e1..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_107.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_108.png b/android-application/src/main/res/drawable-nodpi/symbol_108.png deleted file mode 100644 index f6cd5149..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_108.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_109.png b/android-application/src/main/res/drawable-nodpi/symbol_109.png deleted file mode 100644 index 111bb898..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_109.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_11.png b/android-application/src/main/res/drawable-nodpi/symbol_11.png deleted file mode 100644 index bce59d80..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_11.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_110.png b/android-application/src/main/res/drawable-nodpi/symbol_110.png deleted file mode 100644 index 02a3c498..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_110.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_111.png b/android-application/src/main/res/drawable-nodpi/symbol_111.png deleted file mode 100644 index 7e8b5473..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_111.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_112.png b/android-application/src/main/res/drawable-nodpi/symbol_112.png deleted file mode 100644 index 3783dae0..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_112.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_113.png b/android-application/src/main/res/drawable-nodpi/symbol_113.png deleted file mode 100644 index e64f65c7..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_113.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_114.png b/android-application/src/main/res/drawable-nodpi/symbol_114.png deleted file mode 100644 index bcc7fbb3..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_114.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_115.png b/android-application/src/main/res/drawable-nodpi/symbol_115.png deleted file mode 100644 index 068bca26..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_115.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_116.png b/android-application/src/main/res/drawable-nodpi/symbol_116.png deleted file mode 100644 index f35baf2e..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_116.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_117.png b/android-application/src/main/res/drawable-nodpi/symbol_117.png deleted file mode 100644 index 3f2c70d9..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_117.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_118.png b/android-application/src/main/res/drawable-nodpi/symbol_118.png deleted file mode 100644 index 32f224b7..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_118.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_119.png b/android-application/src/main/res/drawable-nodpi/symbol_119.png deleted file mode 100644 index bc68adcb..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_119.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_12.png b/android-application/src/main/res/drawable-nodpi/symbol_12.png deleted file mode 100644 index be5a9a48..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_12.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_120.png b/android-application/src/main/res/drawable-nodpi/symbol_120.png deleted file mode 100644 index 405d6f0e..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_120.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_121.png b/android-application/src/main/res/drawable-nodpi/symbol_121.png deleted file mode 100644 index 1fbe60dd..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_121.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_122.png b/android-application/src/main/res/drawable-nodpi/symbol_122.png deleted file mode 100644 index 90bd600d..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_122.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_123.png b/android-application/src/main/res/drawable-nodpi/symbol_123.png deleted file mode 100644 index ef117b50..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_123.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_124.png b/android-application/src/main/res/drawable-nodpi/symbol_124.png deleted file mode 100644 index 7c1642b5..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_124.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_125.png b/android-application/src/main/res/drawable-nodpi/symbol_125.png deleted file mode 100644 index 9c121d54..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_125.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_126.png b/android-application/src/main/res/drawable-nodpi/symbol_126.png deleted file mode 100644 index 0ed45ea3..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_126.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_127.png b/android-application/src/main/res/drawable-nodpi/symbol_127.png deleted file mode 100644 index 423a8248..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_127.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_128.png b/android-application/src/main/res/drawable-nodpi/symbol_128.png deleted file mode 100644 index 867e7b56..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_128.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_129.png b/android-application/src/main/res/drawable-nodpi/symbol_129.png deleted file mode 100644 index 83df2799..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_129.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_13.png b/android-application/src/main/res/drawable-nodpi/symbol_13.png deleted file mode 100644 index f26b2ed4..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_13.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_130.png b/android-application/src/main/res/drawable-nodpi/symbol_130.png deleted file mode 100644 index c7ae8acc..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_130.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_131.png b/android-application/src/main/res/drawable-nodpi/symbol_131.png deleted file mode 100644 index 2cab788e..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_131.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_132.png b/android-application/src/main/res/drawable-nodpi/symbol_132.png deleted file mode 100644 index 76a7a17d..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_132.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_133.png b/android-application/src/main/res/drawable-nodpi/symbol_133.png deleted file mode 100644 index 621c150c..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_133.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_134.png b/android-application/src/main/res/drawable-nodpi/symbol_134.png deleted file mode 100644 index 57702f92..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_134.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_135.png b/android-application/src/main/res/drawable-nodpi/symbol_135.png deleted file mode 100644 index 4177bee8..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_135.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_136.png b/android-application/src/main/res/drawable-nodpi/symbol_136.png deleted file mode 100644 index 7bd8f166..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_136.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_137.png b/android-application/src/main/res/drawable-nodpi/symbol_137.png deleted file mode 100644 index 55a7d171..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_137.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_138.png b/android-application/src/main/res/drawable-nodpi/symbol_138.png deleted file mode 100644 index ae3365ae..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_138.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_139.png b/android-application/src/main/res/drawable-nodpi/symbol_139.png deleted file mode 100644 index 9b5ddc8e..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_139.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_14.png b/android-application/src/main/res/drawable-nodpi/symbol_14.png deleted file mode 100644 index fd82b6d9..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_14.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_140.png b/android-application/src/main/res/drawable-nodpi/symbol_140.png deleted file mode 100644 index 89f5d858..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_140.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_141.png b/android-application/src/main/res/drawable-nodpi/symbol_141.png deleted file mode 100644 index f5eaaf83..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_141.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_142.png b/android-application/src/main/res/drawable-nodpi/symbol_142.png deleted file mode 100644 index 2eb77e73..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_142.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_143.png b/android-application/src/main/res/drawable-nodpi/symbol_143.png deleted file mode 100644 index cd50462c..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_143.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_144.png b/android-application/src/main/res/drawable-nodpi/symbol_144.png deleted file mode 100644 index a1544591..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_144.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_145.png b/android-application/src/main/res/drawable-nodpi/symbol_145.png deleted file mode 100644 index 7e946a60..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_145.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_146.png b/android-application/src/main/res/drawable-nodpi/symbol_146.png deleted file mode 100644 index 7045951a..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_146.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_147.png b/android-application/src/main/res/drawable-nodpi/symbol_147.png deleted file mode 100644 index 68f4a7dc..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_147.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_148.png b/android-application/src/main/res/drawable-nodpi/symbol_148.png deleted file mode 100644 index 616ad5f9..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_148.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_149.png b/android-application/src/main/res/drawable-nodpi/symbol_149.png deleted file mode 100644 index ae22f7a1..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_149.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_15.png b/android-application/src/main/res/drawable-nodpi/symbol_15.png deleted file mode 100644 index 1151118c..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_15.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_150.png b/android-application/src/main/res/drawable-nodpi/symbol_150.png deleted file mode 100644 index 3c1ca5f1..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_150.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_151.png b/android-application/src/main/res/drawable-nodpi/symbol_151.png deleted file mode 100644 index 18f04b8f..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_151.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_152.png b/android-application/src/main/res/drawable-nodpi/symbol_152.png deleted file mode 100644 index 40994599..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_152.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_153.png b/android-application/src/main/res/drawable-nodpi/symbol_153.png deleted file mode 100644 index 52e724a5..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_153.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_154.png b/android-application/src/main/res/drawable-nodpi/symbol_154.png deleted file mode 100644 index e9296614..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_154.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_155.png b/android-application/src/main/res/drawable-nodpi/symbol_155.png deleted file mode 100644 index 2c23a0ea..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_155.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_156.png b/android-application/src/main/res/drawable-nodpi/symbol_156.png deleted file mode 100644 index 846713ef..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_156.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_157.png b/android-application/src/main/res/drawable-nodpi/symbol_157.png deleted file mode 100644 index 87686779..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_157.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_158.png b/android-application/src/main/res/drawable-nodpi/symbol_158.png deleted file mode 100644 index fcae35f8..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_158.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_159.png b/android-application/src/main/res/drawable-nodpi/symbol_159.png deleted file mode 100644 index 8285e69a..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_159.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_16.png b/android-application/src/main/res/drawable-nodpi/symbol_16.png deleted file mode 100644 index 590a1e3d..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_16.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_160.png b/android-application/src/main/res/drawable-nodpi/symbol_160.png deleted file mode 100644 index 70f2add8..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_160.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_161.png b/android-application/src/main/res/drawable-nodpi/symbol_161.png deleted file mode 100644 index bfa17cbe..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_161.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_162.png b/android-application/src/main/res/drawable-nodpi/symbol_162.png deleted file mode 100644 index d0c2d1af..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_162.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_163.png b/android-application/src/main/res/drawable-nodpi/symbol_163.png deleted file mode 100644 index 795691bd..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_163.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_164.png b/android-application/src/main/res/drawable-nodpi/symbol_164.png deleted file mode 100644 index e8fd87b6..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_164.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_165.png b/android-application/src/main/res/drawable-nodpi/symbol_165.png deleted file mode 100644 index d9020584..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_165.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_166.png b/android-application/src/main/res/drawable-nodpi/symbol_166.png deleted file mode 100644 index a2aef6ac..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_166.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_167.png b/android-application/src/main/res/drawable-nodpi/symbol_167.png deleted file mode 100644 index cd2e065d..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_167.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_168.png b/android-application/src/main/res/drawable-nodpi/symbol_168.png deleted file mode 100644 index e6c76b60..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_168.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_169.png b/android-application/src/main/res/drawable-nodpi/symbol_169.png deleted file mode 100644 index 0173ecdb..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_169.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_17.png b/android-application/src/main/res/drawable-nodpi/symbol_17.png deleted file mode 100644 index c81bb7eb..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_17.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_170.png b/android-application/src/main/res/drawable-nodpi/symbol_170.png deleted file mode 100644 index 9423cd53..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_170.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_171.png b/android-application/src/main/res/drawable-nodpi/symbol_171.png deleted file mode 100644 index 95d341d6..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_171.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_172.png b/android-application/src/main/res/drawable-nodpi/symbol_172.png deleted file mode 100644 index a6f4e63c..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_172.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_173.png b/android-application/src/main/res/drawable-nodpi/symbol_173.png deleted file mode 100644 index 0f598023..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_173.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_174.png b/android-application/src/main/res/drawable-nodpi/symbol_174.png deleted file mode 100644 index 35bf4427..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_174.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_175.png b/android-application/src/main/res/drawable-nodpi/symbol_175.png deleted file mode 100644 index 8ed5cc68..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_175.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_176.png b/android-application/src/main/res/drawable-nodpi/symbol_176.png deleted file mode 100644 index 057632b2..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_176.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_177.png b/android-application/src/main/res/drawable-nodpi/symbol_177.png deleted file mode 100644 index ffb316ee..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_177.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_178.png b/android-application/src/main/res/drawable-nodpi/symbol_178.png deleted file mode 100644 index c22ea2dc..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_178.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_179.png b/android-application/src/main/res/drawable-nodpi/symbol_179.png deleted file mode 100644 index 4c9e7988..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_179.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_18.png b/android-application/src/main/res/drawable-nodpi/symbol_18.png deleted file mode 100644 index edb5bd7c..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_18.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_180.png b/android-application/src/main/res/drawable-nodpi/symbol_180.png deleted file mode 100644 index 19f56bc8..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_180.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_181.png b/android-application/src/main/res/drawable-nodpi/symbol_181.png deleted file mode 100644 index 4d413cad..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_181.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_182.png b/android-application/src/main/res/drawable-nodpi/symbol_182.png deleted file mode 100644 index d2188cd2..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_182.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_183.png b/android-application/src/main/res/drawable-nodpi/symbol_183.png deleted file mode 100644 index 9a86bb72..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_183.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_184.png b/android-application/src/main/res/drawable-nodpi/symbol_184.png deleted file mode 100644 index e994ccf3..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_184.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_185.png b/android-application/src/main/res/drawable-nodpi/symbol_185.png deleted file mode 100644 index 8293726c..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_185.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_186.png b/android-application/src/main/res/drawable-nodpi/symbol_186.png deleted file mode 100644 index 778d66b9..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_186.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_187.png b/android-application/src/main/res/drawable-nodpi/symbol_187.png deleted file mode 100644 index ed326998..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_187.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_188.png b/android-application/src/main/res/drawable-nodpi/symbol_188.png deleted file mode 100644 index b4399b80..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_188.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_189.png b/android-application/src/main/res/drawable-nodpi/symbol_189.png deleted file mode 100644 index 5e2b50fa..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_189.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_19.png b/android-application/src/main/res/drawable-nodpi/symbol_19.png deleted file mode 100644 index 9b11d510..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_19.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_190.png b/android-application/src/main/res/drawable-nodpi/symbol_190.png deleted file mode 100644 index 176a0378..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_190.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_191.png b/android-application/src/main/res/drawable-nodpi/symbol_191.png deleted file mode 100644 index 260d2cdb..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_191.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_2.png b/android-application/src/main/res/drawable-nodpi/symbol_2.png deleted file mode 100644 index 4355622e..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_2.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_20.png b/android-application/src/main/res/drawable-nodpi/symbol_20.png deleted file mode 100644 index a88a8299..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_20.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_207.png b/android-application/src/main/res/drawable-nodpi/symbol_207.png deleted file mode 100644 index d36b7869..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_207.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_208.png b/android-application/src/main/res/drawable-nodpi/symbol_208.png deleted file mode 100644 index 16f43e99..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_208.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_209.png b/android-application/src/main/res/drawable-nodpi/symbol_209.png deleted file mode 100644 index e984cdda..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_209.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_21.png b/android-application/src/main/res/drawable-nodpi/symbol_21.png deleted file mode 100644 index 0be31163..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_21.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_210.png b/android-application/src/main/res/drawable-nodpi/symbol_210.png deleted file mode 100644 index c167e4db..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_210.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_211.png b/android-application/src/main/res/drawable-nodpi/symbol_211.png deleted file mode 100644 index 960e94a8..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_211.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_212.png b/android-application/src/main/res/drawable-nodpi/symbol_212.png deleted file mode 100644 index b150fc3c..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_212.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_213.png b/android-application/src/main/res/drawable-nodpi/symbol_213.png deleted file mode 100644 index 7f0c3048..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_213.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_214.png b/android-application/src/main/res/drawable-nodpi/symbol_214.png deleted file mode 100644 index ed3eb8fc..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_214.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_215.png b/android-application/src/main/res/drawable-nodpi/symbol_215.png deleted file mode 100644 index 007206a5..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_215.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_216.png b/android-application/src/main/res/drawable-nodpi/symbol_216.png deleted file mode 100644 index 2eb11517..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_216.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_22.png b/android-application/src/main/res/drawable-nodpi/symbol_22.png deleted file mode 100644 index dfd1fb97..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_22.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_224.png b/android-application/src/main/res/drawable-nodpi/symbol_224.png deleted file mode 100644 index 7de04d12..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_224.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_225.png b/android-application/src/main/res/drawable-nodpi/symbol_225.png deleted file mode 100644 index 3aa23639..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_225.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_226.png b/android-application/src/main/res/drawable-nodpi/symbol_226.png deleted file mode 100644 index 9aac0dca..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_226.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_227.png b/android-application/src/main/res/drawable-nodpi/symbol_227.png deleted file mode 100644 index 679f3488..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_227.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_228.png b/android-application/src/main/res/drawable-nodpi/symbol_228.png deleted file mode 100644 index e8a4cde2..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_228.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_229.png b/android-application/src/main/res/drawable-nodpi/symbol_229.png deleted file mode 100644 index ff2cb03c..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_229.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_23.png b/android-application/src/main/res/drawable-nodpi/symbol_23.png deleted file mode 100644 index ae14dd66..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_23.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_230.png b/android-application/src/main/res/drawable-nodpi/symbol_230.png deleted file mode 100644 index 7ef5c1b8..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_230.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_231.png b/android-application/src/main/res/drawable-nodpi/symbol_231.png deleted file mode 100644 index 92888167..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_231.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_232.png b/android-application/src/main/res/drawable-nodpi/symbol_232.png deleted file mode 100644 index a7b9a450..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_232.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_233.png b/android-application/src/main/res/drawable-nodpi/symbol_233.png deleted file mode 100644 index 017a1d92..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_233.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_234.png b/android-application/src/main/res/drawable-nodpi/symbol_234.png deleted file mode 100644 index 0964e8de..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_234.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_235.png b/android-application/src/main/res/drawable-nodpi/symbol_235.png deleted file mode 100644 index 02bbb710..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_235.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_236.png b/android-application/src/main/res/drawable-nodpi/symbol_236.png deleted file mode 100644 index 3caa619c..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_236.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_237.png b/android-application/src/main/res/drawable-nodpi/symbol_237.png deleted file mode 100644 index e67b5b12..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_237.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_238.png b/android-application/src/main/res/drawable-nodpi/symbol_238.png deleted file mode 100644 index c0a7ae82..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_238.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_239.png b/android-application/src/main/res/drawable-nodpi/symbol_239.png deleted file mode 100644 index 49fe5b50..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_239.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_24.png b/android-application/src/main/res/drawable-nodpi/symbol_24.png deleted file mode 100644 index d2b4e9d7..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_24.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_240.png b/android-application/src/main/res/drawable-nodpi/symbol_240.png deleted file mode 100644 index b0122b7b..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_240.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_241.png b/android-application/src/main/res/drawable-nodpi/symbol_241.png deleted file mode 100644 index 7bbc9dc4..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_241.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_242.png b/android-application/src/main/res/drawable-nodpi/symbol_242.png deleted file mode 100644 index ed705314..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_242.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_243.png b/android-application/src/main/res/drawable-nodpi/symbol_243.png deleted file mode 100644 index 54805671..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_243.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_244.png b/android-application/src/main/res/drawable-nodpi/symbol_244.png deleted file mode 100644 index 96a61f8a..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_244.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_245.png b/android-application/src/main/res/drawable-nodpi/symbol_245.png deleted file mode 100644 index f47a7d95..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_245.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_246.png b/android-application/src/main/res/drawable-nodpi/symbol_246.png deleted file mode 100644 index fd964abe..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_246.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_247.png b/android-application/src/main/res/drawable-nodpi/symbol_247.png deleted file mode 100644 index 28fe6631..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_247.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_248.png b/android-application/src/main/res/drawable-nodpi/symbol_248.png deleted file mode 100644 index a13a1c0c..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_248.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_249.png b/android-application/src/main/res/drawable-nodpi/symbol_249.png deleted file mode 100644 index 0310ff97..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_249.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_25.png b/android-application/src/main/res/drawable-nodpi/symbol_25.png deleted file mode 100644 index d04db3a0..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_25.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_26.png b/android-application/src/main/res/drawable-nodpi/symbol_26.png deleted file mode 100644 index 01109a86..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_26.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_27.png b/android-application/src/main/res/drawable-nodpi/symbol_27.png deleted file mode 100644 index 2e1817b8..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_27.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_28.png b/android-application/src/main/res/drawable-nodpi/symbol_28.png deleted file mode 100644 index b233b4d3..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_28.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_29.png b/android-application/src/main/res/drawable-nodpi/symbol_29.png deleted file mode 100644 index 3932289e..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_29.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_3.png b/android-application/src/main/res/drawable-nodpi/symbol_3.png deleted file mode 100644 index bfbfd55e..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_3.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_30.png b/android-application/src/main/res/drawable-nodpi/symbol_30.png deleted file mode 100644 index 10efc5cb..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_30.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_31.png b/android-application/src/main/res/drawable-nodpi/symbol_31.png deleted file mode 100644 index 10205d92..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_31.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_32.png b/android-application/src/main/res/drawable-nodpi/symbol_32.png deleted file mode 100644 index d7a3ca7f..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_32.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_33.png b/android-application/src/main/res/drawable-nodpi/symbol_33.png deleted file mode 100644 index 18b742ed..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_33.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_34.png b/android-application/src/main/res/drawable-nodpi/symbol_34.png deleted file mode 100644 index 5fdc76bd..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_34.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_35.png b/android-application/src/main/res/drawable-nodpi/symbol_35.png deleted file mode 100644 index 2775f1d5..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_35.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_36.png b/android-application/src/main/res/drawable-nodpi/symbol_36.png deleted file mode 100644 index 992bec8a..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_36.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_37.png b/android-application/src/main/res/drawable-nodpi/symbol_37.png deleted file mode 100644 index 8a39a684..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_37.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_38.png b/android-application/src/main/res/drawable-nodpi/symbol_38.png deleted file mode 100644 index 24fb3260..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_38.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_39.png b/android-application/src/main/res/drawable-nodpi/symbol_39.png deleted file mode 100644 index 5180f9d5..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_39.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_4.png b/android-application/src/main/res/drawable-nodpi/symbol_4.png deleted file mode 100644 index d14d2760..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_4.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_40.png b/android-application/src/main/res/drawable-nodpi/symbol_40.png deleted file mode 100644 index ae4862ee..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_40.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_41.png b/android-application/src/main/res/drawable-nodpi/symbol_41.png deleted file mode 100644 index ae17cee7..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_41.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_42.png b/android-application/src/main/res/drawable-nodpi/symbol_42.png deleted file mode 100644 index 0ae5e5e6..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_42.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_43.png b/android-application/src/main/res/drawable-nodpi/symbol_43.png deleted file mode 100644 index f51e0a10..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_43.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_44.png b/android-application/src/main/res/drawable-nodpi/symbol_44.png deleted file mode 100644 index ed4685a9..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_44.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_45.png b/android-application/src/main/res/drawable-nodpi/symbol_45.png deleted file mode 100644 index 5a674f8d..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_45.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_46.png b/android-application/src/main/res/drawable-nodpi/symbol_46.png deleted file mode 100644 index a5ddeab0..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_46.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_47.png b/android-application/src/main/res/drawable-nodpi/symbol_47.png deleted file mode 100644 index 5ea25be6..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_47.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_48.png b/android-application/src/main/res/drawable-nodpi/symbol_48.png deleted file mode 100644 index 44045e82..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_48.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_49.png b/android-application/src/main/res/drawable-nodpi/symbol_49.png deleted file mode 100644 index db644900..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_49.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_5.png b/android-application/src/main/res/drawable-nodpi/symbol_5.png deleted file mode 100644 index e64a46c7..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_5.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_50.png b/android-application/src/main/res/drawable-nodpi/symbol_50.png deleted file mode 100644 index 1e3514ef..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_50.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_51.png b/android-application/src/main/res/drawable-nodpi/symbol_51.png deleted file mode 100644 index 0f33cefb..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_51.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_52.png b/android-application/src/main/res/drawable-nodpi/symbol_52.png deleted file mode 100644 index 67e85f83..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_52.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_53.png b/android-application/src/main/res/drawable-nodpi/symbol_53.png deleted file mode 100644 index 0b8332cb..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_53.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_54.png b/android-application/src/main/res/drawable-nodpi/symbol_54.png deleted file mode 100644 index 27c9a4f6..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_54.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_55.png b/android-application/src/main/res/drawable-nodpi/symbol_55.png deleted file mode 100644 index 70374308..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_55.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_56.png b/android-application/src/main/res/drawable-nodpi/symbol_56.png deleted file mode 100644 index 708c41fb..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_56.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_57.png b/android-application/src/main/res/drawable-nodpi/symbol_57.png deleted file mode 100644 index a309d781..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_57.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_58.png b/android-application/src/main/res/drawable-nodpi/symbol_58.png deleted file mode 100644 index bee09dd5..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_58.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_59.png b/android-application/src/main/res/drawable-nodpi/symbol_59.png deleted file mode 100644 index 60178333..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_59.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_6.png b/android-application/src/main/res/drawable-nodpi/symbol_6.png deleted file mode 100644 index 555a5f82..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_6.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_60.png b/android-application/src/main/res/drawable-nodpi/symbol_60.png deleted file mode 100644 index 2e81ceca..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_60.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_61.png b/android-application/src/main/res/drawable-nodpi/symbol_61.png deleted file mode 100644 index 47d4e85a..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_61.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_62.png b/android-application/src/main/res/drawable-nodpi/symbol_62.png deleted file mode 100644 index c61c55f0..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_62.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_63.png b/android-application/src/main/res/drawable-nodpi/symbol_63.png deleted file mode 100644 index 7da08ba5..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_63.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_64.png b/android-application/src/main/res/drawable-nodpi/symbol_64.png deleted file mode 100644 index bd7e147a..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_64.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_65.png b/android-application/src/main/res/drawable-nodpi/symbol_65.png deleted file mode 100644 index 67e25573..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_65.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_66.png b/android-application/src/main/res/drawable-nodpi/symbol_66.png deleted file mode 100644 index 4e0569f1..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_66.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_67.png b/android-application/src/main/res/drawable-nodpi/symbol_67.png deleted file mode 100644 index 49f92467..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_67.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_68.png b/android-application/src/main/res/drawable-nodpi/symbol_68.png deleted file mode 100644 index c5ff4abe..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_68.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_69.png b/android-application/src/main/res/drawable-nodpi/symbol_69.png deleted file mode 100644 index 4ab2f334..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_69.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_7.png b/android-application/src/main/res/drawable-nodpi/symbol_7.png deleted file mode 100644 index 385dd85d..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_7.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_70.png b/android-application/src/main/res/drawable-nodpi/symbol_70.png deleted file mode 100644 index 65debdfe..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_70.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_71.png b/android-application/src/main/res/drawable-nodpi/symbol_71.png deleted file mode 100644 index ccf8127c..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_71.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_72.png b/android-application/src/main/res/drawable-nodpi/symbol_72.png deleted file mode 100644 index 80616a53..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_72.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_73.png b/android-application/src/main/res/drawable-nodpi/symbol_73.png deleted file mode 100644 index ed3aaf8f..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_73.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_74.png b/android-application/src/main/res/drawable-nodpi/symbol_74.png deleted file mode 100644 index b464cc65..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_74.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_75.png b/android-application/src/main/res/drawable-nodpi/symbol_75.png deleted file mode 100644 index bcf5f9cb..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_75.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_76.png b/android-application/src/main/res/drawable-nodpi/symbol_76.png deleted file mode 100644 index 83c7a979..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_76.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_77.png b/android-application/src/main/res/drawable-nodpi/symbol_77.png deleted file mode 100644 index e10eb3e3..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_77.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_78.png b/android-application/src/main/res/drawable-nodpi/symbol_78.png deleted file mode 100644 index 233f3196..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_78.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_79.png b/android-application/src/main/res/drawable-nodpi/symbol_79.png deleted file mode 100644 index d56d33d4..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_79.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_8.png b/android-application/src/main/res/drawable-nodpi/symbol_8.png deleted file mode 100644 index 44a2f1ff..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_8.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_80.png b/android-application/src/main/res/drawable-nodpi/symbol_80.png deleted file mode 100644 index ec13176c..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_80.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_81.png b/android-application/src/main/res/drawable-nodpi/symbol_81.png deleted file mode 100644 index be72fcb7..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_81.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_82.png b/android-application/src/main/res/drawable-nodpi/symbol_82.png deleted file mode 100644 index 8170c341..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_82.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_83.png b/android-application/src/main/res/drawable-nodpi/symbol_83.png deleted file mode 100644 index 82aa3aca..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_83.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_84.png b/android-application/src/main/res/drawable-nodpi/symbol_84.png deleted file mode 100644 index 54180da2..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_84.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_85.png b/android-application/src/main/res/drawable-nodpi/symbol_85.png deleted file mode 100644 index e50c34e2..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_85.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_86.png b/android-application/src/main/res/drawable-nodpi/symbol_86.png deleted file mode 100644 index 2144d722..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_86.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_87.png b/android-application/src/main/res/drawable-nodpi/symbol_87.png deleted file mode 100644 index 995df582..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_87.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_88.png b/android-application/src/main/res/drawable-nodpi/symbol_88.png deleted file mode 100644 index fbf5d9e3..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_88.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_89.png b/android-application/src/main/res/drawable-nodpi/symbol_89.png deleted file mode 100644 index f26946ab..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_89.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_9.png b/android-application/src/main/res/drawable-nodpi/symbol_9.png deleted file mode 100644 index 2d2dff76..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_9.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_90.png b/android-application/src/main/res/drawable-nodpi/symbol_90.png deleted file mode 100644 index 002bdf75..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_90.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_91.png b/android-application/src/main/res/drawable-nodpi/symbol_91.png deleted file mode 100644 index e962f341..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_91.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_92.png b/android-application/src/main/res/drawable-nodpi/symbol_92.png deleted file mode 100644 index 4ff2790b..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_92.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_93.png b/android-application/src/main/res/drawable-nodpi/symbol_93.png deleted file mode 100644 index fc19a49b..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_93.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_94.png b/android-application/src/main/res/drawable-nodpi/symbol_94.png deleted file mode 100644 index 1a94de28..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_94.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_95.png b/android-application/src/main/res/drawable-nodpi/symbol_95.png deleted file mode 100644 index ee59829b..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_95.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_96.png b/android-application/src/main/res/drawable-nodpi/symbol_96.png deleted file mode 100644 index 9bd41f55..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_96.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_97.png b/android-application/src/main/res/drawable-nodpi/symbol_97.png deleted file mode 100644 index 88645e06..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_97.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_98.png b/android-application/src/main/res/drawable-nodpi/symbol_98.png deleted file mode 100644 index d31c253d..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_98.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-nodpi/symbol_99.png b/android-application/src/main/res/drawable-nodpi/symbol_99.png deleted file mode 100644 index 5e467600..00000000 Binary files a/android-application/src/main/res/drawable-nodpi/symbol_99.png and /dev/null differ diff --git a/android-application/src/main/res/drawable-v31/ic_launcher_foreground.xml b/android-application/src/main/res/drawable-v31/ic_launcher_foreground.xml deleted file mode 100644 index aeeb65a8..00000000 --- a/android-application/src/main/res/drawable-v31/ic_launcher_foreground.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - diff --git a/android-application/src/main/res/drawable/capture_service_notification_icon.xml b/android-application/src/main/res/drawable/capture_service_notification_icon.xml deleted file mode 100644 index 9379c0f4..00000000 --- a/android-application/src/main/res/drawable/capture_service_notification_icon.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/android-application/src/main/res/drawable/ic_humid.xml b/android-application/src/main/res/drawable/ic_humid.xml deleted file mode 100644 index c976ab31..00000000 --- a/android-application/src/main/res/drawable/ic_humid.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/android-application/src/main/res/drawable/ic_launcher_foreground.xml b/android-application/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 6fd29bdc..00000000 --- a/android-application/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - diff --git a/android-application/src/main/res/drawable/ic_rain.xml b/android-application/src/main/res/drawable/ic_rain.xml deleted file mode 100644 index 7a40e264..00000000 --- a/android-application/src/main/res/drawable/ic_rain.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/android-application/src/main/res/drawable/ic_snow.xml b/android-application/src/main/res/drawable/ic_snow.xml deleted file mode 100644 index 968ccd47..00000000 --- a/android-application/src/main/res/drawable/ic_snow.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/android-application/src/main/res/drawable/ic_windy.xml b/android-application/src/main/res/drawable/ic_windy.xml deleted file mode 100644 index a27a3729..00000000 --- a/android-application/src/main/res/drawable/ic_windy.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/android-application/src/main/res/drawable/wave.xml b/android-application/src/main/res/drawable/wave.xml deleted file mode 100644 index 63af3062..00000000 --- a/android-application/src/main/res/drawable/wave.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/android-application/src/main/res/font/anonymous_pro.ttf b/android-application/src/main/res/font/anonymous_pro.ttf deleted file mode 100644 index 57aa8938..00000000 Binary files a/android-application/src/main/res/font/anonymous_pro.ttf and /dev/null differ diff --git a/android-application/src/main/res/font/anonymous_pro_bold.ttf b/android-application/src/main/res/font/anonymous_pro_bold.ttf deleted file mode 100644 index 1d4bf2b5..00000000 Binary files a/android-application/src/main/res/font/anonymous_pro_bold.ttf and /dev/null differ diff --git a/android-application/src/main/res/font/lato_regular.ttf b/android-application/src/main/res/font/lato_regular.ttf deleted file mode 100644 index 33eba8b1..00000000 Binary files a/android-application/src/main/res/font/lato_regular.ttf and /dev/null differ diff --git a/android-application/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android-application/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 7353dbd1..00000000 --- a/android-application/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android-application/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android-application/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 7353dbd1..00000000 --- a/android-application/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android-application/src/main/res/mipmap-hdpi/ic_launcher.png b/android-application/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 2bea0691..00000000 Binary files a/android-application/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/android-application/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android-application/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index f5f7227c..00000000 Binary files a/android-application/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/android-application/src/main/res/mipmap-mdpi/ic_launcher.png b/android-application/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index b4f921cc..00000000 Binary files a/android-application/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/android-application/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android-application/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index bee8a07a..00000000 Binary files a/android-application/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/android-application/src/main/res/mipmap-xhdpi/ic_launcher.png b/android-application/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 3ab278ce..00000000 Binary files a/android-application/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/android-application/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android-application/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 92b37fe2..00000000 Binary files a/android-application/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android-application/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android-application/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 2ed89a34..00000000 Binary files a/android-application/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/android-application/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android-application/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index cca3b6b0..00000000 Binary files a/android-application/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android-application/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android-application/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 5cd53133..00000000 Binary files a/android-application/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/android-application/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android-application/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index e5c30456..00000000 Binary files a/android-application/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android-application/src/main/res/values-night-v23/theme.xml b/android-application/src/main/res/values-night-v23/theme.xml deleted file mode 100644 index 67bf33d2..00000000 --- a/android-application/src/main/res/values-night-v23/theme.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - diff --git a/android-application/src/main/res/values-v21/theme.xml b/android-application/src/main/res/values-v21/theme.xml deleted file mode 100644 index 2dddf810..00000000 --- a/android-application/src/main/res/values-v21/theme.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/android-application/src/main/res/values-v23/theme.xml b/android-application/src/main/res/values-v23/theme.xml deleted file mode 100644 index 8b39cac1..00000000 --- a/android-application/src/main/res/values-v23/theme.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - diff --git a/android-application/src/main/res/values-v31/theme.xml b/android-application/src/main/res/values-v31/theme.xml deleted file mode 100644 index 81875579..00000000 --- a/android-application/src/main/res/values-v31/theme.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android-application/src/main/res/values/application.xml b/android-application/src/main/res/values/application.xml deleted file mode 100644 index 9ec91c2d..00000000 --- a/android-application/src/main/res/values/application.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - Ack - Ack (debug) - Ack %s-%s (%s) - Ack %s-%s - Play Services Unavailable - No Play Services - unknown - Navigate Up - Unknown Error - Selected - diff --git a/android-application/src/main/res/values/capture.xml b/android-application/src/main/res/values/capture.xml deleted file mode 100644 index aee80002..00000000 --- a/android-application/src/main/res/values/capture.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - Map - Log - Insights - Messages - Enable Recording - Disable Recording - Controls - Close - Unknown - Not Connected - Connected - Position Disabled - Position Enabled - Settings - Bluetooth Settings - Audio - Internet - Monitor Status - Status notifications when ACK is monitoring APRS - Monitoring Audio for APRS - Monitoring Internet for APRS - APRS Connected - off - No Callsign - Audio - Internet - TNC - diff --git a/android-application/src/main/res/values/colors.xml b/android-application/src/main/res/values/colors.xml deleted file mode 100644 index 11ec8270..00000000 --- a/android-application/src/main/res/values/colors.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - #FFA72B - #212121 - #70212121 - #FFFFFF - #70FFFFFF - diff --git a/android-application/src/main/res/values/connection.xml b/android-application/src/main/res/values/connection.xml deleted file mode 100644 index 9e9be1bc..00000000 --- a/android-application/src/main/res/values/connection.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - Connection - Callsign - APRS-IS Passcode - APRS-IS Server - Invalid Hostname - APRS-IS Port - Invalid Port - APRS-IS Search Radius (mi) - Invalid Distance - Invalid Callsign - Invalid Passcode - Incorrect Passcode - diff --git a/android-application/src/main/res/values/content.xml b/android-application/src/main/res/values/content.xml deleted file mode 100644 index d16250cb..00000000 --- a/android-application/src/main/res/values/content.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - 60dp - diff --git a/android-application/src/main/res/values/ic_launcher_background.xml b/android-application/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index 606b9bd3..00000000 --- a/android-application/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #383838 - \ No newline at end of file diff --git a/android-application/src/main/res/values/info.xml b/android-application/src/main/res/values/info.xml deleted file mode 100644 index 3c3aebd7..00000000 --- a/android-application/src/main/res/values/info.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - App Information - diff --git a/android-application/src/main/res/values/insights.xml b/android-application/src/main/res/values/insights.xml deleted file mode 100644 index 1d4050a6..00000000 --- a/android-application/src/main/res/values/insights.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - Insights - APRS Stats - Nearby Frequencies - No nearby stations with broadcast frequencies. - Reported by %s at %s - Packets Received: %d - Stations: %d - No packets collected recently. - Normal Conditions - Rain - Snow - Humid - Windy - No weather data collected yet. - No packets collected yet. - diff --git a/android-application/src/main/res/values/locale.xml b/android-application/src/main/res/values/locale.xml deleted file mode 100644 index 73812f29..00000000 --- a/android-application/src/main/res/values/locale.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - Locale - Prefer Metric Units - - %smph - %skph - %sft - %sm - diff --git a/android-application/src/main/res/values/log.xml b/android-application/src/main/res/values/log.xml deleted file mode 100644 index 7a74c146..00000000 --- a/android-application/src/main/res/values/log.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - Radio Packet - Internet Packet - TNC Packet - Local Packet - No Logs Received Yet - View Station Details - Received %s - Hide Unknown Packets - Log - diff --git a/android-application/src/main/res/values/map.xml b/android-application/src/main/res/values/map.xml deleted file mode 100644 index 2acedfcd..00000000 --- a/android-application/src/main/res/values/map.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - Map Settings - Number of Pins to display - Disable my location - Center on my location - diff --git a/android-application/src/main/res/values/messages.xml b/android-application/src/main/res/values/messages.xml deleted file mode 100644 index 53119d09..00000000 --- a/android-application/src/main/res/values/messages.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - Text Message - No messages yet. - Send - Disconnected - Connected to: %s - Compose Message - New Message - Callsign - Start Messaging %s - Radio Packet - Internet Packet - TNC Packet - Transmitted Packet - diff --git a/android-application/src/main/res/values/prompt.xml b/android-application/src/main/res/values/prompt.xml deleted file mode 100644 index 16156723..00000000 --- a/android-application/src/main/res/values/prompt.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - Save - Cancel - Enter a valid number - Number must be positive - diff --git a/android-application/src/main/res/values/settings.xml b/android-application/src/main/res/values/settings.xml deleted file mode 100644 index a895b0b7..00000000 --- a/android-application/src/main/res/values/settings.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - Settings - Cancel - Update - Authenticated - Made with ❤️ by Ink Applications - User Agreement & License - OSS Licenses - Onboarding - License Agreement Revision - License Prompt Complete - Show Advanced - Advanced - Dev - Must be one of: %s - diff --git a/android-application/src/main/res/values/station.xml b/android-application/src/main/res/values/station.xml deleted file mode 100644 index 9a59d495..00000000 --- a/android-application/src/main/res/values/station.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - %1$s at %2$s up to %3$s - %1$s at %2$s - %s - %sº - Recent Packets - Station Info - Show Station Debugging Data - Recent Packet Limit - diff --git a/android-application/src/main/res/values/theme.xml b/android-application/src/main/res/values/theme.xml deleted file mode 100644 index 9a99185d..00000000 --- a/android-application/src/main/res/values/theme.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - diff --git a/android-application/src/main/res/values/transmit.xml b/android-application/src/main/res/values/transmit.xml deleted file mode 100644 index 6467c2eb..00000000 --- a/android-application/src/main/res/values/transmit.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - Transmit Settings - Repeat Rate (minutes) - Update Rate (minutes) - Distance Threshold (miles) - AFSK Preamble (milliseconds) - Digipeater Path (Comma Separated) - Symbol - Destination - Comment - Volume (%) - Must be exactly 2 characters - Max-Length is 43 Characters - Must be between 0 and 100 - Driver - diff --git a/android-application/src/main/res/values/usage.xml b/android-application/src/main/res/values/usage.xml deleted file mode 100644 index a38b2de6..00000000 --- a/android-application/src/main/res/values/usage.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - - 2 - - "Usage and Privacy Agreement" - Decline - I Understand and Agree - - Legally Restricted Services - - This software provides access and tools for the amateur radio APRS - network. Radio communications are a regulated service. - - - Some of the functionality provided by this application can be used - in ways that may require a license or be restricted in your area. - It is your responsibility to research and follow local laws when using - this software. - - - Data Privacy - - This application can be used to transmit your location and other - information to the APRS radio network. When transmitted, the - information you provide, including but not limited to your location, - will be transmitted publicly, either by radio or by an internet service, - and should not be considered private. - - - This application collects usage data using Google Firebase. - Information collected may include information unique to your device, - information about actions performed within the application, - and logging data related to using the application. - This information is used internally to improve the application - and is not shared with any 3rd parties. - - - No Warranty - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH - THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - Software Rights - - Copyright (c) 2020-2022 Ink Applications - - - 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 2 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. - - - The full license can be obtained at: - http://www.gnu.org/licenses/old-licenses/gpl-2.0.html - The full license, as well as sources can be obtained at: - https://ack.inkapplications.com - or by emailing: legal@inkapplications.com - - diff --git a/android-application/src/stub/kotlin/com/inkapplications/ack/android/maps/MapsImplementation.kt b/android-application/src/stub/kotlin/com/inkapplications/ack/android/maps/MapsImplementation.kt deleted file mode 100644 index eea6979e..00000000 --- a/android-application/src/stub/kotlin/com/inkapplications/ack/android/maps/MapsImplementation.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.inkapplications.ack.android.maps - -val MapsImplementation = DummyMapRenderer diff --git a/android-application/src/test/java/com/inkapplications/ack/android/TestData.kt b/android-application/src/test/java/com/inkapplications/ack/android/TestData.kt deleted file mode 100644 index 42feea26..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/TestData.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.inkapplications.ack.android - -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.data.PacketOrigin -import com.inkapplications.ack.structures.AprsPacket -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.PacketRoute -import com.inkapplications.ack.structures.station.StationAddress -import kotlinx.datetime.Instant - -val testRoute = PacketRoute( - source = StationAddress("KE0YOG"), - destination = StationAddress("KE0YOG", "1"), - digipeaters = listOf(), -) - -fun AprsPacket.toTestCapturedPacket() = CapturedPacket( - id = CaptureId(1), - received = Instant.fromEpochMilliseconds(0), - parsed = this, - origin = PacketOrigin.AprsIs, - raw = byteArrayOf(), -) - -fun PacketData.toTestPacket() = AprsPacket( - route = testRoute, - data = this, -) diff --git a/android-application/src/test/java/com/inkapplications/ack/android/TestDoubles.kt b/android-application/src/test/java/com/inkapplications/ack/android/TestDoubles.kt deleted file mode 100644 index 6542e778..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/TestDoubles.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.inkapplications.ack.android - -import com.inkapplications.ack.android.maps.MarkerViewState -import com.inkapplications.ack.android.settings.BooleanSetting -import com.inkapplications.ack.android.settings.IntSetting -import com.inkapplications.ack.android.settings.SettingsReadAccess -import com.inkapplications.ack.android.settings.StringSetting -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.data.PacketOrigin -import com.inkapplications.ack.data.PacketStorage -import com.inkapplications.ack.data.drivers.DriverConnectionState -import com.inkapplications.ack.data.drivers.PacketDriver -import com.inkapplications.ack.structures.AprsPacket -import com.inkapplications.ack.structures.EncodingConfig -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.PacketRoute -import com.inkapplications.ack.structures.capabilities.Mapable -import com.inkapplications.ack.structures.station.Callsign -import com.inkapplications.ack.structures.station.StationAddress -import com.inkapplications.android.extensions.StringResources -import com.inkapplications.android.extensions.ViewStateFactory -import com.inkapplications.android.extensions.format.DateTimeFormatter -import inkapplications.spondee.spatial.GeoCoordinates -import inkapplications.spondee.spatial.latitude -import inkapplications.spondee.spatial.longitude -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.datetime.Instant -import kotlin.reflect.KClass -import kotlin.test.assertNotNull -import kotlin.test.assertNull - -/** - * Spits arguments back out as values for testing. - */ -object ParrotStringResources: StringResources { - override fun getString(key: Int): String = "" - override fun getString(key: Int, vararg arguments: Any): String = arguments.joinToString("|") -} - -object StubSettings: SettingsReadAccess { - override fun observeStringState(setting: StringSetting): Flow = flow {} - override fun observeIntState(setting: IntSetting): Flow = flow {} - override fun observeBooleanState(setting: BooleanSetting): Flow = flow {} -} - -object AprsAccessStub: PacketDriver { - override val connectionState: Flow = flow {} - override val incoming: Flow = flow {} - override suspend fun transmitPacket(packet: AprsPacket, encodingConfig: EncodingConfig) {} - override suspend fun connect() {} - override suspend fun disconnect() {} -} - -object PacketStorageStub: PacketStorage { - override fun findRecent(count: Long): Flow> = flow {} - override fun findLatestByConversation(callsign: Callsign): Flow> = flow {} - override fun findConversation(addressee: Callsign, callsign: Callsign): Flow> = flow {} - override fun findById(id: CaptureId): Flow = flow {} - override suspend fun save(data: ByteArray, packet: AprsPacket, origin: PacketOrigin): CapturedPacket = TODO() - override fun count(): Flow = flow {} - override fun countStations(): Flow = flow {} - override fun findByStationComments(limit: Long?): Flow> = TODO() - override fun findMostRecentByType(type: KClass): Flow = flow {} - override fun findBySource(callsign: Callsign, limit: Long?): Flow> = flow {} -} - -object EpochFormatterFake: DateTimeFormatter { - override fun formatTimestamp(instant: Instant): String = instant.toEpochMilliseconds().toString() -} - -object NullMarkerFactoryMock: ViewStateFactory { - override fun create(data: CapturedPacket): MarkerViewState? = null.also { - assertNull((data.parsed.data as? Mapable)?.coordinates) - } -} - -val DummyMarker = MarkerViewState(CaptureId(0), GeoCoordinates(0.latitude, 0.longitude), null) -val DummyPacket = CapturedPacket( - id = CaptureId(0L), - received = Instant.DISTANT_PAST, - parsed = AprsPacket( - route = PacketRoute( - source = StationAddress( - callsign = Callsign(""), - ), - destination = StationAddress( - callsign = Callsign(""), - ), - digipeaters = emptyList(), - ), - data = PacketData.Unknown(""), - ), - origin = PacketOrigin.Local, - raw = byteArrayOf(), -) - -object DummyMarkerFactoryMock: ViewStateFactory { - override fun create(data: CapturedPacket): MarkerViewState = DummyMarker.also { - assertNotNull((data.parsed.data as Mapable).coordinates) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/capture/CaptureScreenFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/capture/CaptureScreenFactoryTest.kt deleted file mode 100644 index 26f2c122..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/capture/CaptureScreenFactoryTest.kt +++ /dev/null @@ -1,286 +0,0 @@ -package com.inkapplications.ack.android.capture - -import com.inkapplications.ack.android.ParrotStringResources -import com.inkapplications.ack.android.connection.DriverSelection -import com.inkapplications.ack.android.settings.LicenseData -import com.inkapplications.ack.android.settings.Passcode -import com.inkapplications.ack.data.drivers.DriverConnectionState -import com.inkapplications.ack.structures.station.toStationAddress -import com.inkapplications.android.extensions.bluetooth.BluetoothDeviceData -import com.inkapplications.android.extensions.control.ControlState -import inkapplications.spondee.scalar.percent -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class CaptureScreenFactoryTest { - private val factory = CaptureScreenStateFactory( - stringResources = ParrotStringResources, - ) - - private val FakeDevice = BluetoothDeviceData("", null, "", null, null, 0) - - @Test - fun internetDefault() { - val result = factory.controlPanelState( - inputAudioLevel = null, - currentDriver = DriverSelection.Internet, - driverConnectionState = DriverConnectionState.Disconnected, - positionTransmit = false, - license = LicenseData(), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals("", result.userCallsign) - assertEquals(null, result.volumeLevel) - assertEquals("", result.connection) - assertEquals(DriverSelection.Internet, result.connectionType) - assertEquals(ControlState.Disabled, result.connectState) - assertEquals(ControlState.Disabled, result.positionTransmitState) - } - - @Test - fun internetWithCallsign() { - val result = factory.controlPanelState( - inputAudioLevel = null, - currentDriver = DriverSelection.Internet, - driverConnectionState = DriverConnectionState.Disconnected, - positionTransmit = false, - license = LicenseData( - address = "KE0YOG-7".toStationAddress() - ), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals("KE0YOG-7", result.userCallsign) - assertEquals(ControlState.Off, result.connectState) - assertEquals(ControlState.Disabled, result.positionTransmitState) - } - - @Test - fun internetWithPasscode() { - val result = factory.controlPanelState( - inputAudioLevel = null, - currentDriver = DriverSelection.Internet, - driverConnectionState = DriverConnectionState.Disconnected, - positionTransmit = false, - license = LicenseData( - address = "KE0YOG-7".toStationAddress(), - passcode = Passcode(12345), - ), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals("KE0YOG-7", result.userCallsign) - assertEquals(ControlState.Off, result.connectState) - assertEquals(ControlState.Disabled, result.positionTransmitState) - } - - @Test - fun internetConnectedWithPasscode() { - val result = factory.controlPanelState( - inputAudioLevel = null, - currentDriver = DriverSelection.Internet, - driverConnectionState = DriverConnectionState.Connected, - positionTransmit = false, - license = LicenseData( - address = "KE0YOG-7".toStationAddress(), - passcode = Passcode(12345), - ), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals("KE0YOG-7", result.userCallsign) - assertEquals(ControlState.On, result.connectState) - assertEquals(ControlState.Off, result.positionTransmitState) - } - - @Test - fun internetConnectedWithTransmit() { - val result = factory.controlPanelState( - inputAudioLevel = null, - currentDriver = DriverSelection.Internet, - driverConnectionState = DriverConnectionState.Connected, - positionTransmit = true, - license = LicenseData( - address = "KE0YOG-7".toStationAddress(), - passcode = Passcode(12345), - ), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals("KE0YOG-7", result.userCallsign) - assertEquals(ControlState.On, result.connectState) - assertEquals(ControlState.On, result.positionTransmitState) - } - - @Test - fun audioDefault() { - val result = factory.controlPanelState( - inputAudioLevel = null, - currentDriver = DriverSelection.Audio, - driverConnectionState = DriverConnectionState.Disconnected, - positionTransmit = false, - license = LicenseData(), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals(ControlState.Off, result.connectState) - assertEquals(ControlState.Disabled, result.positionTransmitState) - assertEquals(null, result.volumeLevel) - } - - @Test - fun audioCapturing() { - val result = factory.controlPanelState( - inputAudioLevel = 12.percent, - currentDriver = DriverSelection.Audio, - driverConnectionState = DriverConnectionState.Connected, - positionTransmit = false, - license = LicenseData(), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals(ControlState.On, result.connectState) - assertEquals(ControlState.Disabled, result.positionTransmitState) - assertEquals(.12f, result.volumeLevel) - } - - @Test - fun audioRegistered() { - val result = factory.controlPanelState( - inputAudioLevel = 12.percent, - currentDriver = DriverSelection.Audio, - driverConnectionState = DriverConnectionState.Disconnected, - positionTransmit = false, - license = LicenseData( - address = "KE0YOG-7".toStationAddress(), - ), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals(ControlState.Off, result.connectState) - assertEquals(ControlState.Disabled, result.positionTransmitState) - assertEquals(.12f, result.volumeLevel) - } - - @Test - fun audioRegisteredConnected() { - val result = factory.controlPanelState( - inputAudioLevel = 12.percent, - currentDriver = DriverSelection.Audio, - driverConnectionState = DriverConnectionState.Connected, - positionTransmit = false, - license = LicenseData( - address = "KE0YOG-7".toStationAddress(), - ), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals(ControlState.On, result.connectState) - assertEquals(ControlState.Off, result.positionTransmitState) - assertEquals(.12f, result.volumeLevel) - } - - @Test - fun audioTransmitting() { - val result = factory.controlPanelState( - inputAudioLevel = 12.percent, - currentDriver = DriverSelection.Audio, - driverConnectionState = DriverConnectionState.Connected, - positionTransmit = true, - license = LicenseData( - address = "KE0YOG-7".toStationAddress(), - ), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals(ControlState.On, result.connectState) - assertEquals(ControlState.On, result.positionTransmitState) - assertEquals(.12f, result.volumeLevel) - } - - @Test - fun tncDefault() { - val result = factory.controlPanelState( - inputAudioLevel = null, - currentDriver = DriverSelection.Tnc, - driverConnectionState = DriverConnectionState.Disconnected, - positionTransmit = false, - license = LicenseData(), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals(ControlState.Off, result.connectState) - assertEquals(ControlState.Disabled, result.positionTransmitState) - assertEquals(null, result.volumeLevel) - } - - @Test - fun tncConnectedOff() { - val result = factory.controlPanelState( - inputAudioLevel = null, - currentDriver = DriverSelection.Tnc, - driverConnectionState = DriverConnectionState.Disconnected, - positionTransmit = false, - license = LicenseData(), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals(ControlState.Off, result.connectState) - assertEquals(ControlState.Disabled, result.positionTransmitState) - assertEquals(null, result.volumeLevel) - } - - @Test - fun tncConnectedEnabled() { - val result = factory.controlPanelState( - inputAudioLevel = null, - currentDriver = DriverSelection.Tnc, - driverConnectionState = DriverConnectionState.Connected, - positionTransmit = false, - license = LicenseData(), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals(ControlState.On, result.connectState) - assertEquals(ControlState.Disabled, result.positionTransmitState) - assertEquals(null, result.volumeLevel) - } - - @Test - fun tncConnectedEnabledRegistered() { - val result = factory.controlPanelState( - inputAudioLevel = null, - currentDriver = DriverSelection.Tnc, - driverConnectionState = DriverConnectionState.Connected, - positionTransmit = false, - license = LicenseData( - address = "KE0YOG-7".toStationAddress(), - ), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals(ControlState.On, result.connectState) - assertEquals(ControlState.Off, result.positionTransmitState) - assertEquals(null, result.volumeLevel) - } - - @Test - fun tncTransmitting() { - val result = factory.controlPanelState( - inputAudioLevel = null, - currentDriver = DriverSelection.Tnc, - driverConnectionState = DriverConnectionState.Connected, - positionTransmit = true, - license = LicenseData( - address = "KE0YOG-7".toStationAddress(), - ), - ) - - assertTrue(result is ControlPanelState.Loaded) - assertEquals(ControlState.On, result.connectState) - assertEquals(ControlState.On, result.positionTransmitState) - assertEquals(null, result.volumeLevel) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/capture/insights/StatsStateFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/capture/insights/StatsStateFactoryTest.kt deleted file mode 100644 index b0820271..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/capture/insights/StatsStateFactoryTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.inkapplications.ack.android.capture.insights - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class StatsStateFactoryTest { - private val factory = StatsStateFactory() - - @Test - fun completeStats() { - val result = factory.createState( - packetCount = 123, - stationCount = 456, - ) - - assertTrue(result is InsightsStatsState.LoadedData) - assertEquals(123, result.packets) - assertEquals(456, result.stations) - } - - @Test - fun noStats() { - val result = factory.createState( - packetCount = 0, - stationCount = 0, - ) - - assertTrue(result is InsightsStatsState.None) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/capture/insights/WeatherStateFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/capture/insights/WeatherStateFactoryTest.kt deleted file mode 100644 index 79ab84c9..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/capture/insights/WeatherStateFactoryTest.kt +++ /dev/null @@ -1,184 +0,0 @@ -package com.inkapplications.ack.android.capture.insights - -import com.inkapplications.ack.android.DummyPacket -import com.inkapplications.ack.android.EpochFormatterFake -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.Precipitation -import com.inkapplications.ack.structures.WindData -import com.inkapplications.ack.structures.station.Callsign -import com.inkapplications.ack.structures.station.Ssid -import com.inkapplications.ack.structures.station.StationAddress -import inkapplications.spondee.measure.metric.celsius -import inkapplications.spondee.measure.us.fahrenheit -import inkapplications.spondee.measure.us.inches -import inkapplications.spondee.measure.us.milesPerHour -import inkapplications.spondee.scalar.percent -import kotlinx.datetime.Instant -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class WeatherStateFactoryTest { - private val factory = WeatherStateFactory(EpochFormatterFake) - - @Test - fun weatherTemperature() { - val fakePacket = DummyPacket.copy( - parsed = DummyPacket.parsed.copy( - route = DummyPacket.parsed.route.copy( - source = StationAddress(Callsign("KE0YOG"), Ssid("9")) - ), - data = PacketData.Weather( - temperature = 80.fahrenheit, - ) - ), - received = Instant.fromEpochMilliseconds(123), - ) - val result = factory.createState( - weatherPacket = fakePacket, - metric = false, - ) - assertTrue(result is InsightsWeatherState.DisplayRecent) - assertEquals("80ºF", result.temperature) - assertEquals(InsightsWeatherState.WeatherIcon.Normal, result.icon) - assertEquals("KE0YOG-9", result.weatherReporter) - assertEquals("123", result.weatherReportTime) - } - - @Test - fun noWeather() { - val result = factory.createState( - weatherPacket = null, - metric = false, - ) - - assertTrue(result is InsightsWeatherState.Unknown) - } - - @Test - fun weatherNoTemp() { - val fakePacket = DummyPacket.copy( - parsed = DummyPacket.parsed.copy( - data = PacketData.Weather() - ), - ) - val result = factory.createState( - weatherPacket = fakePacket, - metric = false, - ) - - assertTrue(result is InsightsWeatherState.DisplayRecent) - assertEquals("--", result.temperature) - } - - @Test - fun weatherSnowy() { - val fakePacket = DummyPacket.copy( - parsed = DummyPacket.parsed.copy( - data = PacketData.Weather( - precipitation = Precipitation( - snowLast24Hours = 12.inches, - ) - ) - ), - ) - val result = factory.createState( - weatherPacket = fakePacket, - metric = false, - ) - assertTrue(result is InsightsWeatherState.DisplayRecent) - assertEquals(InsightsWeatherState.WeatherIcon.Snow, result.icon) - } - - @Test - fun weatherRainy() { - val fakePacket = DummyPacket.copy( - parsed = DummyPacket.parsed.copy( - data = PacketData.Weather( - precipitation = Precipitation( - rainLastHour = 12.inches, - ) - ) - ), - ) - val result = factory.createState( - weatherPacket = fakePacket, - metric = false, - ) - assertTrue(result is InsightsWeatherState.DisplayRecent) - assertEquals(InsightsWeatherState.WeatherIcon.Rain, result.icon) - } - - @Test - fun weatherWindy() { - val fakePacket = DummyPacket.copy( - parsed = DummyPacket.parsed.copy( - data = PacketData.Weather( - windData = WindData( - speed = 25.milesPerHour, - ) - ) - ), - ) - val result = factory.createState( - weatherPacket = fakePacket, - metric = false, - ) - assertTrue(result is InsightsWeatherState.DisplayRecent) - assertEquals(InsightsWeatherState.WeatherIcon.Windy, result.icon) - } - - @Test - fun weatherGusty() { - val fakePacket = DummyPacket.copy( - parsed = DummyPacket.parsed.copy( - data = PacketData.Weather( - windData = WindData( - gust = 25.milesPerHour, - ) - ) - ), - ) - val result = factory.createState( - weatherPacket = fakePacket, - metric = false, - ) - assertTrue(result is InsightsWeatherState.DisplayRecent) - assertEquals(InsightsWeatherState.WeatherIcon.Windy, result.icon) - } - - @Test - fun weatherHumid() { - val fakePacket = DummyPacket.copy( - parsed = DummyPacket.parsed.copy( - data = PacketData.Weather( - humidity = 80.percent, - temperature = 80.fahrenheit, - ) - ), - ) - val result = factory.createState( - weatherPacket = fakePacket, - metric = false, - ) - assertTrue(result is InsightsWeatherState.DisplayRecent) - assertEquals(InsightsWeatherState.WeatherIcon.Humid, result.icon) - } - - @Test - fun metricFormat() { - val fakePacket = DummyPacket.copy( - parsed = DummyPacket.parsed.copy( - data = PacketData.Weather( - temperature = 25.celsius, - ) - ), - ) - val result = factory.createState( - weatherPacket = fakePacket, - metric = true, - ) - assertTrue(result is InsightsWeatherState.DisplayRecent) - assertEquals("25.0ºC", result.temperature) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationViewStateFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationViewStateFactoryTest.kt deleted file mode 100644 index 9608ff7d..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/capture/messages/conversation/ConversationViewStateFactoryTest.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.conversation - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Cloud -import androidx.compose.material.icons.filled.SettingsInputAntenna -import androidx.compose.material.icons.filled.Storage -import androidx.compose.ui.Alignment -import com.inkapplications.ack.android.* -import com.inkapplications.ack.android.capture.messages.MessageData -import com.inkapplications.ack.android.connection.DriverSelection -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.data.PacketOrigin -import com.inkapplications.ack.data.drivers.DriverConnectionState -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.station.toStationAddress -import kotlinx.datetime.Instant -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class ConversationViewStateFactoryTest { - private val factory = ConversationViewStateFactory(EpochFormatterFake, ParrotStringResources) - - private val me = "KE0YOG".toStationAddress() - private val addressee = "KE0YOF".toStationAddress() - - @Test - fun initial() { - val result = factory.createInitial(addressee.callsign) - - assertEquals("KE0YOF", result.title) - } - - @Test - fun emptyMessageList() { - val result = factory.createMessageList(addressee.callsign, emptyList(), DriverConnectionState.Disconnected, DriverSelection.Audio) - - assertTrue(result is ConversationViewState.Empty) - assertEquals("KE0YOF", result.title) - assertEquals(false, result.sendEnabled) - } - - @Test - fun connected() { - val result = factory.createMessageList(addressee.callsign, emptyList(), DriverConnectionState.Connected, DriverSelection.Audio) - assertEquals(true, result.sendEnabled) - } - - @Test - fun testOutgoingMessage() { - val packet = PacketData.Message( - addressee = addressee, - message = "Hello World!", - ).toTestPacket().copy( - route = testRoute.copy( - source = me, - ) - ).toTestCapturedPacket().copy( - received = Instant.fromEpochMilliseconds(12345), - ) - - val messageResult = getMessageState(packet) - assertEquals("Hello World!", messageResult.message) - assertEquals("12345", messageResult.timestamp) - assertEquals(Alignment.CenterEnd, messageResult.alignment) - } - - @Test - fun testIncomingMessageItem() { - val packet = PacketData.Message( - addressee = me, - message = "Hello World!", - ).toTestPacket().copy( - route = testRoute.copy( - source = addressee, - ) - ).toTestCapturedPacket().copy( - received = Instant.fromEpochMilliseconds(12345), - ) - - val messageResult = getMessageState(packet) - - assertEquals("Hello World!", messageResult.message) - assertEquals("12345", messageResult.timestamp) - assertEquals(Alignment.CenterStart, messageResult.alignment) - } - - private fun getMessageState(packet: CapturedPacket): MessageItemState { - val data = MessageData(me.callsign, packet) - val result = factory.createMessageList(addressee.callsign, listOf(data), DriverConnectionState.Disconnected, DriverSelection.Audio) - - assertTrue(result is ConversationViewState.MessageList) - assertEquals(1, result.messages.size) - - return result.messages.single() - } - - @Test - fun testIcons() { - val basePacket = PacketData.Message( - addressee = addressee, - message = "test", - ).toTestPacket().toTestCapturedPacket() - val local = getMessageState(basePacket.copy( - origin = PacketOrigin.Local, - )) - val radio = getMessageState(basePacket.copy( - origin = PacketOrigin.Ax25, - )) - val internet = getMessageState(basePacket.copy( - origin = PacketOrigin.AprsIs, - )) - - assertEquals(Icons.Default.Storage, local.icon) - assertEquals(Icons.Default.SettingsInputAntenna, radio.icon) - assertEquals(Icons.Default.Cloud, internet.icon) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexStateFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexStateFactoryTest.kt deleted file mode 100644 index baa62596..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/capture/messages/index/MessageIndexStateFactoryTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.inkapplications.ack.android.capture.messages.index - -import com.inkapplications.ack.android.capture.messages.MessageData -import com.inkapplications.ack.android.toTestCapturedPacket -import com.inkapplications.ack.android.toTestPacket -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.station.Callsign -import com.inkapplications.ack.structures.station.toStationAddress -import kotlinx.datetime.Instant -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class MessageIndexStateFactoryTest { - private val factory = MessageIndexStateFactory() - - @Test - fun empty() { - val result = factory.createScreenState( - latestMessages = emptyList() - ) - - assertTrue(result is MessageIndexState.Empty) - } - - @Test - fun testConversationItem() { - val station = "KE0YOF-2".toStationAddress() - val message = PacketData.Message( - addressee = station, - message = "First!", - ) - .toTestPacket() - .toTestCapturedPacket() - .copy(received = Instant.fromEpochMilliseconds(0)) - val conversation = MessageData( - selfCallsign = Callsign("KE0YOG"), - message = message, - ) - - val results = factory.createScreenState(listOf(conversation)) - assertTrue(results is MessageIndexState.ConversationList) - assertEquals(1, results.conversations.size) - - val result = results.conversations.single() - assertEquals("First!", result.messagePreview) - assertEquals("KE0YOF", result.name) - assertEquals(station.callsign, result.correspondent) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/input/IntegerValidatorTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/input/IntegerValidatorTest.kt deleted file mode 100644 index 479dac06..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/input/IntegerValidatorTest.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.inkapplications.ack.android.input - -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class IntegerValidatorTest { - @Test - fun positiveValid() { - val validator = IntegerValidator( - error = "error", - ) - - val result = validator.validate(1) - - assertTrue(result is ValidationResult.Valid) - } - - @Test - fun negativeInvalid() { - val validator = IntegerValidator( - error = "error", - ) - - val result = validator.validate(-5) - - assertTrue(result is ValidationResult.Error) - assertEquals("error", result.message) - } - - @Test - fun zeroValid() { - val validator = IntegerValidator( - error = "error", - ) - - val result = validator.validate(0) - - assertTrue(result is ValidationResult.Valid) - } - - @Test - fun zeroInvalid() { - val validator = IntegerValidator( - error = "error", - zeroInclusive = false, - ) - - val result = validator.validate(0) - - assertTrue(result is ValidationResult.Error) - assertEquals("error", result.message) - } - - @Test - fun sentinelValid() { - val validator = IntegerValidator( - error = "error", - allowSentinel = -1, - ) - - val result = validator.validate(-1) - - assertTrue(result is ValidationResult.Valid) - } - - @Test - fun invalidSentinel() { - val validator = IntegerValidator( - error = "error", - allowSentinel = -1, - ) - - val result = validator.validate(-4) - - assertTrue(result is ValidationResult.Error) - assertEquals("error", result.message) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/log/CombinedLogItemViewStateFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/log/CombinedLogItemViewStateFactoryTest.kt deleted file mode 100644 index 8147f03f..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/log/CombinedLogItemViewStateFactoryTest.kt +++ /dev/null @@ -1,242 +0,0 @@ -package com.inkapplications.ack.android.log - -import com.inkapplications.ack.android.symbol.SymbolFactoryDummy -import com.inkapplications.ack.android.toTestPacket -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.ReportState -import com.inkapplications.ack.structures.TelemetryValues -import com.inkapplications.ack.structures.station.StationAddress -import com.inkapplications.ack.structures.symbolOf -import inkapplications.spondee.measure.us.fahrenheit -import inkapplications.spondee.spatial.GeoCoordinates -import inkapplications.spondee.spatial.latitude -import inkapplications.spondee.spatial.longitude -import kotlin.test.Test -import kotlin.test.assertEquals - -class CombinedLogItemViewStateFactoryTest { - val factory = CombinedLogItemViewStateFactory(SymbolFactoryDummy) - - @Test - fun unknownPacket() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.Unknown( - body = "", - ).toTestPacket(), - metric = false, - ) - - assertEquals(123L, result.id.value) - assertEquals("KE0YOG", result.origin) - assertEquals("Unknown data", result.comment) - } - - @Test - fun position() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.Position( - coordinates = GeoCoordinates(12f.latitude, 34f.longitude), - symbol = symbolOf('-', '/'), - comment = "Test Position", - ).toTestPacket(), - metric = false, - ) - - assertEquals("Position: Test Position", result.comment) - } - - @Test - fun positionGeneric() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.Position( - comment = "", - coordinates = GeoCoordinates(12f.latitude, 34f.longitude), - symbol = symbolOf('-', '/'), - ).toTestPacket(), - metric = false, - ) - - assertEquals("Position", result.comment) - } - - @Test - fun weather() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.Weather( - temperature = 72.fahrenheit, - ).toTestPacket(), - metric = false, - ) - - assertEquals("Weather: 72ºF", result.comment) - } - - @Test - fun weatherGeneric() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.Weather().toTestPacket(), - metric = false, - ) - - assertEquals("Weather", result.comment) - } - - @Test - fun objectReport() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.ObjectReport( - coordinates = GeoCoordinates(12f.latitude, 34f.longitude), - symbol = symbolOf('-', '/'), - comment = "Hello World", - name = "Test Object", - state = ReportState.Live, - ).toTestPacket(), - metric = false, - ) - - assertEquals("Object: Test Object - Hello World", result.comment) - } - - @Test - fun objectReportNoComment() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.ObjectReport( - coordinates = GeoCoordinates(12f.latitude, 34f.longitude), - symbol = symbolOf('-', '/'), - name = "Test Object", - comment = " ", - state = ReportState.Live, - ).toTestPacket(), - metric = false, - ) - - assertEquals("Object: Test Object", result.comment) - } - - @Test - fun itemReport() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.ItemReport( - coordinates = GeoCoordinates(12f.latitude, 34f.longitude), - symbol = symbolOf('-', '/'), - name = "Test Item", - comment = "Hello World", - state = ReportState.Live, - ).toTestPacket(), - metric = false, - ) - - assertEquals("Item: Test Item - Hello World", result.comment) - } - - @Test - fun itemReportNoComment() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.ItemReport( - coordinates = GeoCoordinates(12f.latitude, 34f.longitude), - symbol = symbolOf('-', '/'), - name = "Test Item", - comment = " ", - state = ReportState.Live, - ).toTestPacket(), - metric = false, - ) - - assertEquals("Item: Test Item", result.comment) - } - - @Test - fun message() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.Message( - addressee = StationAddress("KE0YOG-2"), - message = "Hello KE0YOG", - ).toTestPacket(), - metric = false, - ) - - assertEquals("[KE0YOG-2] Hello KE0YOG", result.comment) - } - - @Test - fun messageWithCount() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.Message( - addressee = StationAddress("KE0YOG-2"), - message = "Hello KE0YOG", - messageNumber = 69, - ).toTestPacket(), - metric = false, - ) - - assertEquals("[KE0YOG-2] Hello KE0YOG (69)", result.comment) - } - - @Test - fun telemetryReport() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.TelemetryReport( - sequenceId = "", - data = TelemetryValues(1f, 2f, 3f, 4f, 5f, 0u), - comment = "Hello World", - ).toTestPacket(), - metric = false, - ) - - assertEquals("Telemetry: Hello World", result.comment) - } - - @Test - fun telemetryReportGeneric() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.TelemetryReport( - sequenceId = "", - data = TelemetryValues(1f, 2f, 3f, 4f, 5f, 0u), - comment = " ", - ).toTestPacket(), - metric = false, - ) - - assertEquals("Telemetry", result.comment) - } - - @Test - fun statusReport() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.StatusReport( - status = "Testing", - ).toTestPacket(), - metric = false, - ) - - assertEquals("Status: Testing", result.comment) - } - - @Test - fun capabilityReport() { - val result = factory.create( - id = CaptureId(123L), - packet = PacketData.CapabilityReport( - capabilityData = setOf(), - ).toTestPacket(), - metric = false, - ) - - assertEquals("Capability Report", result.comment) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/log/SummaryFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/log/SummaryFactoryTest.kt deleted file mode 100644 index 10706e2d..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/log/SummaryFactoryTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.inkapplications.ack.android.log - -import com.inkapplications.ack.android.ParrotStringResources -import com.inkapplications.ack.structures.WindData -import inkapplications.spondee.measure.us.milesPerHour -import inkapplications.spondee.spatial.Cardinal -import inkapplications.spondee.spatial.toAngle -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class SummaryFactoryTest { - private val factory = SummaryFactory(ParrotStringResources) - - @Test - fun noData() { - val result = factory.createWindSummary(WindData(), false) - - assertNull(result) - } - - @Test - fun speedOnly() { - val result = factory.createWindSummary( - WindData(speed = 12.milesPerHour), - false, - ) - - assertEquals("12mph", result) - } - - @Test - fun directionOnly() { - val result = factory.createWindSummary( - WindData(direction = Cardinal.South.toAngle()), - false, - ) - - assertEquals("180º", result) - } - - @Test - fun speedAndDirection() { - val result = factory.createWindSummary( - WindData( - speed = 12.milesPerHour, - direction = Cardinal.East.toAngle(), - ), - false, - ) - - assertEquals("90º|12mph", result) - } - - @Test - fun full() { - val result = factory.createWindSummary( - WindData( - speed = 12.milesPerHour, - direction = Cardinal.East.toAngle(), - gust = 34.milesPerHour, - ), - false, - ) - - assertEquals("90º|12mph|34mph", result) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactoryTest.kt deleted file mode 100644 index 203f927c..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/log/details/LogDetailsViewStateFactoryTest.kt +++ /dev/null @@ -1,196 +0,0 @@ -package com.inkapplications.ack.android.log.details - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Cloud -import androidx.compose.material.icons.filled.SettingsInputAntenna -import androidx.compose.material.icons.filled.Storage -import com.inkapplications.ack.android.* -import com.inkapplications.ack.android.log.SummaryFactory -import com.inkapplications.ack.data.PacketOrigin -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.WindData -import com.inkapplications.ack.structures.station.StationAddress -import com.inkapplications.ack.structures.symbolOf -import inkapplications.spondee.measure.us.fahrenheit -import inkapplications.spondee.measure.us.milesPerHour -import inkapplications.spondee.spatial.GeoCoordinates -import inkapplications.spondee.spatial.degrees -import inkapplications.spondee.spatial.latitude -import inkapplications.spondee.spatial.longitude -import kotlinx.datetime.Instant -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class LogDetailsViewStateFactoryTest { - val dummySummaryFactory = SummaryFactory(ParrotStringResources) - val factoryWithNullMarkers = LogDetailsViewStateFactory(dummySummaryFactory, NullMarkerFactoryMock, EpochFormatterFake, ParrotStringResources) - val factoryWithDummyMarkers = LogDetailsViewStateFactory(dummySummaryFactory, DummyMarkerFactoryMock, EpochFormatterFake, ParrotStringResources) - - @Test - fun nameFormatting() { - val packet = PacketData.Unknown(body = "Test").toTestPacket().copy( - route = testRoute.copy(source = StationAddress("TEST", "4")) - ).toTestCapturedPacket() - - val result = factoryWithNullMarkers.create(LogDetailData(packet = packet)) - - assertEquals("TEST-4", result.name) - } - - @Test - fun icons() { - val base = PacketData.Unknown( - body = "Test" - ).toTestPacket().toTestCapturedPacket() - - val localResult = LogDetailData(packet = base.copy(origin = PacketOrigin.Local)).let(factoryWithNullMarkers::create) - val isResult = LogDetailData(packet = base.copy(origin = PacketOrigin.AprsIs)).let(factoryWithNullMarkers::create) - val ax25Result = LogDetailData(packet = base.copy(origin = PacketOrigin.Ax25)).let(factoryWithNullMarkers::create) - - assertEquals(Icons.Default.Storage, localResult.receiveIcon) - assertEquals(Icons.Default.Cloud, isResult.receiveIcon) - assertEquals(Icons.Default.SettingsInputAntenna, ax25Result.receiveIcon) - } - - @Test - fun timestamp() { - val packet = PacketData.Unknown(body = "Test").toTestPacket().toTestCapturedPacket().copy( - received = Instant.fromEpochMilliseconds(-22073104000) - ) - - val result = factoryWithNullMarkers.create(LogDetailData(packet = packet)) - - assertEquals("-22073104000", result.timestamp) - } - - @Test - fun debugData() { - val packet = PacketData.Unknown( - body = "Test" - ).toTestPacket().toTestCapturedPacket().copy( - raw = "Test Debug".toByteArray() - ) - val data = LogDetailData( - packet = packet, - debug = true - ) - - val result = factoryWithNullMarkers.create(data) - - assertEquals("Test Debug", result.rawSource) - } - - @Test - fun unknownPacket() { - val packet = PacketData.Unknown(body = "Test").toTestPacket().copy( - route = testRoute.copy(source = StationAddress("TEST", "4")) - ).toTestCapturedPacket() - - val result = factoryWithNullMarkers.create(LogDetailData(packet = packet)) - - assertFalse(result.mapable, "Not mapable for non-map data") - assertNull(result.temperature, "No temperature for non weather packet") - assertNull(result.wind, "No wind data for non weather packet") - assertNull(result.comment, "No comment for unknown packet") - assertNull(result.altitude, "No altitude for unknown packet") - assertNull(result.telemetryValues, "No telemetry for unknown packet") - assertNull(result.telemetrySequence, "No telemetry for unknown packet") - assertNull(result.rawSource, "Debug data hidden") - } - - @Test - fun positionlessWeatherPacket() { - val packet = PacketData.Weather( - temperature = 72.fahrenheit, - windData = WindData( - direction = 12.degrees, - speed = 34.milesPerHour, - gust = 56.milesPerHour, - ) - ).toTestPacket().toTestCapturedPacket() - val data = LogDetailData( - packet = packet, - ) - - val result = factoryWithNullMarkers.create(data) - - assertFalse(result.mapable, "Not mapable for non-map data") - assertEquals("72ºF", result.temperature) - assertEquals("12º|34mph|56mph", result.wind) - assertNull(result.comment, "No comment for positionless weather") - assertNull(result.altitude, "No altitude for positionless weather") - assertNull(result.telemetryValues, "No telemetry for positionless weather") - assertNull(result.telemetrySequence, "No telemetry for positionless weather") - assertNull(result.rawSource, "Debug data hidden") - } - - @Test - fun weatherPacket() { - val packet = PacketData.Weather( - coordinates = GeoCoordinates(1.0.latitude, 2.0.longitude), - temperature = 72.fahrenheit, - windData = WindData(12.degrees, 34.milesPerHour, 56.milesPerHour), - ).toTestPacket().toTestCapturedPacket() - val data = LogDetailData( - packet = packet, - ) - - val result = factoryWithDummyMarkers.create(data) - - assertTrue(result.mapable, "Mapable weather data shows map") - assertEquals("72ºF", result.temperature) - assertEquals("12º|34mph|56mph", result.wind) - assertNull(result.comment, "No comment for weather packet") - assertNull(result.altitude, "No altitude for weather packet") - assertNull(result.telemetryValues, "No telemetry for weather packet") - assertNull(result.telemetrySequence, "No telemetry for weather packet") - assertNull(result.rawSource, "Debug data hidden") - } - - @Test - fun emptyWeatherPacket() { - val packet = PacketData.Weather( - coordinates = GeoCoordinates(1.latitude, 2.longitude), - ).toTestPacket().toTestCapturedPacket() - val data = LogDetailData( - packet = packet, - ) - - val result = factoryWithDummyMarkers.create(data) - - assertTrue(result.mapable, "Mapable weather data shows map") - assertNull(result.temperature, "No temperature for empty weather packet") - assertNull(result.wind, "No wind data for empty non weather packet") - assertNull(result.comment, "No comment for unknown packet") - assertNull(result.altitude, "No altitude for unknown packet") - assertNull(result.telemetryValues, "No telemetry for unknown packet") - assertNull(result.telemetrySequence, "No telemetry for unknown packet") - assertNull(result.rawSource, "Debug data hidden") - } - - @Test - fun positionPacket() { - val packet = PacketData.Position( - coordinates = GeoCoordinates(1.latitude, 2.longitude), - symbol = symbolOf('/', 'a'), - comment = "test", - ).toTestPacket().toTestCapturedPacket() - val data = LogDetailData( - packet = packet, - ) - - val result = factoryWithDummyMarkers.create(data) - - assertTrue(result.mapable, "Mapable position data shows map") - assertNull(result.temperature, "No temperature for non weather packet") - assertNull(result.wind, "No wind data for non weather packet") - assertEquals("test", result.comment) - assertNull(result.altitude, "No altitude for position packet") - assertNull(result.telemetryValues, "No telemetry for position packet") - assertNull(result.telemetrySequence, "No telemetry for position packet") - assertNull(result.rawSource, "Debug data hidden") - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/map/MarkerViewStateFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/map/MarkerViewStateFactoryTest.kt deleted file mode 100644 index 8fddafc7..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/map/MarkerViewStateFactoryTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.inkapplications.ack.android.map - -import android.graphics.Bitmap -import com.inkapplications.ack.android.symbol.SymbolFactory -import com.inkapplications.ack.android.symbol.SymbolFactoryDummy -import com.inkapplications.ack.android.toTestCapturedPacket -import com.inkapplications.ack.android.toTestPacket -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.Symbol -import inkapplications.spondee.spatial.GeoCoordinates -import inkapplications.spondee.spatial.latitude -import inkapplications.spondee.spatial.longitude -import kotlin.test.Test -import kotlin.test.assertNull -import kotlin.test.assertTrue - -class MarkerViewStateFactoryTest { - val symbolFactoryStub = object: SymbolFactory { - override val defaultSymbol: Bitmap? get() = TODO() - override fun createSymbol(symbol: Symbol): Bitmap? = TODO() - } - - @Test - fun unknown() { - val factory = MarkerViewStateFactory(symbolFactoryStub) - val packet = PacketData.Unknown(body = "") - val result = factory.create(packet.toTestPacket().toTestCapturedPacket()) - - assertNull(result, "Unknown packet should not produce a marker") - } - - @Test - fun noCoordinates() { - val factory = MarkerViewStateFactory(symbolFactoryStub) - val packet = PacketData.Weather() - val result = factory.create(packet.toTestPacket().toTestCapturedPacket()) - - assertNull(result, "Mappable Packet without location should not produce a marker") - } - - @Test - fun noSymbol() { - val factory = MarkerViewStateFactory(SymbolFactoryDummy) - val packet = PacketData.Weather() - val result = factory.create(packet.toTestPacket().toTestCapturedPacket()) - - assertNull(result, "Mappable Packet without location should not produce a marker") - } - - @Test - fun marker() { - val symbolFactorySpy = object: SymbolFactory by symbolFactoryStub { - var called = false - override fun createSymbol(symbol: Symbol): Bitmap? = null.also { - called = true - } - - } - val factory = MarkerViewStateFactory(symbolFactorySpy) - val packet = PacketData.Weather(coordinates = GeoCoordinates(0.latitude, 0.longitude)) - val result = factory.create(packet.toTestPacket().toTestCapturedPacket()) - - assertTrue(symbolFactorySpy.called, "Symbol is generated when coordinates specified") - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/onboard/OnboardingStateFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/onboard/OnboardingStateFactoryTest.kt deleted file mode 100644 index 737a1d7f..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/onboard/OnboardingStateFactoryTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.inkapplications.ack.android.onboard - -import com.inkapplications.ack.android.settings.Passcode -import com.inkapplications.ack.structures.station.StationAddress -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class OnboardingStateFactoryTest { - private val factory = OnboardingStateFactory() - - @Test - fun agreementMismatch() { - val data = OnboardingStateFactory.OnboardingData( - latestRevision = 2, - agreementRevision = 4, - ) - - val result = factory.screenState(data) - - assertTrue(result is OnboardingState.UserAgreement) - } - - @Test - fun licenseIncomplete() { - val data = OnboardingStateFactory.OnboardingData( - latestRevision = 2, - agreementRevision = 2, - completedLicense = false, - ) - - val result = factory.screenState(data) - - assertTrue(result is OnboardingState.LicensePrompt) - assertEquals("", result.initialValues.callsign) - assertEquals("", result.initialValues.passcode) - } - - @Test - fun licenseIncompleteWithData() { - val data = OnboardingStateFactory.OnboardingData( - latestRevision = 2, - agreementRevision = 2, - completedLicense = false, - currentAddress = StationAddress("TEST", "7"), - currentPasscode = Passcode(1234), - ) - - val result = factory.screenState(data) - - assertTrue(result is OnboardingState.LicensePrompt) - assertEquals("TEST-7", result.initialValues.callsign) - assertEquals("1234", result.initialValues.passcode) - } - - @Test - fun licenseComplete() { - val data = OnboardingStateFactory.OnboardingData( - latestRevision = 2, - agreementRevision = 2, - completedLicense = true, - ) - - val result = factory.screenState(data) - - assertTrue(result is OnboardingState.Complete) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/settings/CompositeSettingsProviderTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/settings/CompositeSettingsProviderTest.kt deleted file mode 100644 index f5cd89cb..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/settings/CompositeSettingsProviderTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.inkapplications.ack.android.settings - -import org.junit.Assert.assertTrue -import org.junit.Test - -class CompositeSettingsProviderTest { - @Test - fun getSettings() { - val first = object: SettingsProvider { - override val settings: List = listOf(stringSetting) - } - val second = object: SettingsProvider { - override val settings: List = listOf(intSetting) - } - val provider = CompositeSettingsProvider(setOf(first, second)) - - val result = provider.settings - - assertTrue("Both settings are in combined result", stringSetting in result) - assertTrue("Both settings are in combined result", intSetting in result) - } - - @Test - fun emptySettings() { - val provider = CompositeSettingsProvider(setOf()) - - val result = provider.settings - - assertTrue("No settings providers results in empty set", result.isEmpty()) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/settings/PrioritySettingValuesTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/settings/PrioritySettingValuesTest.kt deleted file mode 100644 index 918abb59..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/settings/PrioritySettingValuesTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.inkapplications.ack.android.settings - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Test - -class PrioritySettingValuesTest { - @Test - fun firstIsPrioritized() { - val first = object: SettingsReadAccess by SettingsReadAccessStub { - override fun observeStringState(setting: StringSetting): Flow = flowOf("first") - } - val second = object: SettingsReadAccess by SettingsReadAccessStub { - override fun observeStringState(setting: StringSetting): Flow = flowOf("second") - } - val values = PrioritySettingValues(first, second) - - runBlocking { - assertEquals("first", values.observeStringState(stringSetting).first()) - } - } - - @Test - fun secondIsUsed() { - val first = object: SettingsReadAccess by SettingsReadAccessStub { - override fun observeStringState(setting: StringSetting): Flow = flowOf(null) - } - val second = object: SettingsReadAccess by SettingsReadAccessStub { - override fun observeStringState(setting: StringSetting): Flow = flowOf("second") - } - val values = PrioritySettingValues(first, second) - - runBlocking { - assertEquals("second", values.observeStringState(stringSetting).first()) - } - } - - @Test - fun noSetting() { - val first = object: SettingsReadAccess by SettingsReadAccessStub { - override fun observeStringState(setting: StringSetting): Flow = flowOf(null) - } - val second = object: SettingsReadAccess by SettingsReadAccessStub { - override fun observeStringState(setting: StringSetting): Flow = flowOf(null) - } - val values = PrioritySettingValues(first, second) - - runBlocking { - assertNull(values.observeStringState(stringSetting).first()) - } - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/settings/SettingsAccessTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/settings/SettingsAccessTest.kt deleted file mode 100644 index 6d780c56..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/settings/SettingsAccessTest.kt +++ /dev/null @@ -1,197 +0,0 @@ -package com.inkapplications.ack.android.settings - -import com.inkapplications.ack.android.ParrotStringResources -import com.inkapplications.ack.android.connection.ConnectionSettings -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.test.runTest -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class SettingsAccessTest { - @Test - fun items() = runTest { - val access = SettingsAccess( - SettingsProviderStub, - SettingsReadAccessStub, - SettingsWriteAccessStub, - ConnectionSettings(ParrotStringResources), - ) - - val result = access.settingsGroups(SettingVisibility.Advanced).first() - val list = result.settings.map { it.settings }.flatten() - - assertEquals(2, list.size, "Provider should yield a result for each Setting.") - val (first, second) = list - - assertTrue(first is SettingState.IntState) - assertEquals(intSetting.key, first.setting.key, "Settings key is preserved.") - assertEquals(intSetting.name, first.setting.name, "Settings name is preserved.") - assertEquals(1, first.value, "Setting value is provided by read access stub.") - - assertTrue(second is SettingState.StringState, "First setting should be a string item.") - assertEquals(stringSetting.key, second.setting.key, "Settings key is preserved.") - assertEquals(stringSetting.name, second.setting.name, "Settings name is preserved.") - assertEquals("foo - test.string", second.value, "Setting value is provided by read access stub.") - } - - @Test - fun advancedHidden() = runTest { - val settingsProvider = object: SettingsProvider { - override val settings: List = listOf( - stringSetting, - StringSetting(key = "advanced", visibility = SettingVisibility.Advanced, name = "advanced", categoryName = stringSetting.categoryName, defaultValue = "advanced") - ) - } - - val access = SettingsAccess( - settingsProvider, - SettingsReadAccessStub, - SettingsWriteAccessStub, - ConnectionSettings(ParrotStringResources), - ) - - val result = access.settingsGroups(SettingVisibility.Visible).first() - val list = result.settings.map { it.settings }.flatten() - - assertEquals(1, list.size, "List is filtered to only simple settings.") - assertEquals(stringSetting.key, (list.first() as SettingState.StringState).setting.key, "Advanced settings are removed from settings list.") - } - - @Test - fun advancedShown() = runTest { - val settingsProvider = object: SettingsProvider { - override val settings: List = listOf( - stringSetting, - StringSetting(key = "advanced", visibility = SettingVisibility.Advanced, name = "zz_advanced", categoryName = stringSetting.categoryName, defaultValue = "advanced") - ) - } - - val access = SettingsAccess( - settingsProvider, - SettingsReadAccessStub, - SettingsWriteAccessStub, - ConnectionSettings(ParrotStringResources), - ) - - val result = access.settingsGroups(SettingVisibility.Advanced).first() - val list = result.settings.map { it.settings }.flatten() - - assertEquals(2, list.size, "List is filtered to only simple settigns.") - assertEquals(stringSetting.key, (list[0] as SettingState.StringState).setting.key, "Standard settings are still shown.") - assertEquals("advanced", (list[1] as SettingState.StringState).setting.key, "Advanced settings are removed from settings list.") - } - - @Test - fun categorized() = runTest { - val settingsProvider = object: SettingsProvider { - override val settings: List = listOf( - StringSetting(key = "B1", categoryName = "B", name = "B1", defaultValue = "test"), - StringSetting(key = "C1", categoryName = "C", name = "C1", defaultValue = "test"), - StringSetting(key = "A1", categoryName = "A", name = "A1", defaultValue = "test"), - StringSetting(key = "B2", categoryName = "B", name = "B2", defaultValue = "test"), - ) - } - - val access = SettingsAccess( - settingsProvider, - SettingsReadAccessStub, - SettingsWriteAccessStub, - ConnectionSettings(ParrotStringResources), - ) - - val result = access.settingsGroups(SettingVisibility.Advanced).first() - - assertEquals(3, result.settings.size, "Settings are broken into categories by name") - - val (first, second, third) = result.settings - - assertEquals("A", first.name, "Categories are sorted by name") - assertEquals(1, first.settings.size, "Settings are kept in their category") - assertEquals("A1", first.settings[0].setting.key, "Settings are kept in their category") - - assertEquals("B", second.name, "Categories are sorted by name") - assertEquals(2, second.settings.size, "Settings are kept in their category") - assertEquals("B1", second.settings[0].setting.key, "Settings are kept in their category") - assertEquals("B2", second.settings[1].setting.key, "Settings are kept in their category") - - assertEquals("C", third.name, "Categories are sorted by name") - assertEquals(1, third.settings.size, "Settings are kept in their category") - assertEquals("C1", third.settings[0].setting.key, "Settings are kept in their category") - } - - @Test - fun licensePromptFieldValues() = runTest { - val fakeSettings = object: SettingsReadAccess by SettingsReadAccessStub { - override fun observeStringState(setting: StringSetting): Flow { - return flow { emit("KE0YOG") } - } - - override fun observeIntState(setting: IntSetting): Flow { - return flow { emit(12345) } - } - } - val access = SettingsAccess( - SettingsProviderStub, - fakeSettings, - SettingsWriteAccessStub, - ConnectionSettings(ParrotStringResources), - ) - - val result = access.licensePromptFieldValues.first() - - assertEquals("KE0YOG", result.callsign) - assertEquals("12345", result.passcode) - } - - @Test - fun emptyLicensePromptFieldValues() = runTest { - val fakeSettings = object: SettingsReadAccess by SettingsReadAccessStub { - override fun observeStringState(setting: StringSetting): Flow { - return flow { emit(null) } - } - - override fun observeIntState(setting: IntSetting): Flow { - return flow { emit(null) } - } - } - val access = SettingsAccess( - SettingsProviderStub, - fakeSettings, - SettingsWriteAccessStub, - ConnectionSettings(ParrotStringResources), - ) - - val result = access.licensePromptFieldValues.first() - - assertEquals("", result.callsign) - assertEquals("", result.passcode) - } - - @Test - fun sorted() = runTest { - val settingsProvider = object: SettingsProvider { - override val settings: List = listOf( - StringSetting(key = "3", name = "C", categoryName = "test", defaultValue = "test"), - StringSetting(key = "4", name = "D", categoryName = "test", defaultValue = "test"), - StringSetting(key = "1", name = "A", categoryName = "test", defaultValue = "test"), - StringSetting(key = "2", name = "B", categoryName = "test", defaultValue = "test"), - ) - } - - val access = SettingsAccess( - settingsProvider, - SettingsReadAccessStub, - SettingsWriteAccessStub, - ConnectionSettings(ParrotStringResources), - ) - - val result = access.settingsGroups(SettingVisibility.Advanced).first() - val list = result.settings.map { it.settings.map { it.setting.name } }.flatten() - - assertEquals(listOf("A", "B", "C", "D"), list, "Settings are sorted by name") - - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/settings/SettingsDoubles.kt b/android-application/src/test/java/com/inkapplications/ack/android/settings/SettingsDoubles.kt deleted file mode 100644 index 8198b62d..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/settings/SettingsDoubles.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.inkapplications.ack.android.settings - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -val stringSetting = StringSetting("test.string", "test string", "category", "default") -val intSetting = IntSetting("test.int", "test int", "category", 1234) - -object SettingsWriteAccessStub: SettingsWriteAccess { - override fun setString(setting: StringSetting, value: String) = Unit - override fun setInt(setting: IntSetting, value: Int) = Unit - override fun setBoolean(setting: BooleanSetting, value: Boolean) = Unit -} - -object SettingsReadAccessStub: SettingsReadAccess { - override fun observeStringState(setting: StringSetting): Flow = flowOf("foo - ${setting.key}") - override fun observeIntState(setting: IntSetting): Flow = flowOf(1) - override fun observeBooleanState(setting: BooleanSetting): Flow = flowOf(false) -} - -object SettingsProviderStub: SettingsProvider { - override val settings: List = listOf(stringSetting, intSetting) -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/settings/SettingsViewStateFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/settings/SettingsViewStateFactoryTest.kt deleted file mode 100644 index 7075240c..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/settings/SettingsViewStateFactoryTest.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.inkapplications.ack.android.settings - -import com.inkapplications.ack.structures.station.StationAddress -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class SettingsViewStateFactoryTest { - private val factory = SettingsViewStateFactory() - - @Test - fun registered() { - val data = LicenseData( - address = StationAddress("TEST", "7"), - passcode = null, - ) - - val result = factory.licenseState(data) - - assertTrue(result is LicenseViewState.Registered) - assertEquals("TEST-7", result.callsign) - } - - @Test - fun verified() { - val data = LicenseData( - address = StationAddress("TEST", "7"), - passcode = Passcode(1234), - ) - - val result = factory.licenseState(data) - - assertTrue(result is LicenseViewState.Verified) - assertEquals("TEST-7", result.callsign) - } - - @Test - fun unconfigured() { - val data = LicenseData( - address = null, - passcode = null, - ) - - val result = factory.licenseState(data) - - assertTrue(result is LicenseViewState.Unconfigured) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/settings/buildinfo/BuildInfoFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/settings/buildinfo/BuildInfoFactoryTest.kt deleted file mode 100644 index 0ab3d0f4..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/settings/buildinfo/BuildInfoFactoryTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.inkapplications.ack.android.settings.buildinfo - -import com.inkapplications.ack.android.ParrotStringResources -import com.inkapplications.android.extensions.StringResources -import kimchi.logger.EmptyLogger -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue - -class BuildInfoFactoryTest { - private val factory = BuildInfoFactory( - stringResources = ParrotStringResources, - logger = EmptyLogger, - ) - private val data = BuildData( - buildType = "debug", - versionName = "1.0.0", - versionCode = 1, - commit = "ffac537e6cbbf934b08745a378932722df287a53", - usePlayServices = true, - playServicesAvailable = true, - ) - - @Test - fun release() { - val result = factory.buildInfo(data.copy(buildType = "release")) - - assertTrue(result is BuildInfoState.Release) - assertEquals(result.playServices, BuildInfoState.ServiceState.Available) - assertEquals("1.0.0|1|ffac537", result.versionStatment) - } - - @Test - fun shortHash() { - val result = factory.buildInfo(data.copy(buildType = "release", commit = "ff")) - - assertTrue(result is BuildInfoState.Release) - assertEquals("1.0.0|1|ff", result.versionStatment) - } - - @Test - fun noHash() { - val factory = BuildInfoFactory( - stringResources = object: StringResources by ParrotStringResources { - override fun getString(key: Int): String = "Test" - }, - logger = EmptyLogger, - ) - val result = factory.buildInfo(data.copy(buildType = "release", commit = null)) - - assertTrue(result is BuildInfoState.Release) - assertEquals("1.0.0|1|Test", result.versionStatment) - } - - @Test - fun debugWithPlayServices() { - val result = factory.buildInfo(data) - - assertTrue(result is BuildInfoState.Debug) - assertEquals(result.playServices, BuildInfoState.ServiceState.Available) - } - - @Test - fun playServicesMissing() { - val result = factory.buildInfo(data.copy(playServicesAvailable = false)) - - assertTrue(result is BuildInfoState.Debug) - assertEquals(result.playServices, BuildInfoState.ServiceState.Unavailable) - } - - @Test - fun playServicesUnconfigured() { - val result = factory.buildInfo(data.copy(usePlayServices = false)) - - assertTrue(result is BuildInfoState.Debug) - assertEquals(result.playServices, BuildInfoState.ServiceState.Unconfigured) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/MileTransformerTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/MileTransformerTest.kt deleted file mode 100644 index 22cfa444..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/MileTransformerTest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -import inkapplications.spondee.measure.us.miles -import kotlin.test.Test -import kotlin.test.assertEquals - -class MileTransformerTest { - @Test - fun toStorage() { - assertEquals(5, 5.miles.let(MileTransformer::toStorage)) - assertEquals(2, 1.5.miles.let(MileTransformer::toStorage)) - } - - @Test - fun toData() { - assertEquals(5.miles, MileTransformer.toData(5)) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/MillisecondTransformerTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/MillisecondTransformerTest.kt deleted file mode 100644 index a2a35273..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/MillisecondTransformerTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.time.Duration.Companion.seconds - -class MillisecondTransformerTest { - @Test - fun toStorage() { - val result = MillisecondTransformer.toStorage(45.seconds) - - assertEquals(45000, result) - } - - @Test - fun fromStorage() { - val result = MillisecondTransformer.toData(45000) - - assertEquals(45.seconds, result) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/MinuteTransformerTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/MinuteTransformerTest.kt deleted file mode 100644 index 7a085398..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/MinuteTransformerTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.time.Duration.Companion.hours - -class MinuteTransformerTest { - @Test - fun toStorage() { - val result = MinuteTransformer.toStorage(1.5.hours) - - assertEquals(90, result) - } - - @Test - fun fromStorage() { - val result = MinuteTransformer.toData(90) - - assertEquals(1.5.hours, result) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/SentinelOptionalTransformerTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/SentinelOptionalTransformerTest.kt deleted file mode 100644 index 1cf96c26..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/SentinelOptionalTransformerTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class SentinelOptionalTransformerTest { - private val backingFake = object: Transformer { - override fun toStorage(data: String): String = "STORED" - override fun toData(storage: String): String = "DATA" - } - val transformer = SentinelOptionalTransformer("TestNull", backingFake) - - @Test - fun toData() { - assertEquals("DATA", transformer.toData("Test")) - assertNull(transformer.toData("TestNull")) - } - - @Test - fun toStorage() { - assertEquals("STORED", transformer.toStorage("Test")) - assertEquals("TestNull", transformer.toStorage(null)) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/StationAddressTransformerTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/StationAddressTransformerTest.kt deleted file mode 100644 index ec83ad70..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/settings/transformer/StationAddressTransformerTest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.inkapplications.ack.android.settings.transformer - -import com.inkapplications.ack.structures.station.StationAddress -import kotlin.test.Test -import kotlin.test.assertEquals - -class StationAddressTransformerTest { - @Test - fun toData() { - assertEquals(StationAddress("KE0YOG"), StationAddressTransformer.toData("KE0YOG")) - assertEquals(StationAddress("KE0YOG", "2"), StationAddressTransformer.toData("KE0YOG-2")) - } - - @Test - fun toStorage() { - assertEquals("KE0YOG", StationAddress("KE0YOG").let(StationAddressTransformer::toStorage)) - assertEquals("KE0YOG-2", StationAddress("KE0YOG", "2").let(StationAddressTransformer::toStorage)) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/station/StationInsightViewStateFactoryTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/station/StationInsightViewStateFactoryTest.kt deleted file mode 100644 index 731f1df4..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/station/StationInsightViewStateFactoryTest.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.inkapplications.ack.android.station - -import com.inkapplications.ack.android.ParrotStringResources -import com.inkapplications.ack.android.log.SummaryFactory -import com.inkapplications.ack.android.testRoute -import com.inkapplications.ack.android.toTestCapturedPacket -import com.inkapplications.ack.android.toTestPacket -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.data.PacketOrigin -import com.inkapplications.ack.structures.AprsPacket -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.WindData -import com.inkapplications.ack.structures.station.StationAddress -import com.inkapplications.ack.structures.symbolOf -import inkapplications.spondee.measure.us.fahrenheit -import inkapplications.spondee.measure.us.milesPerHour -import inkapplications.spondee.spatial.GeoCoordinates -import inkapplications.spondee.spatial.degrees -import inkapplications.spondee.spatial.latitude -import inkapplications.spondee.spatial.longitude -import kotlinx.datetime.Instant -import org.junit.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class StationInsightViewStateFactoryTest { - val dummySummaryFactory = SummaryFactory(ParrotStringResources) - - @Test - fun unknownPacket() { - val factory = StationInsightViewStateFactory(dummySummaryFactory) - val packet = CapturedPacket( - id = CaptureId(1), - received = Instant.fromEpochMilliseconds(2), - parsed = AprsPacket( - route = testRoute.copy( - source = StationAddress("KE0YOG", "1"), - ), - data = PacketData.Unknown( - body = "test", - ), - ), - origin = PacketOrigin.AprsIs, - raw = byteArrayOf(), - ) - val data = StationData( - packets = listOf(packet), - metric = false, - ) - val result = factory.create(data) - - assertEquals("KE0YOG", result.name) - assertNull(result.temperature, "No temperature for non weather packet") - assertNull(result.wind, "No wind data for non weather packet") - assertNull(result.comment, "No comment for unknown packet") - assertNull(result.altitude, "No altitude for unknown packet") - assertNull(result.telemetryValues, "No telemetry for unknown packet") - assertNull(result.telemetrySequence, "No telemetry for unknown packet") - } - - @Test - fun positionlessWeatherPacket() { - val factory = StationInsightViewStateFactory(dummySummaryFactory) - val packet = PacketData.Weather( - temperature = 72.fahrenheit, - windData = WindData( - direction = 12.degrees, - speed = 34.milesPerHour, - gust = 56.milesPerHour, - ) - ).toTestPacket().toTestCapturedPacket() - val data = StationData( - packets = listOf(packet), - metric = false, - ) - - val result = factory.create(data) - - assertEquals("KE0YOG", result.name) - assertEquals("72ºF", result.temperature) - assertEquals("12º|34mph|56mph", result.wind) - assertNull(result.comment, "No comment for positionless weather") - assertNull(result.altitude, "No altitude for positionless weather") - assertNull(result.telemetryValues, "No telemetry for positionless weather") - assertNull(result.telemetrySequence, "No telemetry for positionless weather") - } - - @Test - fun weatherPacket() { - val factory = StationInsightViewStateFactory(dummySummaryFactory) - val packet = PacketData.Weather( - coordinates = GeoCoordinates(1.0.latitude, 2.0.longitude), - temperature = 72.fahrenheit, - windData = WindData(12.degrees, 34.milesPerHour, 56.milesPerHour), - ).toTestPacket().toTestCapturedPacket() - val data = StationData( - packets = listOf(packet), - metric = false, - ) - - val result = factory.create(data) - - assertEquals("KE0YOG", result.name) - assertEquals("72ºF", result.temperature) - assertEquals("12º|34mph|56mph", result.wind) - assertNull(result.comment, "No comment for weather packet") - assertNull(result.altitude, "No altitude for weather packet") - assertNull(result.telemetryValues, "No telemetry for weather packet") - assertNull(result.telemetrySequence, "No telemetry for weather packet") - } - - @Test - fun emptyWeatherPacket() { - val factory = StationInsightViewStateFactory(dummySummaryFactory) - val packet = PacketData.Weather( - coordinates = GeoCoordinates(1.latitude, 2.longitude), - ).toTestPacket().toTestCapturedPacket() - val data = StationData( - packets = listOf(packet), - metric = false, - ) - - val result = factory.create(data) - - assertEquals("KE0YOG", result.name) - assertNull(result.temperature, "No temperature for empty weather packet") - assertNull(result.wind, "No wind data for empty non weather packet") - assertNull(result.comment, "No comment for unknown packet") - assertNull(result.altitude, "No altitude for unknown packet") - assertNull(result.telemetryValues, "No telemetry for unknown packet") - assertNull(result.telemetrySequence, "No telemetry for unknown packet") - } - - @Test - fun positionPacket() { - val factory = StationInsightViewStateFactory(dummySummaryFactory) - val packet = PacketData.Position( - coordinates = GeoCoordinates(1.latitude, 2.longitude), - symbol = symbolOf('/', 'a'), - comment = "test", - ).toTestPacket().toTestCapturedPacket() - val data = StationData( - packets = listOf(packet), - metric = false, - ) - - val result = factory.create(data) - - assertEquals("KE0YOG", result.name) - assertNull(result.temperature, "No temperature for non weather packet") - assertNull(result.wind, "No wind data for non weather packet") - assertEquals("test", result.comment) - assertNull(result.altitude, "No altitude for position packet") - assertNull(result.telemetryValues, "No telemetry for position packet") - assertNull(result.telemetrySequence, "No telemetry for position packet") - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/symbol/SymbolDoubles.kt b/android-application/src/test/java/com/inkapplications/ack/android/symbol/SymbolDoubles.kt deleted file mode 100644 index a656068d..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/symbol/SymbolDoubles.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.inkapplications.ack.android.symbol - -import android.graphics.Bitmap -import com.inkapplications.ack.structures.Symbol - -object SymbolFactoryDummy: SymbolFactory { - override val defaultSymbol: Bitmap? = null - override fun createSymbol(symbol: Symbol): Bitmap? = null -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/symbol/SymbolResourceLocatorTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/symbol/SymbolResourceLocatorTest.kt deleted file mode 100644 index ab34668e..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/symbol/SymbolResourceLocatorTest.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.inkapplications.ack.android.symbol - -import com.inkapplications.ack.structures.symbolOf -import kimchi.logger.EmptyLogger -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Test - -class SymbolResourceLocatorTest { - @Test - fun primaryResources() { - val locator = SymbolResourceLocator(EmptyLogger) - - assertEquals("symbol_0", locator.getBaseResourceName(symbolOf('!', '/'))) - assertEquals("symbol_93", locator.getBaseResourceName(symbolOf('~', '/'))) - } - - @Test - fun alternateResources() { - val locator = SymbolResourceLocator(EmptyLogger) - - assertEquals("symbol_96", locator.getBaseResourceName(symbolOf('!', '\\'))) - assertEquals("symbol_189", locator.getBaseResourceName(symbolOf('~', '\\'))) - } - - @Test - fun overlayResources() { - val locator = SymbolResourceLocator(EmptyLogger) - - assertEquals("symbol_207", locator.getOverlayResourceName(symbolOf('#', '0'))) - assertEquals("symbol_216", locator.getOverlayResourceName(symbolOf('z', '9'))) - assertEquals("symbol_224", locator.getOverlayResourceName(symbolOf('A', 'A'))) - assertEquals("symbol_249", locator.getOverlayResourceName(symbolOf('^', 'Z'))) - } - - @Test - fun unsupportedOverlay() { - val locator = SymbolResourceLocator(EmptyLogger) - - assertNull(locator.getOverlayResourceName(symbolOf('!', '0'))) - assertNull(locator.getOverlayResourceName(symbolOf('~', '9'))) - assertNull(locator.getOverlayResourceName(symbolOf('1', 'A'))) - assertNull(locator.getOverlayResourceName(symbolOf('B', 'Z'))) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/transmit/PathTransformerTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/transmit/PathTransformerTest.kt deleted file mode 100644 index de18d858..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/transmit/PathTransformerTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.inkapplications.ack.android.transmit - -import com.inkapplications.ack.structures.Digipeater -import com.inkapplications.ack.structures.station.StationAddress -import kotlin.test.Test -import kotlin.test.assertEquals - -class PathTransformerTest { - @Test - fun toStorage() { - val input = listOf( - Digipeater(StationAddress("KE0YOG")), - ) - - val result = PathTransformer.toStorage(input) - - assertEquals("KE0YOG", result) - } - - @Test - fun toStorageMultiple() { - val input = listOf( - Digipeater(StationAddress("KE0YOG")), - Digipeater(StationAddress("KE0YOF", "2")), - ) - - val result = PathTransformer.toStorage(input) - - assertEquals("KE0YOG,KE0YOF-2", result) - } - - @Test - fun toData() { - val expected = listOf( - Digipeater(StationAddress("KE0YOG")), - ) - val result = PathTransformer.toData("KE0YOG") - - assertEquals(expected, result) - } - - @Test - fun toDataMultiple() { - val expected = listOf( - Digipeater(StationAddress("KE0YOG")), - Digipeater(StationAddress("KE0YOF", "2")), - ) - val result = PathTransformer.toData("KE0YOG,KE0YOF-2") - - assertEquals(expected, result) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/transmit/PercentageTransformer.kt b/android-application/src/test/java/com/inkapplications/ack/android/transmit/PercentageTransformer.kt deleted file mode 100644 index e34f37f4..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/transmit/PercentageTransformer.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.inkapplications.ack.android.transmit - -import com.inkapplications.ack.android.settings.transformer.PercentageTransformer -import inkapplications.spondee.scalar.percent -import kotlin.test.Test -import kotlin.test.assertEquals - -class PercentageTransformer { - @Test - fun toData() { - assertEquals(69.percent, PercentageTransformer.toData(69)) - } - - @Test - fun toStorage() { - assertEquals(69, 69.percent.let(PercentageTransformer::toStorage)) - assertEquals(70, 69.5.percent.let(PercentageTransformer::toStorage)) - } -} diff --git a/android-application/src/test/java/com/inkapplications/ack/android/transmit/SymbolTransformerTest.kt b/android-application/src/test/java/com/inkapplications/ack/android/transmit/SymbolTransformerTest.kt deleted file mode 100644 index d2068224..00000000 --- a/android-application/src/test/java/com/inkapplications/ack/android/transmit/SymbolTransformerTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.inkapplications.ack.android.transmit - -import com.inkapplications.ack.structures.Symbol -import kotlin.test.Test -import kotlin.test.assertEquals - -class SymbolTransformerTest { - @Test - fun toStorage() { - assertEquals("$/", Symbol.Primary('$').let(SymbolTransformer::toStorage)) - } - - @Test - fun toData() { - assertEquals(Symbol.Primary('$'), SymbolTransformer.toData("$/")) - } -} diff --git a/android-extensions/build.gradle.kts b/android-extensions/build.gradle.kts deleted file mode 100644 index 1f4177a3..00000000 --- a/android-extensions/build.gradle.kts +++ /dev/null @@ -1,47 +0,0 @@ -plugins { - id("com.android.library") - kotlin("android") - kotlin("kapt") -} - -android { - namespace = "com.inkapplications.android.extensions" - compileSdk = 33 - - defaultConfig { - minSdk = 21 - } - - buildFeatures { - compose = true - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() - } - compileOptions { - targetCompatibility = JavaVersion.VERSION_11 - sourceCompatibility = JavaVersion.VERSION_11 - } -} - -dependencies { - implementation(libs.coroutines.core) - - api(libs.androidx.core) - api(libs.androidx.compose.ui) - api(libs.androidx.compose.foundation) - implementation(libs.androidx.preference) - - api(libs.ack.structures) - - implementation(libs.bundles.dagger.libraries) - kapt(libs.bundles.dagger.kapt) - - testImplementation(libs.junit) - testImplementation(libs.coroutines.test) - testImplementation(libs.kotlin.test.core) -} diff --git a/android-extensions/src/main/AndroidManifest.xml b/android-extensions/src/main/AndroidManifest.xml deleted file mode 100644 index 508d5b01..00000000 --- a/android-extensions/src/main/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/Activities.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/Activities.kt deleted file mode 100644 index fff7f026..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/Activities.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.inkapplications.android.extensions - -import android.app.Activity -import android.app.Application -import android.os.Bundle - -/** - * Sends lifecycle event logs to a logger. - */ -class LifecycleLogger( - private val logger: (String) -> Unit -): Application.ActivityLifecycleCallbacks { - override fun onActivityPaused(activity: Activity) { - logger("${activity.javaClass.simpleName} -> Paused") - } - - override fun onActivityStarted(activity: Activity) { - logger("${activity.javaClass.simpleName} -> Started") - } - - override fun onActivityDestroyed(activity: Activity) { - logger("${activity.javaClass.simpleName} -> Destroyed") - } - - override fun onActivitySaveInstanceState(activity: Activity, p1: Bundle) { - logger("${activity.javaClass.simpleName} -> Saved Instance") - } - - override fun onActivityStopped(activity: Activity) { - logger("${activity.javaClass.simpleName} -> Stopped") - } - - override fun onActivityCreated(activity: Activity, bundle: Bundle?) { - logger("${activity.javaClass.simpleName} -> Created") - } - - override fun onActivityResumed(activity: Activity) { - logger("${activity.javaClass.simpleName} -> Resumed") - } -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/ApplicationModule.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/ApplicationModule.kt deleted file mode 100644 index 11dc2feb..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/ApplicationModule.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.inkapplications.android.extensions - -import android.app.Application -import android.app.NotificationManager -import android.bluetooth.BluetoothAdapter -import android.content.Context -import android.content.SharedPreferences -import android.content.res.Resources -import android.location.LocationManager -import androidx.preference.PreferenceManager -import com.inkapplications.android.extensions.bluetooth.AndroidBluetoothAccess -import com.inkapplications.android.extensions.bluetooth.BluetoothDeviceAccess -import com.inkapplications.android.extensions.bluetooth.DummyBluetoothDeviceAccess -import com.inkapplications.android.extensions.format.AndroidResourceDateTimeFormatter -import com.inkapplications.android.extensions.format.DateTimeFormatter -import com.inkapplications.android.extensions.location.AndroidLocationAccess -import com.inkapplications.android.extensions.location.LocationAccess -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.Reusable -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module(includes = [ StaticApplicationModule::class ]) -@InstallIn(SingletonComponent::class) -class ApplicationModule { - @Provides - fun context(application: Application): Context = application - - @Provides - fun sharedPreferences(application: Application): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) - - @Provides - fun resources(application: Application): Resources = application.resources - - @Provides - fun locationManager(application: Application) = application.getSystemService(Context.LOCATION_SERVICE) as LocationManager - - @Provides - fun notificationManager(application: Application) = application.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - @Provides - @Reusable - fun bluetooth(): BluetoothAdapter = BluetoothAdapter.getDefaultAdapter() - - @Provides - @Reusable - fun bluetoothAccess( - context: Context, - ): BluetoothDeviceAccess { - val bluetooth = BluetoothAdapter.getDefaultAdapter() ?: return DummyBluetoothDeviceAccess - return AndroidBluetoothAccess( - context = context, - bluetoothAdapter = bluetooth, - ) - } -} - -@Module -@InstallIn(SingletonComponent::class) -internal abstract class StaticApplicationModule { - @Binds - abstract fun stringResources(androidStringResources: AndroidStringResources): StringResources - - @Binds - abstract fun integerResources(android: AndroidIntegerResources): IntegerResources - - @Binds - abstract fun locationAccess(locationAccess: AndroidLocationAccess): LocationAccess - - @Binds - abstract fun timeFormat(formatter: AndroidResourceDateTimeFormatter): DateTimeFormatter -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/ExtendedActivity.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/ExtendedActivity.kt deleted file mode 100644 index 4aad8be3..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/ExtendedActivity.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.inkapplications.android.extensions - -import android.os.Bundle -import androidx.annotation.CallSuper -import androidx.appcompat.app.AppCompatActivity -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.cancel - -/** - * An Activity with some generic add-ons. - */ -abstract class ExtendedActivity: AppCompatActivity() { - /** - * Only run while the screen is in the foreground, aka started. - */ - protected lateinit var foregroundScope: CoroutineScope - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - onCreate() - - if (savedInstanceState == null) { - onFirstCreate() - } else { - onRestore(savedInstanceState) - } - } - - @CallSuper - open fun onCreate() {} - - @CallSuper - open fun onRestore(savedInstanceState: Bundle) {} - - @CallSuper - protected open fun onFirstCreate() {} - - override fun onStart() { - super.onStart() - foregroundScope = MainScope() - } - - override fun onStop() { - foregroundScope.cancel() - super.onStop() - } -} \ No newline at end of file diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/IntegerResources.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/IntegerResources.kt deleted file mode 100644 index f9c74b1b..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/IntegerResources.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.inkapplications.android.extensions - -import android.content.res.Resources -import androidx.annotation.IntegerRes -import dagger.Reusable -import javax.inject.Inject - -/** - * Provides access for locating integer configurations in resources. - * - * Using this instead of Android's resources makes testing easier. - */ -interface IntegerResources { - /** - * Get an integer from resources. - */ - fun getInteger(@IntegerRes key: Int): Int -} - -/** - * Use Android's Resource class to fetch integer resources - */ -@Reusable -class AndroidIntegerResources @Inject constructor(private val resources: Resources): IntegerResources { - override fun getInteger(key: Int): Int = resources.getInteger(key) -} - -/** - * Stub implementation of integer resources, used for testing. - */ -object StubIntegerResources: IntegerResources { - override fun getInteger(key: Int): Int = TODO("stub") -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/Notifications.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/Notifications.kt deleted file mode 100644 index 48a1aa3e..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/Notifications.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.inkapplications.android.extensions - -import android.app.Notification -import android.content.Context -import android.os.Build - -/** - * Create a notification builder with a channel ID if the OS supports it. - */ -fun Context.notificationBuilder(channelId: String): Notification.Builder { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - Notification.Builder(this, channelId) - } else { - Notification.Builder(this) - } -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/StringResources.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/StringResources.kt deleted file mode 100644 index ba21f003..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/StringResources.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.inkapplications.android.extensions - -import android.content.res.Resources -import androidx.annotation.StringRes -import dagger.Reusable -import javax.inject.Inject - -/** - * Interface for locating string resources. - * - * Using this instead of Android's resource class makes testing - * considerably easier. - */ -interface StringResources { - fun getString(@StringRes key: Int): String - fun getString(@StringRes key: Int, vararg arguments: Any): String -} - -/** - * Load String resources via Android's default resource locator. - */ -@Reusable -class AndroidStringResources @Inject constructor(private val resources: Resources): StringResources { - override fun getString(key: Int): String = resources.getString(key) - override fun getString(key: Int, vararg arguments: Any) = resources.getString(key, *arguments) -} - -/** - * Stub resources, used for testing. - */ -object StubStringResources: StringResources { - override fun getString(key: Int): String = TODO("Stub") - override fun getString(key: Int, vararg arguments: Any): String = TODO("Stub") -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/ViewStateFactory.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/ViewStateFactory.kt deleted file mode 100644 index 356bdd93..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/ViewStateFactory.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.inkapplications.android.extensions - -/** - * Generic interface for creating a view state type from data. - */ -interface ViewStateFactory { - /** - * Create a new instance of the viewmodel from some data. - */ - fun create(data: DATA): STATE -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/bluetooth/AndroidBluetoothAccess.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/bluetooth/AndroidBluetoothAccess.kt deleted file mode 100644 index f36375d9..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/bluetooth/AndroidBluetoothAccess.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.inkapplications.android.extensions.bluetooth - -import android.Manifest -import android.annotation.SuppressLint -import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothDevice -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.Build -import androidx.annotation.RequiresPermission -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.scan -import java.io.InputStream -import java.io.OutputStream -import java.util.* - -/** - * Provides access to bluetooth devices via event streams. - */ -internal class AndroidBluetoothAccess( - context: Context, - private val bluetoothAdapter: BluetoothAdapter, -): BluetoothDeviceAccess { - /** - * Flow that starts bluetooth discovery and emits discovered devices as they're found. - */ - @SuppressLint("MissingPermission") - @RequiresPermission(allOf = [Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN]) - private val deviceEmitter: Flow = callbackFlow { - val deviceFoundReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val action = intent.action - if (BluetoothDevice.ACTION_FOUND == action) { - val device: BluetoothDevice = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) - ?: return - } else { - intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) as? BluetoothDevice - ?: return - } - trySend( - BluetoothDeviceData( - address = device.address, - alias = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) device.alias else null, - name = device.name.orEmpty(), - majorClass = device.bluetoothClass?.majorDeviceClass, - deviceClass = device.bluetoothClass?.deviceClass, - bondState = device.bondState, - ) - ) - } - } - } - - val filter = IntentFilter(BluetoothDevice.ACTION_FOUND) - context.registerReceiver(deviceFoundReceiver, filter) - bluetoothAdapter.startDiscovery() - awaitClose { context.unregisterReceiver(deviceFoundReceiver) } - } - - /** - * An accumulating list of discovered devices. - * - * Starting this flow will start bluetooth discovery and collect discovered - * devices into a list, de-duplicated by device address. - */ - @RequiresPermission(allOf = [Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN]) - override val devices: Flow> = deviceEmitter.scan(emptyList()) { acc, device -> - if (acc.any { it.address == device.address }) acc else acc + device - } - - /** - * Connect to a bluetooth device. - * - * This starts a connection to the specified bluetooth device and invokes - * the [onConnect] callback with the input and output streams for the connection. - * The [onConnect] method should remain suspended to keep the connection open. - * Once the method returns, the bluetooth socket will be closed. - */ - @RequiresPermission(allOf = [Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN]) - override suspend fun connect( - device: BluetoothDeviceData, - uuid: UUID, - onConnect: suspend (InputStream, OutputStream) -> Unit, - ) { - val bluetoothDevice = bluetoothAdapter.getRemoteDevice(device.address) - bluetoothAdapter.cancelDiscovery() - val socket = bluetoothDevice.createRfcommSocketToServiceRecord(uuid) - socket.connect() - onConnect(socket.inputStream, socket.outputStream) - socket.close() - } -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/bluetooth/BluetoothDeviceAccess.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/bluetooth/BluetoothDeviceAccess.kt deleted file mode 100644 index d6d7f73b..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/bluetooth/BluetoothDeviceAccess.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.inkapplications.android.extensions.bluetooth - -import android.Manifest -import androidx.annotation.RequiresPermission -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import java.io.InputStream -import java.io.OutputStream -import java.util.* - -interface BluetoothDeviceAccess { - @get:RequiresPermission(allOf = [Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN]) - val devices: Flow> - - @RequiresPermission(allOf = [Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN]) - suspend fun connect( - device: BluetoothDeviceData, - uuid: UUID, - onConnect: suspend (InputStream, OutputStream) -> Unit, - ) -} - -/** - * No-op implementation of [BluetoothDeviceAccess]. - */ -object DummyBluetoothDeviceAccess: BluetoothDeviceAccess { - override val devices: Flow> = flowOf() - - override suspend fun connect( - device: BluetoothDeviceData, - uuid: UUID, - onConnect: suspend (InputStream, OutputStream) -> Unit, - ) = Unit -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/bluetooth/BluetoothDeviceData.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/bluetooth/BluetoothDeviceData.kt deleted file mode 100644 index d8bae04d..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/bluetooth/BluetoothDeviceData.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.inkapplications.android.extensions.bluetooth - -/** - * Plain Data about a bluetooth device, de-coupled from Android's active model. - */ -data class BluetoothDeviceData( - val address: String, - val alias: String?, - val name: String, - val majorClass: Int?, - val deviceClass: Int?, - val bondState: Int, -) diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/compose/Modifiers.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/compose/Modifiers.kt deleted file mode 100644 index 1b6d7713..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/compose/Modifiers.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.inkapplications.android.extensions.compose - -import android.os.Build -import android.os.VibrationEffect -import android.os.Vibrator -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalContext -import androidx.core.content.ContextCompat - -fun Modifier.longClickable(action: () -> Unit) = composed { - val vibrator = ContextCompat.getSystemService(LocalContext.current, Vibrator::class.java)!! - pointerInput(Unit) { - detectTapGestures( - onLongPress = { - when { - Build.VERSION.SDK_INT >= 29 -> vibrator.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_HEAVY_CLICK)) - Build.VERSION.SDK_INT >= 26 -> vibrator.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE)) - else -> vibrator.vibrate(50) - } - action() - } - ) - } -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/control/ControlState.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/control/ControlState.kt deleted file mode 100644 index 7c34275f..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/control/ControlState.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.inkapplications.android.extensions.control - -import androidx.compose.runtime.Composable - -enum class ControlState { - On, - Off, - Disabled, - Hidden, -} - -@Composable -inline fun ControlState.whenOn(action: @Composable () -> Unit) { - if (this == ControlState.On) action() -} - -@Composable -inline fun ControlState.whenOff(action: @Composable () -> Unit) { - if (this == ControlState.Off) action() -} - -@Composable -inline fun ControlState.whenDisabled(action: @Composable () -> Unit) { - if (this == ControlState.Disabled) action() -} - -@Composable -inline fun ControlState.whenHidden(action: @Composable () -> Unit) { - if (this == ControlState.Hidden) action() -} - -@Composable -inline fun ControlState.whenVisible(action: @Composable () -> Unit) { - if (this != ControlState.Hidden) action() -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/format/AndroidResourceDateTimeFormatter.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/format/AndroidResourceDateTimeFormatter.kt deleted file mode 100644 index f49f246e..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/format/AndroidResourceDateTimeFormatter.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.inkapplications.android.extensions.format - -import com.inkapplications.android.extensions.R -import com.inkapplications.android.extensions.StringResources -import dagger.Reusable -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toLocalDateTime -import java.text.SimpleDateFormat -import javax.inject.Inject - -/** - * Leverages Android's string resources to create time strings. - */ -@Reusable -class AndroidResourceDateTimeFormatter @Inject constructor( - stringResources: StringResources, - private val timeZone: TimeZone, - private val clock: Clock, -): DateTimeFormatter { - private val timeFormat = SimpleDateFormat(stringResources.getString(R.string.datetime_format_time)).also { - it.timeZone = java.util.TimeZone.getTimeZone(timeZone.id) - } - private val dateTimeFormat = SimpleDateFormat(stringResources.getString(R.string.datetime_format_datetime)).also { - it.timeZone = java.util.TimeZone.getTimeZone(timeZone.id) - } - private val fullTimestampFormat = SimpleDateFormat(stringResources.getString(R.string.datetime_format_timestamp)).also { - it.timeZone = java.util.TimeZone.getTimeZone(timeZone.id) - } - - override fun formatTimestamp(instant: Instant): String { - val now = clock.now().toLocalDateTime(timeZone) - val local = instant.toLocalDateTime(timeZone) - - return when { - local.year == now.year && local.dayOfYear == now.dayOfYear -> timeFormat.format(instant.toEpochMilliseconds()) - local.year == now.year && local.month == now.month -> dateTimeFormat.format(instant.toEpochMilliseconds()) - else -> fullTimestampFormat.format(instant.toEpochMilliseconds()) - } - } -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/format/DateTimeFormatter.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/format/DateTimeFormatter.kt deleted file mode 100644 index 4c81f778..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/format/DateTimeFormatter.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.inkapplications.android.extensions.format - -import kotlinx.datetime.Instant - -/** - * Format date/time strings in the application - */ -interface DateTimeFormatter { - /** - * Format used for items with a timestamp, such as messages or logs. - */ - fun formatTimestamp(instant: Instant): String -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/location/AndroidLocationAccess.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/location/AndroidLocationAccess.kt deleted file mode 100644 index 2c764eaf..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/location/AndroidLocationAccess.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.inkapplications.android.extensions.location - -import android.location.Location -import android.location.LocationListener -import android.location.LocationManager -import dagger.Reusable -import inkapplications.spondee.measure.Length -import inkapplications.spondee.measure.metric.meters -import inkapplications.spondee.spatial.GeoCoordinates -import inkapplications.spondee.spatial.latitude -import inkapplications.spondee.spatial.longitude -import inkapplications.spondee.structure.toFloat -import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.trySendBlocking -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import javax.inject.Inject -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes - -@Reusable -class AndroidLocationAccess @Inject constructor( - private val locationManager: LocationManager, -): LocationAccess { - override val lastKnownLocation: LocationUpdate? - get() { - val lastGps = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) - val lastNetwork = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) - val recent = when { - System.currentTimeMillis() - (lastGps?.time ?: 0) < 30.minutes.inWholeMilliseconds -> lastGps - System.currentTimeMillis() - (lastNetwork?.time ?: 0) < 30.minutes.inWholeMilliseconds -> lastNetwork - else -> lastGps ?: lastNetwork - } - - return recent?.toUpdate() - } - override fun observeLocationChanges(minTime: Duration, minDistance: Length): Flow = callbackFlow { - val callback = object: LocationListener { - override fun onLocationChanged(location: Location) { - trySendBlocking(location.toUpdate()) - } - } - val providerPreferences = listOf( - LocationManager.FUSED_PROVIDER, - LocationManager.GPS_PROVIDER, - LocationManager.NETWORK_PROVIDER, - LocationManager.PASSIVE_PROVIDER, - ) - val provider = providerPreferences.firstOrNull { it in locationManager.getProviders(true) } ?: run { - cancel() - awaitClose() - return@callbackFlow - } - locationManager.requestLocationUpdates( - provider, - minTime.inWholeMilliseconds, - minDistance.toMeters().toFloat(), - callback, - ) - - awaitClose { - locationManager.removeUpdates(callback) - } - } - - private fun Location.toUpdate() = LocationUpdate( - location = GeoCoordinates(latitude.latitude, longitude.longitude), - altitude = altitude.meters, - ) -} diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/location/AndroidLocationUpdate.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/location/AndroidLocationUpdate.kt deleted file mode 100644 index fd755a88..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/location/AndroidLocationUpdate.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.inkapplications.android.extensions.location - -import inkapplications.spondee.measure.Length -import inkapplications.spondee.spatial.GeoCoordinates - -data class LocationUpdate( - val location: GeoCoordinates, - val altitude: Length, -) diff --git a/android-extensions/src/main/java/com/inkapplications/android/extensions/location/LocationAccess.kt b/android-extensions/src/main/java/com/inkapplications/android/extensions/location/LocationAccess.kt deleted file mode 100644 index 884c7826..00000000 --- a/android-extensions/src/main/java/com/inkapplications/android/extensions/location/LocationAccess.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.inkapplications.android.extensions.location - -import inkapplications.spondee.measure.Length -import inkapplications.spondee.measure.us.miles -import kotlinx.coroutines.flow.Flow -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes - -interface LocationAccess { - val lastKnownLocation: LocationUpdate? - - fun observeLocationChanges( - minTime: Duration = 5.minutes, - minDistance: Length = 5.miles, - ): Flow -} diff --git a/android-extensions/src/main/res/values/date_formats.xml b/android-extensions/src/main/res/values/date_formats.xml deleted file mode 100644 index 468dd586..00000000 --- a/android-extensions/src/main/res/values/date_formats.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - yyyy-MM-dd HH:mm - HH:mm - MMM d, HH:mm - diff --git a/android-extensions/src/test/java/com/inkapplications/android/extensions/format/AndroidResourceDateTimeFormatterTest.kt b/android-extensions/src/test/java/com/inkapplications/android/extensions/format/AndroidResourceDateTimeFormatterTest.kt deleted file mode 100644 index 4b0923e9..00000000 --- a/android-extensions/src/test/java/com/inkapplications/android/extensions/format/AndroidResourceDateTimeFormatterTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.inkapplications.android.extensions.format - -import com.inkapplications.android.extensions.StringResources -import com.inkapplications.android.extensions.StubStringResources -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.time.Duration.Companion.days -import kotlin.time.Duration.Companion.minutes - -class AndroidResourceDateTimeFormatterTest { - @Test - fun timestampFormatRecent() { - val stubResource = object: StringResources by StubStringResources { - override fun getString(key: Int): String = "HH:mm" - } - val time = Instant.fromEpochMilliseconds(-22073104000) - val formatter = AndroidResourceDateTimeFormatter(stubResource, TimeZone.UTC, (time + 25.minutes).toFixedClock()) - - val result = formatter.formatTimestamp(time) - - assertEquals("12:34", result) - } - - @Test - fun timestampFormatWithinYear() { - val stubResource = object: StringResources by StubStringResources { - override fun getString(key: Int): String = "MMM d, HH:mm" - } - val time = Instant.fromEpochMilliseconds(-22073104000) - val formatter = AndroidResourceDateTimeFormatter(stubResource, TimeZone.UTC, (time + 25.days).toFixedClock()) - - val result = formatter.formatTimestamp(time) - - assertEquals("Apr 20, 12:34", result) - } - - @Test - fun timestampFormatOld() { - val stubResource = object: StringResources by StubStringResources { - override fun getString(key: Int): String = "yyyy-MM-dd HH:mm" - } - val time = Instant.fromEpochMilliseconds(-22073104000) - val formatter = AndroidResourceDateTimeFormatter(stubResource, TimeZone.UTC, (time + 500.days).toFixedClock()) - - val result = formatter.formatTimestamp(time) - - assertEquals("1969-04-20 12:34", result) - } - - private fun Instant.toFixedClock() = object: Clock { - override fun now(): Instant = this@toFixedClock - } -} diff --git a/aprs-android/build.gradle.kts b/aprs-android/build.gradle.kts deleted file mode 100644 index 9e84e01f..00000000 --- a/aprs-android/build.gradle.kts +++ /dev/null @@ -1,76 +0,0 @@ -plugins { - id("com.android.library") - kotlin("android") - kotlin("kapt") - id("app.cash.sqldelight") -} - -sqldelight { - databases { - create("PacketDatabase") { - packageName.set("com.inkapplications.ack.data") - } - } -} - -android { - namespace = "com.inkapplications.ack.data" - compileSdk = 34 - ndkVersion = "21.3.6528147" - - defaultConfig { - minSdk = 14 - kapt { - arguments { - arg("room.schemaLocation", "$projectDir/src/main/schema") - } - } - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } - sourceSets { - // Adds exported schema location as test app assets. - getByName("androidTest").assets.srcDirs("$projectDir/src/main/schema") - } - - externalNativeBuild { - cmake { - version = "3.31.6" - path = File("${projectDir}/src/main/cpp/CMakeLists.txt") - } - } - packaging { - resources { - excludes += "META-INF/kotlinx-coroutines-core.kotlin_module" - } - } - compileOptions { - targetCompatibility = JavaVersion.VERSION_11 - sourceCompatibility = JavaVersion.VERSION_11 - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - } -} - -dependencies { - api(libs.coroutines.core) - - implementation(projects.androidExtensions) - - api(libs.ack.codec) - api(libs.ack.client) - api(libs.ack.structures) - - api(libs.kimchi.logger) - - implementation(libs.watermelon.coroutines) - implementation(libs.watermelon.standard) - implementation(libs.spondee.units) - - implementation(libs.bundles.sqldelight.android) - implementation(libs.bundles.dagger.libraries) - kapt(libs.bundles.dagger.kapt) - - androidTestImplementation(libs.androidx.test.runner) - androidTestImplementation(libs.androidx.test.core) -} diff --git a/aprs-android/src/main/AndroidManifest.xml b/aprs-android/src/main/AndroidManifest.xml deleted file mode 100644 index a83cf7be..00000000 --- a/aprs-android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/aprs-android/src/main/cpp/CMakeLists.txt b/aprs-android/src/main/cpp/CMakeLists.txt deleted file mode 100644 index a8b079c7..00000000 --- a/aprs-android/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -cmake_minimum_required(VERSION 3.4.1) - -set(SOURCES ${SOURCES} hdlc.c multimon.c demod_afsk12.c) -add_library(multimon SHARED ${SOURCES}) - -# Include libraries needed for kotlin-jni lib -target_link_libraries(multimon android log) diff --git a/aprs-android/src/main/cpp/demod_afsk12.c b/aprs-android/src/main/cpp/demod_afsk12.c deleted file mode 100644 index 925db2bc..00000000 --- a/aprs-android/src/main/cpp/demod_afsk12.c +++ /dev/null @@ -1,126 +0,0 @@ -/* - * demod_afsk12.c -- 1200 baud AFSK demodulator - * - * Copyright (C) 1996 - * Thomas Sailer (sailer@ife.ee.ethz.ch, hb9jnx@hb9w.che.eu) - * - * 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 2 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, write to the Free Software - * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - */ - -/* ---------------------------------------------------------------------- */ - -#include "multimon.h" -#include "filter.h" -#include -#include - -/* ---------------------------------------------------------------------- */ - -/* - * Standard TCM3105 clock frequency: 4.4336MHz - * Mark frequency: 2200 Hz - * Space frequency: 1200 Hz - */ - -#define FREQ_MARK 1200 -#define FREQ_SPACE 2200 -#define FREQ_SAMP 22050 -#define BAUD 1200 -#define SUBSAMP 2 - -/* ---------------------------------------------------------------------- */ - -#define CORRLEN ((int)(FREQ_SAMP/BAUD)) -#define SPHASEINC (0x10000u*BAUD*SUBSAMP/FREQ_SAMP) - -static float corr_mark_i[CORRLEN]; -static float corr_mark_q[CORRLEN]; -static float corr_space_i[CORRLEN]; -static float corr_space_q[CORRLEN]; - -/* ---------------------------------------------------------------------- */ - -static void afsk12_init(struct demod_state *s) -{ - float f; - int i; - - hdlc_init(s); - memset(&s->l1.afsk12, 0, sizeof(s->l1.afsk12)); - for (f = 0, i = 0; i < CORRLEN; i++) { - corr_mark_i[i] = cos(f); - corr_mark_q[i] = sin(f); - f += 2.0*M_PI*FREQ_MARK/FREQ_SAMP; - } - for (f = 0, i = 0; i < CORRLEN; i++) { - corr_space_i[i] = cos(f); - corr_space_q[i] = sin(f); - f += 2.0*M_PI*FREQ_SPACE/FREQ_SAMP; - } -} - -/* ---------------------------------------------------------------------- */ - -static void afsk12_demod(struct demod_state *s, float *buffer, int length) -{ - float f; - unsigned char curbit; - - if (s->l1.afsk12.subsamp) { - int numfill = SUBSAMP - s->l1.afsk12.subsamp; - if (length < numfill) { - s->l1.afsk12.subsamp += length; - return; - } - buffer += numfill; - length -= numfill; - s->l1.afsk12.subsamp = 0; - } - for (; length >= SUBSAMP; length -= SUBSAMP, buffer += SUBSAMP) { - f = fsqr(mac(buffer, corr_mark_i, CORRLEN)) + - fsqr(mac(buffer, corr_mark_q, CORRLEN)) - - fsqr(mac(buffer, corr_space_i, CORRLEN)) - - fsqr(mac(buffer, corr_space_q, CORRLEN)); - s->l1.afsk12.dcd_shreg <<= 1; - s->l1.afsk12.dcd_shreg |= (f > 0); - /* - * check if transition - */ - if ((s->l1.afsk12.dcd_shreg ^ (s->l1.afsk12.dcd_shreg >> 1)) & 1) { - if (s->l1.afsk12.sphase < (0x8000u-(SPHASEINC/2))) - s->l1.afsk12.sphase += SPHASEINC/8; - else - s->l1.afsk12.sphase -= SPHASEINC/8; - } - s->l1.afsk12.sphase += SPHASEINC; - if (s->l1.afsk12.sphase >= 0x10000u) { - s->l1.afsk12.sphase &= 0xffffu; - s->l1.afsk12.lasts <<= 1; - s->l1.afsk12.lasts |= s->l1.afsk12.dcd_shreg & 1; - curbit = (s->l1.afsk12.lasts ^ - (s->l1.afsk12.lasts >> 1) ^ 1) & 1; - hdlc_rxbit(s, curbit); - } - } - s->l1.afsk12.subsamp = length; -} - -/* ---------------------------------------------------------------------- */ - -const struct demod_param demod_afsk1200 = { - "AFSK1200", FREQ_SAMP, CORRLEN, afsk12_init, afsk12_demod -}; - -/* ---------------------------------------------------------------------- */ diff --git a/aprs-android/src/main/cpp/filter.h b/aprs-android/src/main/cpp/filter.h deleted file mode 100644 index 26de7494..00000000 --- a/aprs-android/src/main/cpp/filter.h +++ /dev/null @@ -1,74 +0,0 @@ -/* - * filter.h -- optimized filter routines - * - * Copyright (C) 1996 - * Thomas Sailer (sailer@ife.ee.ethz.ch, hb9jnx@hb9w.che.eu) - * - * 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 2 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, write to the Free Software - * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - */ - -#ifndef _FILTER_H -#define _FILTER_H - -#ifdef ARCH_I386 -#include "filter-i386.h" -#endif /* ARCH_I386 */ - -static inline unsigned int hweight32(unsigned int w) -#ifndef _MSC_VER -__attribute__ ((unused)) -#endif -; -static inline unsigned int hweight16(unsigned short w) -#ifndef _MSC_VER -__attribute__ ((unused)) -#endif -; - -static inline unsigned int hweight8(unsigned char w) -#ifndef _MSC_VER -__attribute__ ((unused)) -#endif -; - -static inline unsigned int gcd(unsigned int x, unsigned int y) -#ifndef _MSC_VER -__attribute__ ((unused)) -#endif -; -static inline unsigned int lcm(unsigned int x, unsigned int y) -#ifndef _MSC_VER -__attribute__ ((unused)) -#endif -; - -#ifndef __HAVE_ARCH_MAC -static inline float mac(const float *a, const float *b, unsigned int size) -{ - float sum = 0; - unsigned int i; - - for (i = 0; i < size; i++) - sum += (*a++) * (*b++); - return sum; -} -#endif /* __HAVE_ARCH_MAC */ - -static inline float fsqr(float f) -{ - return f*f; -} - -#endif /* _FILTER_H */ diff --git a/aprs-android/src/main/cpp/hdlc.c b/aprs-android/src/main/cpp/hdlc.c deleted file mode 100644 index 718163de..00000000 --- a/aprs-android/src/main/cpp/hdlc.c +++ /dev/null @@ -1,125 +0,0 @@ -/* - * hdlc.c -- hdlc decoder and AX.25 packet dump - * - * Copyright (C) 1996 - * Thomas Sailer (sailer@ife.ee.ethz.ch, hb9jnx@hb9w.che.eu) - * - * 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 2 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, write to the Free Software - * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - */ - -#include "multimon.h" -#include -#include - -static const unsigned short crc_ccitt_table[] = { - 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, - 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, - 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, - 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, - 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, - 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, - 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, - 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, - 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, - 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, - 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, - 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, - 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, - 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, - 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, - 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, - 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, - 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, - 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, - 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, - 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, - 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, - 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, - 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, - 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, - 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, - 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, - 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, - 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, - 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, - 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, - 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 -}; - -/* ---------------------------------------------------------------------- */ - -static inline int check_crc_ccitt(const unsigned char *buf, int cnt) -{ - unsigned int crc = 0xffff; - - for (; cnt > 0; cnt--) - crc = (crc >> 8) ^ crc_ccitt_table[(crc ^ *buf++) & 0xff]; - return (crc & 0xffff) == 0xf0b8; -} - -/* ---------------------------------------------------------------------- */ -void send_frame_to_java(unsigned char *bp, unsigned int len); - -static void ax25_disp_packet(struct demod_state *s, unsigned char *bp, unsigned int len) -{ - if (!bp || len < 10) - return; - if (!check_crc_ccitt(bp, len)) - return; - - len -= 2; - send_frame_to_java(bp, len); -} - -void hdlc_init(struct demod_state *s) -{ - memset(&s->l2.hdlc, 0, sizeof(s->l2.hdlc)); -} - -void hdlc_rxbit(struct demod_state *s, int bit) -{ - s->l2.hdlc.rxbitstream <<= 1; - s->l2.hdlc.rxbitstream |= !!bit; - if ((s->l2.hdlc.rxbitstream & 0xff) == 0x7e) { - if (s->l2.hdlc.rxstate && (s->l2.hdlc.rxptr - s->l2.hdlc.rxbuf) > 2) - ax25_disp_packet(s, s->l2.hdlc.rxbuf, s->l2.hdlc.rxptr - s->l2.hdlc.rxbuf); - s->l2.hdlc.rxstate = 1; - s->l2.hdlc.rxptr = s->l2.hdlc.rxbuf; - s->l2.hdlc.rxbitbuf = 0x80; - return; - } - if ((s->l2.hdlc.rxbitstream & 0x7f) == 0x7f) { - s->l2.hdlc.rxstate = 0; - return; - } - if (!s->l2.hdlc.rxstate) - return; - if ((s->l2.hdlc.rxbitstream & 0x3f) == 0x3e) /* stuffed bit */ - return; - if (s->l2.hdlc.rxbitstream & 1) - s->l2.hdlc.rxbitbuf |= 0x100; - if (s->l2.hdlc.rxbitbuf & 1) { - if (s->l2.hdlc.rxptr >= s->l2.hdlc.rxbuf+sizeof(s->l2.hdlc.rxbuf)) { - s->l2.hdlc.rxstate = 0; - __android_log_print(ANDROID_LOG_ERROR,"Multimon","Error: packet size too large"); - return; - } - *s->l2.hdlc.rxptr++ = s->l2.hdlc.rxbitbuf >> 1; - s->l2.hdlc.rxbitbuf = 0x80; - return; - } - s->l2.hdlc.rxbitbuf >>= 1; -} - diff --git a/aprs-android/src/main/cpp/multimon.c b/aprs-android/src/main/cpp/multimon.c deleted file mode 100644 index e0b7b693..00000000 --- a/aprs-android/src/main/cpp/multimon.c +++ /dev/null @@ -1,78 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "multimon.h" - -static const struct demod_param *dem[] = { &demod_afsk1200 }; - -#define NUMDEMOD (sizeof(dem)/sizeof(dem[0])) - -static struct demod_state dem_st[NUMDEMOD]; - -static void process_buffer(float *buf, unsigned int len) -{ - dem[0]->demod(dem_st+0, buf, len); -} - - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) -{ - return JNI_VERSION_1_6; -} - - -void Java_com_inkapplications_ack_data_Multimon_init(JNIEnv *env, jobject object) { - static int sample_rate = -1; - unsigned int i; - unsigned int overlap = 0; - - for (i = 0; i < NUMDEMOD; i++) { - memset(dem_st+i, 0, sizeof(dem_st[i])); - dem_st[i].dem_par = dem[i]; - if (dem[i]->init) - dem[i]->init(dem_st+i); - if (sample_rate == -1) - sample_rate = dem[i]->samplerate; - else if (sample_rate != dem[i]->samplerate) { - exit(3); - } - if (dem[i]->overlap > overlap) - overlap = dem[i]->overlap; - } -} - - -JNIEnv *env_global; -jobject *abp_global; - -void Java_com_inkapplications_ack_data_Multimon_process(JNIEnv *env, jobject object, jfloatArray fbuf, jint length) { - env_global = env; - abp_global = object; - jfloat *jfbuf = (*env)->GetFloatArrayElements(env, fbuf, 0); - process_buffer(jfbuf, length); - (*env)->ReleaseFloatArrayElements(env, fbuf, jfbuf, 0); -} - -void send_frame_to_java(unsigned char *bp, unsigned int len) { - jbyteArray data = (*env_global)->NewByteArray(env_global, len); - if (data == NULL) { - __android_log_print(ANDROID_LOG_ERROR,"Multimon","OOM on allocating data buffer"); - return; - } - (*env_global)->SetByteArrayRegion(env_global, data, 0, len, (jbyte*)bp); - - jclass cls = (*env_global)->GetObjectClass(env_global, abp_global); - jmethodID callback = (*env_global)->GetMethodID(env_global, cls, "callback", "([B)V"); - (*env_global)->CallVoidMethod(env_global, abp_global, callback, data); - -} diff --git a/aprs-android/src/main/cpp/multimon.h b/aprs-android/src/main/cpp/multimon.h deleted file mode 100644 index db6172db..00000000 --- a/aprs-android/src/main/cpp/multimon.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * multimon.h -- Monitor for many different modulation formats - * - * Copyright (C) 1996 - * Thomas Sailer (sailer@ife.ee.ethz.ch, hb9jnx@hb9w.che.eu) - * - * 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 2 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, write to the Free Software - * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - */ - -#ifndef _MULTIMON_H -#define _MULTIMON_H - -#include - -struct demod_state { - const struct demod_param *dem_par; - union { - struct l2_state_hdlc { - unsigned char rxbuf[512]; - unsigned char *rxptr; - unsigned int rxstate; - unsigned int rxbitstream; - unsigned int rxbitbuf; - } hdlc; - - struct l2_state_pocsag { - unsigned long rx_data; - struct l2_pocsag_rx { - unsigned char rx_sync; - unsigned char rx_word; - unsigned char rx_bit; - char func; - unsigned long adr; - unsigned char buffer[128]; - unsigned int numnibbles; - } rx[2]; - } pocsag; - } l2; - union { - struct l1_state_afsk12 { - unsigned int dcd_shreg; - unsigned int sphase; - unsigned int lasts; - unsigned int subsamp; - } afsk12; - } l1; -}; - -struct demod_param { - const char *name; - unsigned int samplerate; - unsigned int overlap; - void (*init)(struct demod_state *s); - void (*demod)(struct demod_state *s, float *buffer, int length); -}; - - -extern const struct demod_param demod_afsk1200; - -void hdlc_init(struct demod_state *s); -void hdlc_rxbit(struct demod_state *s, int bit); - -#endif /* _MULTIMON_H */ diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/AfskModulationConfiguration.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/AfskModulationConfiguration.kt deleted file mode 100644 index ee3f67f4..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/AfskModulationConfiguration.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.inkapplications.ack.data - -import inkapplications.spondee.scalar.Percentage -import inkapplications.spondee.scalar.percent -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds - -data class AfskModulationConfiguration( - val preamble: Duration = 100.milliseconds, - val volume: Percentage = 50.percent, -) diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/AndroidAfskModulator.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/AndroidAfskModulator.kt deleted file mode 100644 index 1f93e14c..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/AndroidAfskModulator.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.inkapplications.ack.data - -/* -The following file is based off of jsoundmodem. -It has been modified extensively, ported to kotlin, and is redistributed under -the GNU General Public License v2.0. -The original source code was obtained from https://github.com/nogy/jsoundmodem. - */ -import android.media.AudioAttributes -import android.media.AudioFormat -import android.media.AudioManager -import android.media.AudioTrack -import android.os.Build -import android.os.Process -import inkapplications.spondee.scalar.Percentage -import inkapplications.spondee.structure.toFloat -import kotlinx.coroutines.* -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine -import kotlin.experimental.or -import kotlin.math.PI -import kotlin.math.cos -import kotlin.math.roundToInt -import kotlin.time.Duration - -private const val FREQ_LOW = 1200 -private const val FREQ_HIGH = 2200 -private const val BPS = 1200 -private const val SAMPLE_RATE = 22050 -private const val PCM_BITS = 16 - -internal class AndroidAfskModulator { - private val trackScope = CoroutineScope(SupervisorJob() + newSingleThreadContext("AudioTrack")) - - fun modulate(data: ByteArray, length: Duration, volume: Percentage) { - val crc = crc16(data) - val (frame, frameLength) = frame(data + crc.first + crc.second, (length.inWholeMilliseconds * BPS/8/1000).toInt()) - val pcmData = ShortArray(frameLength.times(SAMPLE_RATE).div(BPS)) - trackScope.launch { - Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO) - - var lastTone = FREQ_LOW - var cosPos = 0.0 - for (i in 0 until frameLength) { - if (frame[i/8].toInt().and(1.shl(i % 8)) == 0) { - lastTone = if (lastTone == FREQ_LOW) FREQ_HIGH else FREQ_LOW - } - for (t in 0 until SAMPLE_RATE / BPS) { - pcmData[i.times(SAMPLE_RATE / BPS).plus(t)] = cos(cosPos).times(1.shl(PCM_BITS-1) - 1).roundToInt().toShort() - cosPos += 2 * PI * lastTone / SAMPLE_RATE - if (cosPos > 2 * PI) { - cosPos -= 2 * PI - } - } - } - - send(pcmData, volume) - } - } - - private suspend fun send(pcmData: ShortArray, volume: Percentage) { - val track = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - AudioTrack( - AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build(), - AudioFormat.Builder().setEncoding(AudioFormat.ENCODING_PCM_16BIT).setChannelMask(AudioFormat.CHANNEL_OUT_MONO).setSampleRate(SAMPLE_RATE).build(), - pcmData.size * 2, - AudioTrack.MODE_STATIC, - AudioManager.AUDIO_SESSION_ID_GENERATE, - ) - } else AudioTrack( - AudioManager.STREAM_RING, - SAMPLE_RATE, - AudioFormat.CHANNEL_OUT_MONO, - AudioFormat.ENCODING_PCM_16BIT, - pcmData.size * 2, - AudioTrack.MODE_STATIC, - ) - - suspendCoroutine { continuation -> - track.setPlaybackPositionUpdateListener(object: AudioTrack.OnPlaybackPositionUpdateListener { - override fun onMarkerReached(track: AudioTrack) { - track.release() - continuation.resume(Unit) - } - override fun onPeriodicNotification(track: AudioTrack?) = Unit - }) - track.notificationMarkerPosition = pcmData.size - val volume = AudioTrack.getMaxVolume() * volume.toDecimal().toFloat() - track.setStereoVolume(volume, volume) - track.write(pcmData, 0, pcmData.size) - runBlocking { - // Without this delay the audiotrack will automatically pause shortly after playing. I hate this, but cannot figure out an alternative. - delay(500) - } - track.play() - } - } - - private fun crc16(data: ByteArray): Pair { - return data.fold(0xFFFF) { crc, byte -> - (0 until 8).fold(crc) { bitCrc, position -> - if (bitCrc.and(1) != byte.toInt().and(1.shl(position)).shr(position)) { - bitCrc.shr(1).xor(0x8408).and(0xFFFF) - } else { - bitCrc.shr(1) - } - } - } - .xor(0xFFFF).let { - Pair( - it.and(0xFF).minus(256).toByte(), - it.and(0xFF00).shr(8).minus(256).toByte(), - ) - } - } - - private fun frame(data: ByteArray, length: Int): Pair { - val out = ByteArray(data.size + length + Math.ceil(data.size / 5.0).toInt() + 1) - for (i in 0 until length ) { - out[i] = 0x7e - } - - var ones = 0 - var k = length*8 - for (i in 0 until data.size*8) { - if (data[i / 8].toInt() and (1 shl i % 8) > 0) { - out[k / 8] = out[k / 8] or if (1 shl k % 8 > 127) ((1 shl k % 8) - 256).toByte() else (1 shl k % 8).toByte() - if (ones++ == 4) { - k++ - ones = 0 - } - } else { - ones = 0 - } - k++ - } - - for (i in 0 until 8) { - if (0x7e and (1 shl i % 8) > 0) { - out[k / 8] = out[k / 8] or if (1 shl k % 8 > 127) ((1 shl k % 8) - 256).toByte() else (1 shl k % 8).toByte() - } - k++ - } - - return out to k+1 - } -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/AndroidAprsModule.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/AndroidAprsModule.kt deleted file mode 100644 index 47735154..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/AndroidAprsModule.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.inkapplications.ack.data - -import android.content.Context -import app.cash.sqldelight.driver.android.AndroidSqliteDriver -import com.inkapplications.ack.client.AprsClientModule -import com.inkapplications.ack.codec.Ack -import com.inkapplications.ack.codec.AprsCodec -import com.inkapplications.ack.data.adapters.CallsignAdapter -import com.inkapplications.ack.data.adapters.CaptureIdColumnAdapter -import com.inkapplications.ack.data.adapters.InstantAdapter -import com.inkapplications.ack.data.adapters.PacketOriginAdapter -import com.inkapplications.ack.data.drivers.* -import com.inkapplications.android.extensions.bluetooth.BluetoothDeviceAccess -import dagger.Module -import dagger.Provides -import dagger.Reusable -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.datetime.Clock -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object AndroidAprsModule { - @Provides - @Reusable - fun aprsCodec( - logger: KimchiLogger, - ): AprsCodec { - val logAdapter = object: KimchiLogger by logger { - override fun debug(message: String, cause: Throwable?) = Unit - override fun debug(cause: Throwable?, message: () -> String) = Unit - override fun trace(message: String, cause: Throwable?) = Unit - override fun trace(cause: Throwable?, message: () -> String) = Unit - } - return Ack(logAdapter).defaultParser() - } - - @Provides - @Reusable - fun packetStorage( - context: Context, - codec: AprsCodec, - clock: Clock, - logger: KimchiLogger, - ): PacketStorage { - val driver = AndroidSqliteDriver(PacketDatabase.Schema, context, "packets.db") - val database = PacketDatabase( - driver = driver, - CapturedPacketEntity.Adapter( - idAdapter = CaptureIdColumnAdapter, - timestampAdapter = InstantAdapter, - packet_originAdapter = PacketOriginAdapter, - source_callsignAdapter = CallsignAdapter, - addressee_callsignAdapter = CallsignAdapter, - ), - ) - return DaoPacketStorage(database.capturedPacketEntityQueries, codec, clock, logger) - } - - @Provides - @Singleton - fun drivers( - codec: AprsCodec, - logger: KimchiLogger, - androidLocationProvider: AndroidLocationProvider, - driverSettings: DriverSettingsProvider, - packetStorage: PacketStorage, - bluetoothAccess: BluetoothDeviceAccess, - ): PacketDrivers { - val audioCapture = AudioDataCapture(logger) - val audioProcessor = AudioDataProcessor(audioCapture) - val backgroundScope = CoroutineScope(Dispatchers.Default) - - val internet = InternetDriver( - aprsCodec = codec, - packetStorage = packetStorage, - client = AprsClientModule.createDataClient(), - locationProvider = androidLocationProvider, - settings = driverSettings, - runScope = backgroundScope, - logger = logger, - ) - val afsk = AfskDriver( - aprsCodec = codec, - packetStorage = packetStorage, - audioProcessor = audioProcessor, - modulator = AndroidAfskModulator(), - settings = driverSettings, - runScope = backgroundScope, - logger = logger, - ) - - val tncDriver = TncDriver( - bluetoothAccess = bluetoothAccess, - packetStorage = packetStorage, - ack = codec, - runScope = backgroundScope, - logger = logger, - ) - - return PacketDrivers( - internetDriver = internet, - afskDriver = afsk, - tncDriver = tncDriver, - ) - } -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/AndroidLocationProvider.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/AndroidLocationProvider.kt deleted file mode 100644 index 4e456c10..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/AndroidLocationProvider.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.inkapplications.ack.data - -import android.Manifest -import android.location.Location -import android.location.LocationListener -import android.location.LocationManager -import android.os.Bundle -import androidx.annotation.RequiresPermission -import dagger.Reusable -import inkapplications.spondee.measure.metric.meters -import inkapplications.spondee.spatial.GeoCoordinates -import inkapplications.spondee.spatial.latitude -import inkapplications.spondee.spatial.longitude -import inkapplications.spondee.structure.Kilo -import inkapplications.spondee.structure.scale -import inkapplications.spondee.structure.toFloat -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.trySendBlocking -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext -import javax.inject.Inject -import kotlin.time.Duration.Companion.minutes - -@Reusable -class AndroidLocationProvider @Inject constructor( - private val locationManager: LocationManager, - private val logger: KimchiLogger, -) { - private val MIN_TIME = 5.minutes.inWholeMilliseconds - private val MIN_DISTANCE = 1000.scale(Kilo).meters.toFloat() - - @RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) - private val androidLocationFlow: Flow = callbackFlow { - val listener = object : LocationListener { - override fun onLocationChanged(location: Location) { - logger.debug("Location Changed: $location") - trySendBlocking(location) - } - - override fun onStatusChanged(p0: String?, p1: Int, p2: Bundle?) { - logger.debug("Location Status Changed: $p0 $p1") - } - - override fun onProviderEnabled(p0: String) { - logger.debug("Location Provider Enabled: $p0") - } - - override fun onProviderDisabled(p0: String) { - logger.debug("Location Provider Disabled: $p0") - } - } - - withContext(Dispatchers.Main) { - locationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER) - ?.run { send(this) } - - locationManager.requestLocationUpdates( - LocationManager.GPS_PROVIDER, - MIN_TIME, - MIN_DISTANCE, - listener, - ) - } - awaitClose { - locationManager.removeUpdates(listener) - } - } - - @RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) - val location: Flow = androidLocationFlow - .distinctUntilChanged { a, b -> - a.distanceTo(b) < MIN_DISTANCE - } - .map { location -> - GeoCoordinates(location.latitude.latitude, location.longitude.longitude) - } -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/AudioDataCapture.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/AudioDataCapture.kt deleted file mode 100644 index 9c2d84aa..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/AudioDataCapture.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.inkapplications.ack.data - -import android.media.AudioFormat -import android.media.AudioRecord -import android.media.MediaRecorder -import android.os.Process -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.ReceiveChannel - -private const val BUFFER_SIZE = 16 -private const val SAMPLE_RATE = 22050 -private const val AUDIO_BUFFER_SIZE = 16384 -private const val PROCESS_BUFFER_SIZE = 8192 - -/** - * Capture buffered audio data on a separate prioritized thread. - * - * Data from this capture can be processed by consuming the [audioData] channel. - */ -internal class AudioDataCapture( - private val logger: KimchiLogger -) { - private val recorder: AudioRecord by lazy { - AudioRecord( - MediaRecorder.AudioSource.MIC, - SAMPLE_RATE, - AudioFormat.CHANNEL_IN_MONO, - AudioFormat.ENCODING_PCM_16BIT, - AUDIO_BUFFER_SIZE - ) - } - private val captureScope = CoroutineScope(SupervisorJob() + newSingleThreadContext("AudioCapture")) - private val buffers = Array(BUFFER_SIZE) { ShortArray(PROCESS_BUFFER_SIZE) } - private val audioChannel = BroadcastChannel(BUFFER_SIZE) - - /** - * Outputs buffered arrays of audio data. - * - * Note: The arrays from this channel are recycled and should not be stored - * directly. This channel has a limited capacity of [BUFFER_SIZE] arrays. - */ - val audioData: ReceiveChannel get() = audioChannel.openSubscription() - - /** - * Start capturing audio data. - * - * This will suspend until [cancel] is called. - */ - fun capture() { - logger.debug("Audio capture started") - captureScope.launch { - Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO) - recorder.startRecording() - - var bufferIndex = 0 - while (recorder.recordingState != AudioRecord.RECORDSTATE_STOPPED) { - val buffer = buffers[bufferIndex] - recorder.read(buffer, 0, buffer.size) - bufferIndex = if (bufferIndex == buffers.size - 1) 0 else bufferIndex + 1 - audioChannel.trySend(buffer) - } - } - } - - /** - * Stop capturing audio data. - */ - fun cancel() { - logger.debug("Audio capture stopped") - recorder.stop() - } -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/AudioDataProcessor.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/AudioDataProcessor.kt deleted file mode 100644 index 3415446e..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/AudioDataProcessor.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.inkapplications.ack.data - -import inkapplications.spondee.scalar.Percentage -import inkapplications.spondee.scalar.decimalPercentage -import kotlinx.coroutines.channels.consumeEach -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.map -import kotlin.math.abs -import kotlin.math.absoluteValue - -private const val overlap = 18 - -internal class AudioDataProcessor( - private val audioIn: AudioDataCapture -) { - private val multimon by lazy { - Multimon.apply { - init() - } - } - - private val fbuf = FloatArray(16384) - private var fbuf_cnt = 0 - - val data = callbackFlow { - multimon.onReceive = { trySend(it) } - - audioIn.capture() - - invokeOnClose { - audioIn.cancel() - peak.value = null - } - audioIn.audioData.consumeEach { decode(it) } - } - - private val peak = MutableStateFlow(null) - val volume: Flow = peak.map { - it?.toDouble()?.absoluteValue?.div(Short.MAX_VALUE)?.decimalPercentage - } - - private fun decode(audioData: ShortArray) { - peak.value = audioData.maxOf { abs(it.toInt()) }.toShort() - for (i in audioData.indices) { - fbuf[fbuf_cnt++] = audioData[i] * (1.0f / 32768.0f) - } - - if (fbuf_cnt > overlap) { - multimon.process(fbuf, fbuf_cnt - overlap) - System.arraycopy(fbuf, fbuf_cnt - overlap, fbuf, 0, overlap) - fbuf_cnt = overlap - } - } -} - diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/CaptureId.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/CaptureId.kt deleted file mode 100644 index 878359c7..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/CaptureId.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.inkapplications.ack.data - -@JvmInline -value class CaptureId(val value: Long) diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/CapturedPacket.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/CapturedPacket.kt deleted file mode 100644 index e8fe983c..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/CapturedPacket.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.inkapplications.ack.data - -import com.inkapplications.ack.structures.AprsPacket -import kotlinx.datetime.Instant - -data class CapturedPacket( - val id: CaptureId, - val received: Instant, - val parsed: AprsPacket, - val origin: PacketOrigin, - val raw: ByteArray, -) diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/ConnectionConfiguration.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/ConnectionConfiguration.kt deleted file mode 100644 index 6a2d86ba..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/ConnectionConfiguration.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.inkapplications.ack.data - -import com.inkapplications.ack.client.Credentials -import com.inkapplications.ack.structures.station.StationAddress -import inkapplications.spondee.measure.Length -import inkapplications.spondee.measure.us.miles - -const val DEFAULT_CONNECTION_SERVER = "rotate.aprs2.net" -const val DEFAULT_CONNECTION_PORT = 14580 -const val DEFAULT_SEARCH_RADIUS_MILES = 100 - -data class ConnectionConfiguration( - val address: StationAddress, - val passcode: Int = -1, - val host: String = DEFAULT_CONNECTION_SERVER, - val port: Int = DEFAULT_CONNECTION_PORT, - val searchRadius: Length = DEFAULT_SEARCH_RADIUS_MILES.miles, -) { - val credentials = Credentials(address.toString(), passcode) -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/DaoPacketStorage.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/DaoPacketStorage.kt deleted file mode 100644 index 17dc71b8..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/DaoPacketStorage.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.inkapplications.ack.data - -import app.cash.sqldelight.Query -import app.cash.sqldelight.coroutines.asFlow -import app.cash.sqldelight.coroutines.mapToList -import app.cash.sqldelight.coroutines.mapToOne -import app.cash.sqldelight.coroutines.mapToOneOrNull -import com.inkapplications.ack.codec.AprsCodec -import com.inkapplications.ack.structures.AprsPacket -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.station.Callsign -import com.inkapplications.coroutines.filterItemNotNull -import com.inkapplications.coroutines.mapItems -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.datetime.Clock -import kotlin.reflect.KClass - -internal class DaoPacketStorage( - private val packetDao: CapturedPacketEntityQueries, - private val codec: AprsCodec, - private val clock: Clock, - private val logger: KimchiLogger, -): PacketStorage { - private fun Query.toListFlow() = asFlow().mapToList(Dispatchers.IO) - private fun Query.toSingleFlow() = asFlow().mapToOne(Dispatchers.IO) - private fun Query.toMaybeFlow() = asFlow().mapToOneOrNull(Dispatchers.IO) - - override fun findRecent(count: Long): Flow> { - return packetDao.findRecent(count) - .toListFlow() - .map { entities -> - entities.mapNotNull { createCapturedPacket(it, fromEntityOrNull(it)) } - } - } - - override fun findById(id: CaptureId): Flow { - return packetDao.findById(id) - .toMaybeFlow() - .map { it?.let { createCapturedPacket(it, fromEntityOrNull(it)) } } - } - - override fun findLatestByConversation(callsign: Callsign): Flow> { - return packetDao.findLatestConversationMessages(callsign) - .toListFlow() - .mapItems { createCapturedPacket(it, fromEntityOrNull(it)) } - .filterItemNotNull() - } - - override fun findConversation(addressee: Callsign, callsign: Callsign): Flow> { - return packetDao.findConversation(addressee, callsign) - .toListFlow() - .mapItems { createCapturedPacket(it, fromEntityOrNull(it)) } - .filterItemNotNull() - } - - override fun count(): Flow { - return packetDao.countAll().toSingleFlow() - } - - override fun countStations(): Flow { - return packetDao.countSources().toSingleFlow() - } - - override fun findByStationComments(limit: Long?): Flow> { - return packetDao.findStationComments(limit ?: -1L) - .toListFlow() - .mapItems { createCapturedPacket(it, fromEntityOrNull(it)) } - .filterItemNotNull() - } - - override fun findMostRecentByType(type: KClass): Flow { - return packetDao.findMostRecentByType(type.simpleName!!) - .toMaybeFlow() - .map { if (it == null) null else createCapturedPacket(it, fromEntityOrNull(it)) } - } - - override fun findBySource(callsign: Callsign, limit: Long?): Flow> { - return packetDao.findBySourceCallsign(callsign, limit ?: -1) - .toListFlow() - .mapItems { createCapturedPacket(it, fromEntityOrNull(it)) } - .filterItemNotNull() - } - - override suspend fun save(data: ByteArray, packet: AprsPacket, origin: PacketOrigin): CapturedPacket { - val now = clock.now() - return packetDao.transactionWithResult { - packetDao.addPacket( - timestamp = now, - raw_data = data, - packet_origin = origin, - source_callsign = packet.route.source.callsign, - addressee_callsign = (packet.data as? PacketData.Message)?.addressee?.callsign, - data_type = packet.data.javaClass.simpleName, - comment_field = packet.data.commentLikeField, - ) - - packetDao.lastId() - }.executeAsOne().let { - CapturedPacket( - id = CaptureId(it), - received = now, - parsed = packet, - origin = origin, - raw = data, - ) - } - } - - private fun fromEntityOrNull(data: CapturedPacketEntity): AprsPacket? { - try { - return when (data.packet_origin) { - PacketOrigin.Ax25, PacketOrigin.Tnc -> codec.fromAx25(data.raw_data) - PacketOrigin.AprsIs, PacketOrigin.Local -> codec.fromString(data.raw_data.toString(Charsets.UTF_8)) - } - } catch (error: Throwable) { - logger.warn("Unable to parse packet", error) - return null - } - } - - private fun createCapturedPacket(entity: CapturedPacketEntity, parsed: AprsPacket?): CapturedPacket? { - parsed ?: return null - - return CapturedPacket( - id = entity.id, - received = entity.timestamp, - parsed = parsed, - origin = entity.packet_origin, - raw = entity.raw_data, - ) - } -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/Multimon.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/Multimon.kt deleted file mode 100644 index 17341eab..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/Multimon.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.inkapplications.ack.data - -/** - * API for interfacing with the JNI Multimon code. - */ -internal object Multimon { - /** - * Invoked when data is parsed from processing. - */ - var onReceive: (ByteArray) -> Unit = {} - - init { - System.loadLibrary("multimon") - } - - /** - * Initialize the Multimon library. - */ - external fun init() - - /** - * Process an array of audio data. - */ - external fun process(buffer: FloatArray, length: Int) - - private fun callback(data: ByteArray) { - onReceive(data) - } -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/PacketOrigin.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/PacketOrigin.kt deleted file mode 100644 index f265cd97..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/PacketOrigin.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.inkapplications.ack.data - -enum class PacketOrigin { - Ax25, - AprsIs, - Tnc, - Local, -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/PacketStorage.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/PacketStorage.kt deleted file mode 100644 index cd768e7d..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/PacketStorage.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.inkapplications.ack.data - -import com.inkapplications.ack.structures.AprsPacket -import com.inkapplications.ack.structures.PacketData -import com.inkapplications.ack.structures.station.Callsign -import kotlinx.coroutines.flow.Flow -import kotlin.reflect.KClass - -/** - * Provides query access to packets that have been captured and stored. - */ -interface PacketStorage { - /** - * Find a limited number of packets by recency. - * - * @param count The max limit of packets to find. - */ - fun findRecent(count: Long): Flow> - - /** - * Find the latest packet in every conversation. - * - * @param callsign The callsign to find conversations with. - */ - fun findLatestByConversation(callsign: Callsign): Flow> - - /** - * Find all packets in a specific conversation. - * - * @param addressee The external callsign to look up conversations with. - * @param callsign The user's callsign to look up conversations with. - */ - fun findConversation(addressee: Callsign, callsign: Callsign): Flow> - - /** - * Find a specific packet by its id - * - * @param id The locally generated ID to find a packet by - */ - fun findById(id: CaptureId): Flow - - /** - * Find the quantity of packets saved. - */ - fun count(): Flow - - /** - * Find the number of distinct stations in the packets received. - */ - fun countStations(): Flow - - /** - * Find packets with comments that appear to be broadcasting a station frequency. - */ - fun findByStationComments(limit: Long? = null): Flow> - - /** - * Find the most recent packet received of a specific data type. - * - * @param type The data type of the packet to find. - */ - fun findMostRecentByType(type: KClass): Flow - - /** - * Find all of the packets received from a callsign. - * - * @param callsign The source/origin callsign to find packets of. - */ - fun findBySource(callsign: Callsign, limit: Long? = null): Flow> - - /** - * Store a packet locally. - * - * @param data The raw data of the packet received. - * @param packet The parsed version of the packet data, - * @param origin The origin of the packet data. - */ - suspend fun save(data: ByteArray, packet: AprsPacket, origin: PacketOrigin): CapturedPacket -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/Packets.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/Packets.kt deleted file mode 100644 index 6eb78e52..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/Packets.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.inkapplications.ack.data - -import com.inkapplications.ack.structures.PacketData - -/** - * Extract any comment-like field from a packet, if available. - */ -val PacketData.commentLikeField: String? get() = when(this) { - is PacketData.ItemReport -> comment - is PacketData.Message -> message - is PacketData.ObjectReport -> comment - is PacketData.Position -> comment - is PacketData.StatusReport -> status - is PacketData.TelemetryReport -> comment - is PacketData.CapabilityReport, - is PacketData.Unknown, - is PacketData.Weather -> null -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/adapters/CallsignAdapter.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/adapters/CallsignAdapter.kt deleted file mode 100644 index b0a5cb43..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/adapters/CallsignAdapter.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.inkapplications.ack.data.adapters - -import app.cash.sqldelight.ColumnAdapter -import com.inkapplications.ack.structures.station.Callsign - -/** - * SQLDelight ColumnAdapter for storing a Callsign as its canonical string. - */ -internal object CallsignAdapter: ColumnAdapter { - override fun decode(databaseValue: String): Callsign = Callsign(databaseValue) - override fun encode(value: Callsign): String = value.canonical.uppercase() -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/adapters/CaptureIdColumnAdapter.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/adapters/CaptureIdColumnAdapter.kt deleted file mode 100644 index 78c07699..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/adapters/CaptureIdColumnAdapter.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.inkapplications.ack.data.adapters - -import app.cash.sqldelight.ColumnAdapter -import com.inkapplications.ack.data.CaptureId - -/** - * SQLDelight ColumnAdapter for storing a CaptureId as a long. - */ -internal object CaptureIdColumnAdapter: ColumnAdapter { - override fun decode(databaseValue: Long): CaptureId = CaptureId(databaseValue) - override fun encode(value: CaptureId): Long = value.value -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/adapters/InstantAdapter.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/adapters/InstantAdapter.kt deleted file mode 100644 index 15333a58..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/adapters/InstantAdapter.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.inkapplications.ack.data.adapters - -import app.cash.sqldelight.ColumnAdapter -import kotlinx.datetime.Instant - -/** - * SQLDelight ColumnAdapter for storing an Instant as a long in epoch milliseconds. - */ -internal object InstantAdapter: ColumnAdapter { - override fun decode(databaseValue: Long): Instant = Instant.fromEpochMilliseconds(databaseValue) - override fun encode(value: Instant): Long = value.toEpochMilliseconds() -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/adapters/PacketOriginAdapter.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/adapters/PacketOriginAdapter.kt deleted file mode 100644 index 75f184eb..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/adapters/PacketOriginAdapter.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.inkapplications.ack.data.adapters - -import app.cash.sqldelight.ColumnAdapter -import com.inkapplications.ack.data.PacketOrigin - -/** - * SQLDelight ColumnAdapter for storing a PacketSource as its string name. - */ -internal object PacketOriginAdapter: ColumnAdapter { - override fun decode(databaseValue: String): PacketOrigin = PacketOrigin.valueOf(databaseValue) - override fun encode(value: PacketOrigin): String = value.name -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/AfskDriver.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/AfskDriver.kt deleted file mode 100644 index cba97789..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/AfskDriver.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.inkapplications.ack.data.drivers - -import android.Manifest -import android.os.Build -import com.inkapplications.ack.codec.AprsCodec -import com.inkapplications.ack.data.* -import com.inkapplications.ack.structures.AprsPacket -import com.inkapplications.ack.structures.EncodingConfig -import com.inkapplications.coroutines.combinePair -import inkapplications.spondee.scalar.toWholePercentage -import inkapplications.spondee.structure.roundToInt -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch - -class AfskDriver internal constructor( - private val aprsCodec: AprsCodec, - private val packetStorage: PacketStorage, - private val audioProcessor: AudioDataProcessor, - private val modulator: AndroidAfskModulator, - private val settings: DriverSettingsProvider, - private val runScope: CoroutineScope, - private val logger: KimchiLogger, -): PacketDriver, AudioConnectionMonitor { - private val runJob = MutableStateFlow(null) - override val connectionState: Flow = runJob.map { job -> - when { - job?.isActive == true -> DriverConnectionState.Connected - else -> DriverConnectionState.Disconnected - } - } - override val incoming = MutableSharedFlow() - override val receivePermissions: Set = when { - Build.VERSION.SDK_INT >= 34 -> setOf( - Manifest.permission.RECORD_AUDIO, - Manifest.permission.FOREGROUND_SERVICE_MICROPHONE, - // TODO: This could be simplified/removed if we create separate services for transmit and receive. - // Leaving for now, since all the other drivers require it anyway. - Manifest.permission.FOREGROUND_SERVICE_LOCATION, - Manifest.permission.POST_NOTIFICATIONS, - ) - Build.VERSION.SDK_INT >= 33 -> setOf( - Manifest.permission.RECORD_AUDIO, - Manifest.permission.POST_NOTIFICATIONS, - ) - else -> setOf(Manifest.permission.RECORD_AUDIO) - } - override val transmitPermissions: Set = when { - Build.VERSION.SDK_INT >= 33 -> setOf( - Manifest.permission.POST_NOTIFICATIONS - ) - else -> emptySet() - } - override val volume = audioProcessor.volume - private val transmitQueue = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - - override suspend fun transmitPacket(packet: AprsPacket, encodingConfig: EncodingConfig) { - val data = try { - aprsCodec.toAx25(packet, encodingConfig) - } catch (encodingError: Throwable) { - logger.error("Unable to encode packet for AX25", encodingError) - return - } - - transmitQueue.emit(data) - } - - override suspend fun connect() { - logger.debug("Connecting Audio Packet Capture") - - runJob.updateAndGet { priorJob -> - priorJob?.takeIf { it.isActive } ?: launchNewJob() - } - } - - private fun launchNewJob(): Job { - logger.debug("Launching new job for Audio Packet Capture") - return runScope.launch { - launch { - audioProcessor.data - .mapNotNull { captureAx25Packet(it) } - .onEach { logger.debug("APRS Packet Parsed: $it") } - .collect { incoming.emit(it) } - } - launch { - settings.afskConfiguration.combinePair(transmitQueue) - .collect { (config, data) -> - logger.info("Modulating ${data.size} bytes with a ${config.preamble} preamble at ${config.volume.toWholePercentage().roundToInt()}% volume") - modulator.modulate(data, config.preamble, config.volume) - } - } - } - } - - override suspend fun disconnect() { - logger.debug("Disconnecting Audio Packet Capture") - runJob.value?.cancelAndJoin() - runJob.value = null - logger.debug("Audio Packet Capture Disconnected") - } - - private suspend fun captureAx25Packet(data: ByteArray): CapturedPacket? { - val parsed = try { - aprsCodec.fromAx25(data) - } catch (parsingError: Throwable) { - logger.warn("Unable to parse packet", parsingError) - return null - } - return packetStorage.save(data, parsed, PacketOrigin.Ax25) - } -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/AudioConnectionMonitor.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/AudioConnectionMonitor.kt deleted file mode 100644 index a9f43085..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/AudioConnectionMonitor.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.inkapplications.ack.data.drivers - -import inkapplications.spondee.scalar.Percentage -import kotlinx.coroutines.flow.Flow - -interface AudioConnectionMonitor { - val volume: Flow -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/ConnectTncData.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/ConnectTncData.kt deleted file mode 100644 index 7dccb687..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/ConnectTncData.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.inkapplications.ack.data.drivers - -import com.inkapplications.android.extensions.bluetooth.BluetoothDeviceData - -/** - * TNC Connection information. - * - * This represents the current state of the TNC connection driver. - */ -data class ConnectTncData( - /** - * List of bluetooth devices discovered that may be TNC devices. - */ - var discoveredDevices: List, - - /** - * Device that is currently being connected to. - */ - var connectingDevice: BluetoothDeviceData?, - - /** - * Device that is currently connected to. - */ - var connectedDevice: BluetoothDeviceData?, -) diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/DriverConnectionState.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/DriverConnectionState.kt deleted file mode 100644 index 71f563ba..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/DriverConnectionState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.inkapplications.ack.data.drivers - -enum class DriverConnectionState { - Connecting, - Connected, - Disconnecting, - Disconnected, -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/DriverSettingsProvider.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/DriverSettingsProvider.kt deleted file mode 100644 index 11bdd8ee..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/DriverSettingsProvider.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.inkapplications.ack.data.drivers - -import com.inkapplications.ack.data.AfskModulationConfiguration -import com.inkapplications.ack.data.ConnectionConfiguration -import kotlinx.coroutines.flow.Flow - -interface DriverSettingsProvider { - val internetServiceConfiguration: Flow - val afskConfiguration: Flow -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/InternetDriver.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/InternetDriver.kt deleted file mode 100644 index 579a6857..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/InternetDriver.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.inkapplications.ack.data.drivers - -import android.Manifest -import android.os.Build -import com.inkapplications.ack.client.AprsDataClient -import com.inkapplications.ack.codec.AprsCodec -import com.inkapplications.ack.data.* -import com.inkapplications.ack.structures.AprsPacket -import com.inkapplications.ack.structures.EncodingConfig -import com.inkapplications.coroutines.combinePair -import inkapplications.spondee.spatial.GeoCoordinates -import inkapplications.spondee.structure.Kilo -import inkapplications.spondee.structure.value -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.consumeEach -import kotlinx.coroutines.flow.* -import kotlin.math.pow - -class InternetDriver internal constructor( - private val aprsCodec: AprsCodec, - private val packetStorage: PacketStorage, - private val client: AprsDataClient, - private val locationProvider: AndroidLocationProvider, - private val settings: DriverSettingsProvider, - private val runScope: CoroutineScope, - private val logger: KimchiLogger, -): PacketDriver { - private val runJob = MutableStateFlow(null) - private val clientConnectionState = MutableStateFlow(DriverConnectionState.Disconnected) - override val connectionState: Flow = clientConnectionState - override val incoming = MutableSharedFlow() - override val receivePermissions: Set = when { - Build.VERSION.SDK_INT >= 34 -> setOf( - Manifest.permission.POST_NOTIFICATIONS, - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.FOREGROUND_SERVICE_LOCATION, - ) - Build.VERSION.SDK_INT >= 33 -> setOf( - Manifest.permission.POST_NOTIFICATIONS, - Manifest.permission.ACCESS_FINE_LOCATION, - ) - else -> setOf(Manifest.permission.ACCESS_FINE_LOCATION) - } - override val transmitPermissions: Set = when { - Build.VERSION.SDK_INT >= 33 -> setOf( - Manifest.permission.POST_NOTIFICATIONS, - ) - else -> emptySet() - } - private val transmitQueue = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - - override suspend fun transmitPacket(packet: AprsPacket, encodingConfig: EncodingConfig) { - val encoded = try { - aprsCodec.toString(packet, encodingConfig) - } catch (encodingError: Throwable) { - logger.error("Unable to encode packet to String", encodingError) - return - } - - transmitQueue.emit(encoded) - } - - override suspend fun connect() { - logger.debug("Connecting Internet Packet Capture") - - runJob.updateAndGet { priorJob -> - priorJob?.takeIf { it.isActive } ?: launchNewJob() - } - } - - override suspend fun disconnect() { - clientConnectionState.value = DriverConnectionState.Disconnecting - runJob.value?.cancelAndJoin() - } - - private fun launchNewJob(): Job { - logger.debug("Launching new job for Internet Packet Capture") - return runScope.launch { - settings.internetServiceConfiguration - .combinePair(locationProvider.location) - .onEach { logger.debug("New Settings/Location pair: $it") } - .flatMapLatest { (settings, location) -> connectWithRetry(settings, location) } - .onEach { if (it.startsWith('#')) logger.info("APRS-IS: $it") } - .filter { !it.startsWith('#') } - .mapNotNull { captureStringPacket(it) } - .onEach { logger.debug("IS Packet Parsed: $it") } - .flowOn(Dispatchers.IO) - .collect { incoming.emit(it) } - } - } - - private fun connectWithRetry(settings: ConnectionConfiguration, location: GeoCoordinates): Flow { - return callbackFlow { - logger.info("Opening APRS-IS Client to ${settings.host}:${settings.port}") - var attempts = 0 - while (currentCoroutineContext().isActive) { - clientConnectionState.value = DriverConnectionState.Connecting - try { - client.connect( - server = settings.host, - port = settings.port, - credentials = settings.credentials, - filters = listOf( - "r/${location.latitude.asDecimal}/${location.longitude.asDecimal}/${ - settings.searchRadius.toMeters().value(Kilo).toInt() - }" - ) - ) { read, write -> - logger.info("APRS-IS Connected") - clientConnectionState.value = DriverConnectionState.Connected - attempts = 0 - coroutineScope { - launch { read.consumeEach { send(it) } } - launch { transmitQueue.collect { write.send(it) } } - } - } - } catch (error: CancellationException) { - logger.trace("APRS-IS Connection cancelling", error) - throw error - } catch (error: Throwable) { - logger.error("APRS-IS Connection terminated unexpectedly", error) - delay(100*(2.0.pow(attempts)).toLong().coerceAtMost(60_000)) - ++attempts - } finally { - clientConnectionState.value = DriverConnectionState.Disconnected - } - } - } - } - - private suspend fun captureStringPacket(data: String): CapturedPacket? { - val parsed = try { - aprsCodec.fromString(data) - } catch (parsingError: Throwable) { - logger.warn("Unable to parse packet", parsingError) - return null - } - - return packetStorage.save(data.toByteArray(Charsets.US_ASCII), parsed, PacketOrigin.AprsIs) - } -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/PacketDriver.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/PacketDriver.kt deleted file mode 100644 index 9ad7780f..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/PacketDriver.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.inkapplications.ack.data.drivers - -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.structures.AprsPacket -import com.inkapplications.ack.structures.EncodingConfig -import kotlinx.coroutines.flow.Flow - -interface PacketDriver { - val connectionState: Flow - val incoming: Flow - val transmitPermissions get() = emptySet() - val receivePermissions get() = emptySet() - suspend fun transmitPacket(packet: AprsPacket, encodingConfig: EncodingConfig) - suspend fun connect() - suspend fun disconnect() -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/PacketDrivers.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/PacketDrivers.kt deleted file mode 100644 index 989d385a..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/PacketDrivers.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.inkapplications.ack.data.drivers - -class PacketDrivers( - val internetDriver: InternetDriver, - val afskDriver: AfskDriver, - val tncDriver: TncDriver, -) diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/TncDriver.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/TncDriver.kt deleted file mode 100644 index 83836ca8..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/drivers/TncDriver.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.inkapplications.ack.data.drivers - -import android.Manifest -import android.bluetooth.BluetoothClass -import android.os.Build -import androidx.annotation.RequiresPermission -import com.inkapplications.ack.codec.AprsCodec -import com.inkapplications.ack.data.CapturedPacket -import com.inkapplications.ack.data.PacketOrigin -import com.inkapplications.ack.data.PacketStorage -import com.inkapplications.ack.data.kiss.kissData -import com.inkapplications.ack.data.kiss.writeKissData -import com.inkapplications.ack.structures.AprsPacket -import com.inkapplications.ack.structures.EncodingConfig -import com.inkapplications.android.extensions.bluetooth.BluetoothDeviceAccess -import com.inkapplications.android.extensions.bluetooth.BluetoothDeviceData -import com.inkapplications.coroutines.filterItems -import com.inkapplications.standard.throwCancels -import kimchi.logger.KimchiLogger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import java.io.IOException -import java.io.OutputStream -import java.util.* - -class TncDriver internal constructor( - private val bluetoothAccess: BluetoothDeviceAccess, - private val packetStorage: PacketStorage, - private val ack: AprsCodec, - private val runScope: CoroutineScope, - private val logger: KimchiLogger, -): PacketDriver { - /** - * Bluetooth Serial Port Profile (SPP) UUID. - */ - private val SPP_UID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB") - - private val runJob = MutableStateFlow(null) - private val selectedDevice = MutableStateFlow(null) - private val deviceConnectionState = MutableStateFlow(DriverConnectionState.Disconnected) - override val connectionState: Flow = combine(deviceConnectionState, selectedDevice) { connection, device -> - when { - device == null -> DriverConnectionState.Disconnected - connection == DriverConnectionState.Disconnected -> DriverConnectionState.Connecting - else -> connection - } - } - override val receivePermissions: Set = when { - Build.VERSION.SDK_INT >= 34 -> setOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.FOREGROUND_SERVICE_LOCATION, - Manifest.permission.BLUETOOTH_CONNECT, - Manifest.permission.BLUETOOTH_SCAN, - ) - Build.VERSION.SDK_INT > 31 -> setOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.BLUETOOTH_CONNECT, - Manifest.permission.BLUETOOTH_SCAN, - ) - else -> setOf(Manifest.permission.ACCESS_FINE_LOCATION) - } - override val transmitPermissions: Set = when { - Build.VERSION.SDK_INT >= 34 -> setOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.FOREGROUND_SERVICE_LOCATION, - Manifest.permission.BLUETOOTH_CONNECT, - Manifest.permission.BLUETOOTH_SCAN - ) - Build.VERSION.SDK_INT > 31 -> setOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.BLUETOOTH_CONNECT, - Manifest.permission.BLUETOOTH_SCAN - ) - else -> setOf(Manifest.permission.ACCESS_FINE_LOCATION) - } - private val outputStream = MutableStateFlow(null) - private val networkDevices = bluetoothAccess.devices - .filterItems { it.majorClass == BluetoothClass.Device.Major.NETWORKING } - - val deviceData: Flow = combine( - deviceConnectionState, - selectedDevice, - networkDevices, - ) { connectionState, selectedDevice, devices -> - ConnectTncData( - connectedDevice = selectedDevice.takeIf { connectionState == DriverConnectionState.Connected }, - connectingDevice = selectedDevice.takeIf { connectionState == DriverConnectionState.Connecting }, - discoveredDevices = devices, - ) - } - - override val incoming = MutableSharedFlow() - - fun selectDevice(device: BluetoothDeviceData) { - selectedDevice.value = device - } - - override suspend fun disconnect() { - deviceConnectionState.value = DriverConnectionState.Disconnecting - runJob.value?.cancelAndJoin() - } - - override suspend fun transmitPacket(packet: AprsPacket, encodingConfig: EncodingConfig) { - val data = ack.toAx25(packet, encodingConfig) - outputStream.value?.writeKissData(data) - } - - @RequiresPermission(allOf = [Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN]) - override suspend fun connect() { - runJob.updateAndGet { priorJob -> - priorJob?.takeIf { it.isActive } ?: launchNewJob() - } - } - - @RequiresPermission(allOf = [Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN]) - private fun launchNewJob(): Job { - logger.debug("Launching new job for Bluetooth Packet Capture") - return runScope.launch { - try { - selectedDevice - .filterNotNull() - .collectLatest { deviceData -> - deviceConnectionState.value = DriverConnectionState.Connecting - try { - bluetoothAccess.connect( - device = deviceData, - uuid = SPP_UID - ) { input, output -> - outputStream.value = output - deviceConnectionState.value = DriverConnectionState.Connected - input.kissData().collect { data -> - runCatching { - val packet = ack.fromAx25(data) - val saved = packetStorage.save(data, packet, PacketOrigin.Tnc) - incoming.emit(saved) - }.throwCancels().onFailure { - logger.error("Failed to capture packet", it) - } - } - } - } catch (error: IOException) { - logger.warn("IO Error while connecting to TNC. Possibly during Disconnect.", error) - } finally { - outputStream.value = null - deviceConnectionState.value = DriverConnectionState.Disconnected - } - } - } finally { - selectedDevice.value = null - } - } - } -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/kiss/KissSchema.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/kiss/KissSchema.kt deleted file mode 100644 index 9e551200..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/kiss/KissSchema.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.inkapplications.ack.data.kiss - -/** - * KISS Protocol signifiers used for parsing. - * - * @see http://www.ka9q.net/papers/kiss.html - */ -internal object KissSchema { - const val FrameEnd = 0xC0 - const val FrameEscape = 0xDB - const val TransposedFrameEnd = 0xDC - const val TransposedFrameEscape = 0xDD - - object HostTypes { - const val Data = 0x00 - } - - object TncTypes { - const val Data = 0x00 - const val TxDelay = 0x01 - const val Persistence = 0x02 - const val SlotTime = 0x03 - const val TxTail = 0x04 - const val FullDuplex = 0x05 - const val SetHardware = 0x06 - const val Return = 0xFF - } -} diff --git a/aprs-android/src/main/java/com/inkapplications/ack/data/kiss/Streams.kt b/aprs-android/src/main/java/com/inkapplications/ack/data/kiss/Streams.kt deleted file mode 100644 index cb983fca..00000000 --- a/aprs-android/src/main/java/com/inkapplications/ack/data/kiss/Streams.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.inkapplications.ack.data.kiss - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import java.io.InputStream -import java.io.OutputStream -import kotlin.coroutines.resume - -/** - * Extract frame data from a serial stream via the KISS protocol. - * - * @see http://www.ka9q.net/papers/kiss.html - */ -fun InputStream.kissData(): Flow { - return flow { - val buffer = ArrayList() - while (currentCoroutineContext().isActive) { - when (val char = readSuspend()) { - KissSchema.FrameEnd -> { - if (buffer.isNotEmpty()) { - val command = buffer[0] - - when (command) { - KissSchema.HostTypes.Data.toByte() -> { - emit(buffer.drop(1).toByteArray()) - buffer.clear() - } - } - } - } - KissSchema.FrameEscape -> when (readSuspend()) { - KissSchema.TransposedFrameEnd -> buffer += KissSchema.FrameEnd.toByte() - KissSchema.TransposedFrameEscape -> buffer += KissSchema.FrameEscape.toByte() - } - else -> buffer += char.toByte() - } - } - } -} - -/** - * Write a byte array to the stream via the KISS protocol. - * - * @param bytes The data to write. - * @param type The type of frame the data bytes represent. - * @see http://www.ka9q.net/papers/kiss.html - */ -fun OutputStream.writeKissData( - bytes: ByteArray, - type: Int = KissSchema.TncTypes.Data, -) { - write(KissSchema.FrameEnd) - write(type) - write(bytes) - write(KissSchema.FrameEnd) - flush() -} - -/** - * Read a single byte from the stream, suspending on the IO thread until - * it is available. - */ -private suspend fun InputStream.readSuspend(): Int { - return withContext(Dispatchers.IO) { - suspendCancellableCoroutine { cont -> - cont.invokeOnCancellation { close() } - cont.resume(read()) - } - } -} diff --git a/aprs-android/src/main/sqldelight/com/inkapplications/ack/data/CapturedPacketEntity.sq b/aprs-android/src/main/sqldelight/com/inkapplications/ack/data/CapturedPacketEntity.sq deleted file mode 100644 index 73554277..00000000 --- a/aprs-android/src/main/sqldelight/com/inkapplications/ack/data/CapturedPacketEntity.sq +++ /dev/null @@ -1,69 +0,0 @@ -import kotlinx.datetime.Instant; -import com.inkapplications.ack.data.CaptureId; -import com.inkapplications.ack.data.PacketOrigin; -import com.inkapplications.ack.structures.station.Callsign; - -CREATE TABLE CapturedPacketEntity ( - id INTEGER AS CaptureId PRIMARY KEY AUTOINCREMENT, - timestamp INTEGER AS Instant NOT NULL, - raw_data BLOB NOT NULL, - packet_origin TEXT AS PacketOrigin NOT NULL, - source_callsign TEXT AS Callsign NOT NULL, - addressee_callsign TEXT AS Callsign, - data_type TEXT NOT NULL, - comment_field TEXT -); - -findById: -SELECT * FROM CapturedPacketEntity WHERE id = :id; - -countAll: -SELECT count(*) FROM CapturedPacketEntity; - -findRecent: -SELECT * FROM CapturedPacketEntity ORDER BY timestamp DESC LIMIT :count; - -findBySourceCallsign: -SELECT * FROM CapturedPacketEntity WHERE source_callsign = :callsign LIMIT :limit; - -countSources: -SELECT count(DISTINCT source_callsign) FROM CapturedPacketEntity; - -findMostRecentByType: -SELECT * FROM CapturedPacketEntity WHERE data_type = :type ORDER BY timestamp DESC LIMIT 1; - -findStationComments: -SELECT * FROM CapturedPacketEntity WHERE comment_field LIKE '%MHz%' ORDER BY timestamp DESC LIMIT :limit; - -findConversation: -SELECT * FROM CapturedPacketEntity -WHERE (source_callsign = :addresseeCallsign AND addressee_callsign = :callsign) -OR (source_callsign = :callsign AND addressee_callsign = :addresseeCallsign); - -findLatestConversationMessages: -SELECT * FROM CapturedPacketEntity -WHERE id IN ( - SELECT id FROM ( - SELECT source_callsign AS filterkey, * FROM CapturedPacketEntity - WHERE addressee_callsign = :callsign - UNION - SELECT addressee_callsign AS filterkey, * FROM CapturedPacketEntity - WHERE (source_callsign = :callsign AND addressee_callsign IS NOT NULL) - ORDER BY timestamp DESC - ) - GROUP BY filterkey -); - -addPacket: - INSERT INTO CapturedPacketEntity ( - timestamp, - raw_data, - packet_origin, - source_callsign, - addressee_callsign, - data_type, - comment_field - ) VALUES (?, ?, ?, ?, ?, ?, ?); - -lastId: - SELECT last_insert_rowid(); diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index e0a3ab61..00000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -plugins { - `kotlin-dsl` -} -repositories { - gradlePluginPortal() - mavenCentral() - google() -} -dependencies { - implementation(libs.kotlin.gradle) - implementation(libs.android.gradle) - implementation(libs.google.services) - implementation(libs.google.license.plugin) - implementation(libs.firebase.crashlytics.gradle) - implementation(libs.hilt.plugin) - implementation(libs.sqldelight.gradle) -} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts deleted file mode 100644 index b5a0fabf..00000000 --- a/buildSrc/settings.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -dependencyResolutionManagement { - versionCatalogs { - create("libs") { - from(files("../gradle/libs.versions.toml")) - } - } -} diff --git a/buildSrc/src/main/kotlin/Properties.kt b/buildSrc/src/main/kotlin/Properties.kt deleted file mode 100644 index 4f2f296c..00000000 --- a/buildSrc/src/main/kotlin/Properties.kt +++ /dev/null @@ -1,21 +0,0 @@ -import org.gradle.api.Project - -fun Project.stringProperty(key: String, default: String): String { - return if (hasProperty(key)) property(key).toString() else default -} - -fun Project.optionalStringProperty(key: String): String? { - return if (hasProperty(key)) property(key).toString() else null -} - -fun Any?.buildQuote(): String { - return if (this == null) "null" else "\"$this\"" -} - -fun Project.intProperty(key: String, default: Int): Int { - return if (hasProperty(key)) property(key).toString().toInt() else default -} - -fun Project.booleanProperty(key: String, default: Boolean): Boolean { - return if (hasProperty(key)) property(key).toString().toBoolean() else default -} diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 23bba3eb..00000000 --- a/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -android.enableJetifier=false -android.useAndroidX=true -org.gradle.jvmargs=-Xmx2G diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml deleted file mode 100644 index 971eeb52..00000000 --- a/gradle/libs.versions.toml +++ /dev/null @@ -1,283 +0,0 @@ -[versions] -activity = "1.9.3" -compose-compiler = "1.5.9" -ack = "1.0.0" -kimchi-core = "2.2.0" -kimchi-firebase = "2.0.0" -watermelon = "1.7.0" -kotlin = "1.9.22" -coroutines = "1.8.1" -dagger = "2.46.1" -sqldelight = "2.0.1" - -## -# Kotlin Dependencies -## -[libraries.kotlin-gradle] -version.ref = "kotlin" -module = "org.jetbrains.kotlin:kotlin-gradle-plugin" - -[libraries.kotlin-test-junit] -version.ref = "kotlin" -module = "org.jetbrains.kotlin:kotlin-test-junit" - -[libraries.kotlin-test-core] -version.ref = "kotlin" -module = "org.jetbrains.kotlin:kotlin-test" - -[libraries.kotlin-test-annotations] -version.ref = "kotlin" -module = "org.jetbrains.kotlin:kotlin-test-annotations-common" - -[libraries.coroutines-core] -version.ref = "coroutines" -module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" - -[libraries.coroutines-android] -version.ref = "coroutines" -module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" - -[libraries.coroutines-test] -version.ref = "coroutines" -module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" - -## -# Android Dependencies -## -[libraries.android-gradle] -version = "8.1.1" -module = "com.android.tools.build:gradle" - -[libraries.androidx-core] -version = "1.12.0" -module = "androidx.core:core-ktx" - -[libraries.androidx-test-core] -version = "1.9.0" -module = "androidx.test:core" - -[libraries.androidx-test-runner] -version = "1.5.2" -module = "androidx.test:runner" - -[libraries.androidx-appcompat] -version = "1.6.1" -module = "androidx.appcompat:appcompat" - -[libraries.androidx-preference] -version = "1.2.0" -module = "androidx.preference:preference-ktx" - -[libraries.androidx-compose-ui] -version = "1.6.1" -module = "androidx.compose.ui:ui" - -[libraries.androidx-compose-foundation] -version = "1.6.1" -module = "androidx.compose.foundation:foundation" - -[libraries.androidx-compose-material-core] -version = "1.4.3" -module = "androidx.compose.material:material" - -[libraries.androidx-activity-compose] -version.ref = "activity" -module = "androidx.activity:activity-compose" - -[libraries.androidx-activity-ktx] -version.ref = "activity" -module = "androidx.activity:activity-ktx" - -[libraries.androidx-compose-navigation] -version = "2.5.3" -module = "androidx.navigation:navigation-compose" - -[libraries.androidx-compose-material-icons-core] -version = "1.4.3" -module = "androidx.compose.material:material-icons-core" - -[libraries.androidx-compose-material-icons-extended] -version = "1.4.3" -module = "androidx.compose.material:material-icons-extended" - -[libraries.android-tools-desugar-jdk] -version = "2.0.4" -module = "com.android.tools:desugar_jdk_libs" - -[libraries.androidx-annotation] -version = "1.7.1" -module = "androidx.annotation:annotation" - -## -# Ink Dependencies -## -[libraries.kimchi-logger] -version.ref = "kimchi-core" -module = "com.inkapplications.kimchi:logger" - -[libraries.kimchi-core] -version.ref = "kimchi-core" -module = "com.inkapplications.kimchi:core" - -[libraries.kimchi-analytics] -version.ref = "kimchi-core" -module = "com.inkapplications.kimchi:analytics" - -[libraries.kimchi-firebase-crashlytics] -version.ref = "kimchi-firebase" -module = "com.inkapplications.kimchi-firebase:crashlytics" - -[libraries.kimchi-firebase-analytics] -version.ref = "kimchi-firebase" -module = "com.inkapplications.kimchi-firebase:analytics" - -[libraries.ack-codec] -version.ref = "ack" -module = "com.inkapplications.ack:codec" - -[libraries.ack-client] -version.ref = "ack" -module = "com.inkapplications.ack:client" - -[libraries.ack-structures] -version.ref = "ack" -module = "com.inkapplications.ack:structures" - -[libraries.spondee-units] -version = "1.2.0" -module = "com.inkapplications.spondee:units" - -[libraries.watermelon-coroutines] -version.ref = "watermelon" -module = "com.inkapplications.watermelon:coroutines" - -[libraries.watermelon-standard] -version.ref = "watermelon" -module = "com.inkapplications.watermelon:standard" - -[libraries.watermelon-android] -version.ref = "watermelon" -module = "com.inkapplications.watermelon:android" - -## -# Other / Misc -## -[libraries.sqldelight-gradle] -version.ref = "sqldelight" -module = "app.cash.sqldelight:app.cash.sqldelight.gradle.plugin" - -[libraries.sqldelight-android-driver] -version.ref = "sqldelight" -module = "app.cash.sqldelight:android-driver" - -[libraries.sqldelight-primitives] -version.ref = "sqldelight" -module = "app.cash.sqldelight:primitive-adapters" - -[libraries.sqldelight-coroutines-extensions] -version.ref = "sqldelight" -module = "app.cash.sqldelight:coroutines-extensions" - -[libraries.dagger-core] -version.ref = "dagger" -module = "com.google.dagger:dagger" - -[libraries.dagger-compiler] -version.ref = "dagger" -module = "com.google.dagger:dagger-compiler" - -[libraries.hilt-compiler] -version.ref = "dagger" -module = "com.google.dagger:hilt-compiler" - -[libraries.hilt-plugin] -version.ref = "dagger" -module = "com.google.dagger:hilt-android-gradle-plugin" - -[libraries.hilt-core] -version.ref = "dagger" -module = "com.google.dagger:hilt-android" - -[libraries.androidx-hilt-common] -version.ref = "dagger" -module = "com.google.dagger:hilt-android" - -[libraries.androidx-hilt-compiler] -version.ref = "dagger" -module = "com.google.dagger:hilt-compiler" - -[libraries.androidx-hilt-navigation-compose] -version = "1.0.0" -module = "androidx.hilt:hilt-navigation-compose" - -[libraries.firebase-config] -version = "21.2.1" -module = "com.google.firebase:firebase-config-ktx" - -[libraries.firebase-analytics] -version = "21.2.0" -module = "com.google.firebase:firebase-analytics-ktx" - -[libraries.firebase-crashlytics-gradle] -version = "2.9.4" -module = "com.google.firebase:firebase-crashlytics-gradle" - -[libraries.google-material-core] -version = "1.8.0" -module = "com.google.android.material:material" - -[libraries.google-services] -version = "4.3.15" -module = "com.google.gms:google-services" - -[libraries.google-license-plugin] -version = "0.10.5" -module = "com.google.android.gms:oss-licenses-plugin" - -[libraries.google-license-core] -version = "17.0.0" -module = "com.google.android.gms:play-services-oss-licenses" - -[libraries.junit] -version = "4.12" -module = "junit:junit" - -[libraries.mapbox-android-sdk] -version = "10.11.2" -module = "com.mapbox.maps:android" - -[bundles] -androidx-compose-icons = [ - "androidx-compose-material-icons-core", - "androidx-compose-material-icons-extended", -] -androidx-compose-full = [ - "androidx-compose-ui", - "androidx-compose-foundation", - "androidx-compose-material-core", - "androidx-compose-material-icons-core", - "androidx-compose-material-icons-extended", - "androidx-compose-navigation", - "androidx-activity-compose", -] -watermelon = [ - "watermelon-coroutines", - "watermelon-standard", - "watermelon-android", -] -dagger-kapt = [ - "dagger-compiler", - "hilt-compiler", - "androidx-hilt-compiler", -] -dagger-libraries = [ - "dagger-core", - "hilt-core", - "androidx-hilt-common", - "androidx-hilt-navigation-compose", -] -sqldelight-android = [ - "sqldelight-android-driver", - "sqldelight-primitives", - "sqldelight-coroutines-extensions", -] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 7f93135c..00000000 Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ac72c34e..00000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew deleted file mode 100755 index 0adc8e1a..00000000 --- a/gradlew +++ /dev/null @@ -1,249 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -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 - -# 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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -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 ;; #( - 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 - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - 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" && ! "$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=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=SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -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" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - 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 - # 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 -fi - - -# 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"' - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# 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 deleted file mode 100644 index 6689b85b..00000000 --- a/gradlew.bat +++ /dev/null @@ -1,92 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@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 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, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -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" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -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. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -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. - -goto fail - -: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 %* - -:end -@rem End local scope for the variables with windows NT shell -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! -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 - -:omega diff --git a/mapbox/build.gradle.kts b/mapbox/build.gradle.kts deleted file mode 100644 index 5f3d969a..00000000 --- a/mapbox/build.gradle.kts +++ /dev/null @@ -1,37 +0,0 @@ -plugins { - id("com.android.library") - kotlin("android") -} - -android { - namespace = "com.inkapplications.ack.android.mapbox" - compileSdk = 34 - - defaultConfig { - minSdk = 21 - buildConfigField("String", "MAPBOX_ACCESS_TOKEN", optionalStringProperty("mapboxPublic").buildQuote()) - } - buildFeatures { - compose = true - buildConfig = true - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() - } - compileOptions { - targetCompatibility = JavaVersion.VERSION_11 - sourceCompatibility = JavaVersion.VERSION_11 - } -} - -dependencies { - api(projects.maps) - implementation(projects.aprsAndroid) - implementation(libs.androidx.annotation) - api(libs.androidx.compose.foundation) - implementation(libs.mapbox.android.sdk) - implementation(libs.watermelon.android) -} diff --git a/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapController.kt b/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapController.kt deleted file mode 100644 index fb5c627a..00000000 --- a/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapController.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.inkapplications.ack.android.mapbox - -import android.Manifest -import android.annotation.SuppressLint -import android.content.res.Resources -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import androidx.annotation.RequiresPermission -import com.google.gson.JsonObject -import com.inkapplications.ack.android.maps.* -import com.inkapplications.ack.data.CaptureId -import com.inkapplications.android.continuePropagation -import com.mapbox.geojson.Point -import com.mapbox.maps.CameraOptions -import com.mapbox.maps.MapView -import com.mapbox.maps.MapboxMap -import com.mapbox.maps.Style -import com.mapbox.maps.plugin.animation.easeTo -import com.mapbox.maps.plugin.annotation.annotations -import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions -import com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager -import com.mapbox.maps.plugin.attribution.attribution -import com.mapbox.maps.plugin.gestures.addOnMapClickListener -import com.mapbox.maps.plugin.gestures.gestures -import com.mapbox.maps.plugin.locationcomponent.location -import com.mapbox.maps.plugin.logo.logo -import com.mapbox.maps.plugin.scalebar.scalebar -import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateBearing -import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateOptions -import com.mapbox.maps.plugin.viewport.viewport -import java.util.* - -/** - * Adapt a Mapbox Map view into our controller interface. - */ -class MapboxMapController( - private val view: MapView, - private val map: MapboxMap, - private val style: Style, - private val resources: Resources, - private val onSelect: (CaptureId?) -> Unit, -): MapController { - private val defaultMarkerId = UUID.randomUUID().toString() - - private val symbolManager = view.annotations.createPointAnnotationManager().also { - it.iconAllowOverlap = true - it.addClickListener { - val json = (it.getData() as JsonObject) - map.easeTo( - CameraOptions.Builder() - .center(Point.fromLngLat(json.get("lon").asDouble, json.get("lat").asDouble)) - .build() - ) - onSelect(CaptureId(json.get("id").asLong)) - true - } - map.addOnMapClickListener { - continuePropagation { onSelect(null) } - } - style.addImage(defaultMarkerId, BitmapFactory.decodeResource(resources, R.drawable.default_marker)) - } - - override fun initDefaults() { - view.scalebar.enabled = false - view.gestures.rotateEnabled = false - view.gestures.pitchEnabled = false - setCamera(CameraPositionDefaults.unknownLocation) - view.location.updateSettings { - pulsingEnabled = true - } - } - - override fun setBottomPadding(padding: Float) { - view.attribution.marginBottom = padding - view.logo.marginBottom = padding - } - - override fun showMarkers(markers: Collection) { - markers - .map { marker -> - PointAnnotationOptions() - .withData(JsonObject().also { - it.addProperty("id", marker.id.value) - it.addProperty("lat", marker.coordinates.latitude.asDecimal) - it.addProperty("lon", marker.coordinates.longitude.asDecimal) - }) - .withPoint(Point.fromLngLat(marker.coordinates.longitude.asDecimal, marker.coordinates.latitude.asDecimal)) - .withIconImage(marker.symbol?.let { createImage(it, style) } ?: defaultMarkerId) - } - .run { symbolManager.create(this) } - } - - override fun zoomTo(cameraPosition: MapCameraPosition) { - map.easeTo( - CameraOptions.Builder() - .center(Point.fromLngLat(cameraPosition.coordinates.longitude.asDecimal, cameraPosition.coordinates.latitude.asDecimal)) - .zoom(cameraPosition.zoom.mapboxValue) - .build() - ) - } - - override fun setCamera(cameraPosition: MapCameraPosition) { - map.setCamera( - CameraOptions.Builder() - .center(Point.fromLngLat(cameraPosition.coordinates.longitude.asDecimal, cameraPosition.coordinates.latitude.asDecimal)) - .zoom(cameraPosition.zoom.mapboxValue) - .build() - ) - } - - @RequiresPermission(anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]) - override fun enablePositionTracking() { - view.location.enabled = true - val state = view.viewport.makeFollowPuckViewportState( - FollowPuckViewportStateOptions.Builder() - .zoom(ZoomLevels.ROADS.mapboxValue) - .pitch(0.0) - .bearing(FollowPuckViewportStateBearing.Constant(0.0)) - .build() - ) - view.viewport.transitionTo(state) - } - - @SuppressLint("MissingPermission") - override fun disablePositionTracking() { - view.location.enabled = false - } - - override fun setPanEnabled(boolean: Boolean) { - view.gestures.scrollEnabled = boolean - } - - private fun createImage(image: Bitmap, style: Style): String { - val id = UUID.randomUUID().toString() - - style.addImage(id, image) - - return id - } -} diff --git a/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapRenderer.kt b/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapRenderer.kt deleted file mode 100644 index 4aacd35d..00000000 --- a/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapRenderer.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.inkapplications.ack.android.mapbox - -import android.annotation.SuppressLint -import android.app.Application -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.viewinterop.AndroidView -import com.inkapplications.ack.android.maps.MapController -import com.inkapplications.ack.android.maps.MapRenderer -import com.inkapplications.ack.android.maps.MapViewModel -import com.inkapplications.ack.data.CaptureId -import com.mapbox.maps.MapView -import com.mapbox.maps.ResourceOptionsManager -import kotlinx.coroutines.delay - -object MapboxMapRenderer: MapRenderer { - @SuppressLint("MissingPermission") - @Composable - override fun renderMarkerMap( - viewModel: MapViewModel, - onMapItemClicked: (CaptureId?) -> Unit, - bottomProtection: Dp, - interactive: Boolean, - modifier: Modifier - ) { - var controllerState by remember { mutableStateOf(null) } - val bottomProtectionPx = with(LocalDensity.current) { - bottomProtection.toPx() - } - AndroidView( - factory = { context -> - MapView(context).apply { - createController( - activity = context, - onInit = { controller -> - controllerState = controller - controller.setCamera(viewModel.cameraPosition) - controller.showMarkers(viewModel.markers) - controller.setBottomPadding(bottomProtectionPx) - controller.setPanEnabled(interactive) - if (viewModel.enablePositionTracking) { - controller.enablePositionTracking() - } else { - controller.disablePositionTracking() - } - }, - onSelect = onMapItemClicked, - ) - } - }, - update = { mapView -> - controllerState?.let { controller -> - controller.setCamera(viewModel.cameraPosition) - controller.showMarkers(viewModel.markers) - controller.setBottomPadding(bottomProtectionPx) - controller.setPanEnabled(interactive) - if (viewModel.enablePositionTracking) { - controller.enablePositionTracking() - } else { - controller.disablePositionTracking() - } - } - }, - modifier = modifier, - ) - } - - override suspend fun initialize(application: Application) { - ResourceOptionsManager.getDefault(application, BuildConfig.MAPBOX_ACCESS_TOKEN) - delay(500) // Fix race condition in MapView initialization - } -} diff --git a/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapViews.kt b/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapViews.kt deleted file mode 100644 index df3785df..00000000 --- a/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxMapViews.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.inkapplications.ack.android.mapbox - -import android.content.Context -import android.content.res.Configuration -import com.inkapplications.ack.android.maps.MapController -import com.inkapplications.ack.data.CaptureId -import com.mapbox.maps.MapView -import com.mapbox.maps.Style - -/** - * Create a Map controller by initializing a Mapbox Map. - */ -internal inline fun MapView.createController( - activity: Context, - crossinline onInit: (MapController) -> Unit, - noinline onSelect: (CaptureId?) -> Unit, -) { - val map = getMapboxMap() - val styleUri = if (activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { - Style.DARK - } else { - Style.LIGHT - } - - map.loadStyleUri(styleUri) { style -> - val controller = MapboxMapController(this, map, style, activity.resources, onSelect = onSelect) - controller.initDefaults() - onInit(controller) - } -} diff --git a/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxZoomLevels.kt b/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxZoomLevels.kt deleted file mode 100644 index 26855660..00000000 --- a/mapbox/src/main/kotlin/com/inkapplications/ack/android/mapbox/MapboxZoomLevels.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.inkapplications.ack.android.mapbox - -import com.inkapplications.ack.android.maps.ZoomLevels - -/** - * Mapbox adaptation of zoom levels. - */ -internal val ZoomLevels.mapboxValue: Double get() = when (this) { - ZoomLevels.MIN -> 0.0 - ZoomLevels.CONTINENT -> 2.0 - ZoomLevels.ISLANDS -> 4.0 - ZoomLevels.RIVERS -> 7.0 - ZoomLevels.ROADS -> 10.0 - ZoomLevels.BUILDINGS -> 15.0 - ZoomLevels.MAX -> 22.0 -} diff --git a/mapbox/src/main/res/drawable-nodpi/default_marker.png b/mapbox/src/main/res/drawable-nodpi/default_marker.png deleted file mode 100644 index fd82b6d9..00000000 Binary files a/mapbox/src/main/res/drawable-nodpi/default_marker.png and /dev/null differ diff --git a/maps/build.gradle.kts b/maps/build.gradle.kts deleted file mode 100644 index 857c3344..00000000 --- a/maps/build.gradle.kts +++ /dev/null @@ -1,34 +0,0 @@ -plugins { - id("com.android.library") - kotlin("android") -} - -android { - namespace = "com.inkapplications.ack.android.maps" - compileSdk = 34 - - defaultConfig { - minSdk = 21 - } - - buildFeatures { - compose = true - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() - } - compileOptions { - targetCompatibility = JavaVersion.VERSION_11 - sourceCompatibility = JavaVersion.VERSION_11 - } -} - -dependencies { - implementation(libs.androidx.annotation) - api(projects.aprsAndroid) - api(libs.androidx.compose.foundation) -} diff --git a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/DummyMapRenderer.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/DummyMapRenderer.kt deleted file mode 100644 index f9b7403f..00000000 --- a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/DummyMapRenderer.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.inkapplications.ack.android.maps - -import android.app.Application -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.inkapplications.ack.data.CaptureId - -object DummyMapRenderer: MapRenderer { - @Composable - override fun renderMarkerMap( - viewModel: MapViewModel, - onMapItemClicked: (CaptureId?) -> Unit, - bottomProtection: Dp, - interactive: Boolean, - modifier: Modifier - ) {} - override suspend fun initialize(application: Application) {} -} diff --git a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapCameraPosition.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapCameraPosition.kt deleted file mode 100644 index 1c7926e5..00000000 --- a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapCameraPosition.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.inkapplications.ack.android.maps - -import inkapplications.spondee.spatial.GeoCoordinates -import inkapplications.spondee.spatial.latitude -import inkapplications.spondee.spatial.longitude - -/** - * Contains information on how to display the map to the user - */ -data class MapCameraPosition( - val coordinates: GeoCoordinates, - val zoom: ZoomLevels, -) - -object CameraPositionDefaults { - val unknownLocation = MapCameraPosition( - coordinates = GeoCoordinates(39.828497055897344.latitude, (-98.57943535336678).longitude), - zoom = ZoomLevels.CONTINENT, - ) -} diff --git a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapController.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapController.kt deleted file mode 100644 index abceb0b1..00000000 --- a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapController.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.inkapplications.ack.android.maps - -import android.Manifest.permission -import androidx.annotation.RequiresPermission - -/** - * Actions that can be sent to control the map view. - */ -interface MapController { - /** - * Initialize the map with default settings - */ - fun initDefaults() - - /** - * Add padding to the bottom of the map. - * - * This is used when there is an element like a bottom bar obstructing - * the full map, and ensures that elements are still displayed properly. - */ - fun setBottomPadding(padding: Float) - - /** - * Display a set of markers on the map. - */ - fun showMarkers(markers: Collection) - - /** - * Animate the map to a specific location and zoom level - */ - fun zoomTo(cameraPosition: MapCameraPosition) - - /** - * Immediately set the position and zoom level of the map to a location, without animating. - */ - fun setCamera(cameraPosition: MapCameraPosition) - - /** - * Display and zoom to the device's current location on the map. - */ - @RequiresPermission(anyOf = [permission.ACCESS_FINE_LOCATION, permission.ACCESS_COARSE_LOCATION]) - fun enablePositionTracking() - - /** - * Disable the device's current location from being tracked and displayed. - */ - fun disablePositionTracking() - - /** - * Enable or disable panning the map view. - */ - fun setPanEnabled(boolean: Boolean) -} - diff --git a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapRenderer.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapRenderer.kt deleted file mode 100644 index b041b870..00000000 --- a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapRenderer.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.inkapplications.ack.android.maps - -import android.app.Application -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.inkapplications.ack.data.CaptureId - -interface MapRenderer { - @Composable - fun renderMarkerMap( - viewModel: MapViewModel, - onMapItemClicked: (CaptureId?) -> Unit, - bottomProtection: Dp, - interactive: Boolean, - modifier: Modifier, - ) - - suspend fun initialize(application: Application) -} diff --git a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapViewModel.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapViewModel.kt deleted file mode 100644 index 59f15e24..00000000 --- a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MapViewModel.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.inkapplications.ack.android.maps - -data class MapViewModel( - val cameraPosition: MapCameraPosition, - val markers: Collection, - val enablePositionTracking: Boolean = false, -) diff --git a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MarkerViewState.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MarkerViewState.kt deleted file mode 100644 index 712949f1..00000000 --- a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/MarkerViewState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.inkapplications.ack.android.maps - -import android.graphics.Bitmap -import com.inkapplications.ack.data.CaptureId -import inkapplications.spondee.spatial.GeoCoordinates - -/** - * View data for a marker displayed on the map. - */ -data class MarkerViewState( - val id: CaptureId, - val coordinates: GeoCoordinates, - val symbol: Bitmap? -) diff --git a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/ZoomLevels.kt b/maps/src/main/kotlin/com/inkapplications/ack/android/maps/ZoomLevels.kt deleted file mode 100644 index d2e34c91..00000000 --- a/maps/src/main/kotlin/com/inkapplications/ack/android/maps/ZoomLevels.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.inkapplications.ack.android.maps - -/** - * Shorthand for common zoom levels. - */ -enum class ZoomLevels { - MIN, - CONTINENT, - ISLANDS, - RIVERS, - ROADS, - BUILDINGS, - MAX, -} diff --git a/settings.gradle.kts b/settings.gradle.kts deleted file mode 100644 index 1f5faf7c..00000000 --- a/settings.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") - -rootProject.name = "ack-android" - -include("android-extensions") -include("aprs-android") -include("android-application") -include("maps") -include("mapbox")