Compare commits

...
119 Commits
Author SHA1 Message Date
Arty Bishop c7282a3290 v3.2.3 - Added Spanish (es-rES) translation by CertainBot #183 2025-12-27 13:46:17 +00:00
Arty Bishop 2abe0c4582 v3.2.2 - Fixed translation issues #168 #172, AMSAT priority #173 2025-10-04 11:21:22 +01:00
Eugene c2081e93b8 fixes for Ukrainian translation 2025-09-27 14:51:39 +01:00
Arty Bishop c9d7f5df09 v3.2.1 - Added Ukrainian language and language switching, API 35 2025-09-07 15:35:48 +01:00
Arty Bishop c910e37d00 Added negative elevation filtering when sent via BT/Network (#164) 2025-09-07 15:22:06 +01:00
Eugene c0debc038a Add locale_config.xml to allow per-app language switching (#166) 2025-09-02 17:33:48 +01:00
Eugene c3e2c75b60 Add translations for Ukrainian 2025-08-26 14:21:00 +01:00
Arty Bishop f4c7814061 v3.2.0 - Added calendar support, Sinhala translation 2025-07-07 17:31:58 +01:00
Arty Bishop 3a163fb939 Merge pull request #137 from 493505110/main
Add calendar support
2024-08-04 15:20:24 +01:00
zhou2008 e41ac8f367 add calendar support 2024-07-31 19:07:32 +08:00
Arty Bishop 04da45e278 v3.1.4 - Fixed leap year bug, various other minor fixes 2024-03-09 15:18:50 +00:00
Arty Bishop b1562b4796 v3.1.3 - Fixed the satellite data update bug #108 2023-05-21 19:36:30 +01:00
Chris Kuethe b5adc73a6d Update McCants TLE URL
According to https://www.mmccants.org/tles/index.html, "PrismNet will be shutting down in a few weeks."

I have no idea how long that means, but let's fix it before it's too late.
2023-04-12 10:10:52 +01:00
Arty Bishop 4fc76ecbd9 Updated dependencies, minor fixes 2023-01-27 15:59:19 +00:00
mrchi fb296786a5 Update button width for btn_prev and btn_next 2022-12-07 10:32:40 +00:00
mrchi e41cee61cf Update translations for zh-rCN 2022-12-07 10:32:40 +00:00
mrchi c9d0c414d0 Update zh-rCN file to keep same format with origin 2022-12-07 10:32:40 +00:00
Arty Bishop b156f666ef v3.1.2 - Android 13, auto update, R8 minification 2022-10-23 14:20:54 +01:00
Arty Bishop baf61ca9bc Swapped WorkManager for manual update check 2022-10-23 14:20:54 +01:00
Arty Bishop 1685691d2f Added recent data update info 2022-10-23 14:20:54 +01:00
Arty Bishop e531eb85e3 Enabled R8 build minification 2022-10-23 14:20:54 +01:00
Arty Bishop ccad82aa6d Added automatic data update toggle #96 2022-10-23 14:20:54 +01:00
Arty Bishop 836fac2bb7 Added automatic data update worker 2022-10-23 14:20:54 +01:00
Arty Bishop 216f96caef Added auto passes recalculation on TLE update 2022-10-23 14:20:54 +01:00
Arty Bishop c5c8df0b5e Switched to targeting Android 13, API v33 2022-10-23 14:20:54 +01:00
Arty Bishop b9a25cc941 Added Charles proxy network config for debugging 2022-10-23 14:20:54 +01:00
Arty Bishop bbb89b7e6b v3.1.1 - R4UAB source, minor fixes, libs update 2022-08-07 13:33:21 +01:00
Arty Bishop bf1ac5fc2c Fixed negative lat/lon manual input bug 2022-07-16 13:47:41 +02:00
Arty Bishop 73557d30f2 Increased radar sweep rotation speed 2022-07-16 13:34:09 +02:00
Arty Bishop 0de157f3e2 Re-enabled the rotator control switch 2022-07-16 13:33:50 +02:00
Arty Bishop 9529f74c48 v3.1.0 - Chinese i18n, BT reporting, satellite categories 2022-06-03 16:14:40 +01:00
Arty Bishop 378c3ac4bf Updated dependencies, minor fixes 2022-05-18 19:00:48 +01:00
Arty Bishop e4a4b7eeb1 Updated dependencies, extracted versions 2022-05-18 19:00:48 +01:00
Arty Bishop 7653de2cee Added basic SatTypes spinner to EntriesFragment.kt 2022-05-18 19:00:48 +01:00
Arty Bishop 53697ef0c4 Now storing fetched satellite types in shared prefs 2022-05-18 19:00:48 +01:00
Arty Bishop 8c9d2031c2 Added satellite categories and sources map 2022-05-18 19:00:48 +01:00
Arty Bishop a006d0558a Fixed radar track display bug in RadarViewModel.kt 2022-05-18 19:00:48 +01:00
Arty Bishop 278c06170b Minor fixes to strings and ui, crash fixed #63 2022-05-18 19:00:48 +01:00
Arty Bishop a2f37e6b9f Merge pull request #92 from BA7LWN/main
Add files via upload
2022-05-16 20:27:52 +01:00
BA7LWN 93fa038b36 Add files via upload
Add Simplified Chinese
2022-05-15 12:36:09 +08:00
Arty Bishop 9f10f1d2e4 Merge pull request #87 from ajohns1288/main
Bluetooth added - but needs improvement
2022-03-21 19:44:01 +00:00
Andrew dd76dcfc98 Bluetooth added 2022-03-18 14:35:14 -04:00
Arty Bishop b35c3a0e61 v3.0.3 - Minor correcting release 2022-03-16 08:55:15 +00:00
Arty Bishop 47a83a8bfe Fixed UninitPropertyAccessException crash in MapFragment 2022-03-13 20:38:06 +00:00
Arty Bishop de2978138a v3.0.2 - Fixes and code cleanup, version bumped 2022-03-11 14:38:53 +00:00
Arty Bishop abb1bd4718 Various tweaks and minor refactoring, fixed issue #84 2022-03-11 12:45:26 +00:00
Arty Bishop ec553bf429 Added more satellite parameters to the map screen 2022-03-10 10:42:20 +00:00
Arty Bishop 488fe352cd Added satellite visibility and phase to MapFragment.kt #62 2022-03-09 13:22:20 +00:00
Arty Bishop 7fdc939450 Added satellite visibility parameter to RadarFragment.kt #62 2022-03-09 12:50:15 +00:00
Arty Bishop ae08c7c937 Added satellite visibility parameter to SatPos.kt #62 2022-03-07 15:14:57 +00:00
Arty Bishop 4ee74a6789 Renamed and moved several classes 2022-03-06 18:12:11 +00:00
Arty Bishop 5804f42211 Swapped buy me a coffee platform for ko-fi 2022-03-06 18:12:11 +00:00
Arty Bishop 29c3b2f2f1 Moved and renamed several classes 2022-03-06 18:12:11 +00:00
Arty Bishop 20bf1cedd0 Added Celestrak and SatNOGS links to the README.md 2022-03-03 20:28:32 +00:00
Arty Bishop 8a24c50448 v3.0.0 - Minor fixes, updates to readme, copyright and version 2022-03-02 15:07:29 +00:00
Arty Bishop 3b761f9589 Added an empty list message to the Radar screen 2022-03-01 18:49:32 +00:00
Arty Bishop 20306ec83e Added a basic default fragment transition animation 2022-03-01 18:33:40 +00:00
Arty Bishop 930e05cc58 Various changes to layouts and strings 2022-03-01 18:32:59 +00:00
Arty Bishop acc308379c Added an empty list message to the Passes screen 2022-03-01 18:32:58 +00:00
Arty Bishop 65941a8304 Sorted out the memory leak in radar fragment 2022-02-27 10:41:46 +00:00
Arty Bishop c58cf94181 Sorted out the memory leak in passes fragment 2022-02-26 19:50:09 +00:00
Arty Bishop 88228bfad7 Fixed an issue with messed up layouts via system override #83 2022-02-25 21:33:39 +00:00
Arty Bishop 550ffdf92d Swapped colors and enabled smooth scroll on the map screen 2022-02-24 18:53:48 +00:00
Arty Bishop 89be2dfa43 Added satellite dots to increase visibility on the map 2022-02-24 18:24:45 +00:00
Arty Bishop 03323172ea Fixed an old issue regarding satellite footprint rendering #36 2022-02-23 19:17:41 +00:00
Arty Bishop 74cbad5127 Interfaces and classes refactoring and code cleanup 2022-02-22 19:30:07 +00:00
Arty Bishop 0c71c4db29 Changes to RadarFragment's satellite data views 2022-02-22 11:29:49 +00:00
Arty Bishop 6189fb497e Further fixes to strings and PassesFragment's ui 2022-02-21 18:45:41 +00:00
Arty Bishop 00549bbcba Various fixes to PassesFragment's ui state handling 2022-02-21 17:27:53 +00:00
Arty Bishop 9fd0d5f2e2 Various changes to layouts, styles, strings and drawables 2022-02-20 20:01:47 +00:00
Arty Bishop 567200cfe4 Showing entries and radios numbers in SettingsFragment.kt 2022-02-19 13:52:45 +00:00
Arty Bishop 0404f7be90 Added ability to get the number of entries and radios 2022-02-19 13:27:15 +00:00
Arty Bishop afe9f77869 Further changes to strings, layouts and styles 2022-02-19 12:32:24 +00:00
Arty Bishop 72f78e5c6b Changes to location and settings handlers 2022-02-19 12:13:02 +00:00
Arty Bishop ffd1fc1551 Restored manual location input functionality #40 2022-02-19 12:13:01 +00:00
Arty Bishop 9d6dbc8d31 Swapped locator alert dialog for a dedicated dialog fragment 2022-02-19 12:11:50 +00:00
Arty Bishop 6400d1d395 Alterations to the pass filter dialog UI 2022-02-19 12:11:50 +00:00
Arty Bishop dc680231ea Swapped modes alert dialog for a custom dialog fragment 2022-02-19 12:11:50 +00:00
Arty Bishop 97e6f8b7f5 Various UI and code changes to SourcesDialog.kt 2022-02-19 12:11:50 +00:00
Arty Bishop 22d0db1d78 Further changes to layouts and views #76 2022-02-19 12:11:49 +00:00
Arty Bishop bdc0000476 Added BottomAppBar to several fragments, various ui changes 2022-02-19 12:10:30 +00:00
Arty Bishop 64b5010a44 Destructively migrated to the new database 2022-02-19 12:10:30 +00:00
Arty Bishop 92eec290b1 Moved satellite selections storage to SharedPreferences 2022-02-19 12:10:30 +00:00
Arty Bishop a7c840d532 Restored satellite selection functionality 2022-02-19 12:10:30 +00:00
Arty Bishop 80ba3644f7 Altered bottom navigation view back stack saving 2022-02-19 12:10:30 +00:00
Arty Bishop a0c6ceef15 Restored bottom navigation bar for comparison 2022-02-19 12:10:29 +00:00
Arty Bishop 8b8c443bd1 Switched to using SafeArgs plugin for data transfer 2022-02-19 12:07:40 +00:00
Arty Bishop 72f71b2153 Fixed pass preferences values handling 2022-02-19 12:07:40 +00:00
Arty Bishop 7349ee6c5e Optimized satellite item selection flow + bugfix 2022-02-19 12:07:39 +00:00
Arty Bishop 04c4ef25d2 Extracted some logic to the core Predictor.kt class 2022-02-19 12:07:39 +00:00
Arty Bishop b5da3d3ac4 Various visual changes, update error fix 2022-02-19 12:07:39 +00:00
Arty Bishop 28a6fecab3 Added instrumentation testing dependencies 2022-02-19 12:07:39 +00:00
Arty Bishop 7eb25e45b3 Disabled navigation to satellites fragment on empty database 2022-02-19 12:07:39 +00:00
Arty Bishop e9dc166e1e Added a function to clear the database #79 2022-02-19 12:07:39 +00:00
Arty Bishop a7427f3632 Extracted ContentResolver into LocalSource.kt 2022-02-07 17:50:03 +00:00
Arty Bishop 9b38e2c63e Enabled data update progress indicator 2022-02-07 16:01:40 +00:00
Arty Bishop 61cbc212b5 Swapped SettingsHandler.kt class for its interface 2022-02-06 19:22:39 +00:00
Arty Bishop 27156b5070 Dependencies update, minor code cleanup 2022-02-06 18:02:35 +00:00
Arty Bishop b8bb6d41f9 Extracted logic into SettingsViewModel.kt 2022-01-30 20:32:49 +00:00
Arty Bishop 794d132086 Dependencies update, code cleanup 2022-01-26 19:15:44 +00:00
Arty Bishop 608b7e9311 Now properly handling the state of location updates 2022-01-24 17:56:57 +00:00
Arty Bishop b0b2283ca5 Even further changes to layouts and styles 2022-01-23 19:21:07 +00:00
Arty Bishop 38787b45c1 More changes to layouts, fragments and styles 2022-01-23 17:01:59 +00:00
Arty Bishop f9ebe84012 Further changes to layouts and fragments 2022-01-22 20:36:59 +00:00
Arty Bishop f34cb8eb3c Added database migration to v5 2022-01-22 13:42:10 +00:00
Arty Bishop 148104880e Minor fixes and code cleanup 2022-01-13 21:13:17 +00:00
Arty Bishop f559c60154 Temporarily restored BottomNavigationBar 2022-01-10 22:15:21 +00:00
Arty Bishop 82032109dd Minor visual changes to dialogs and settings 2022-01-09 18:03:21 +00:00
Arty Bishop 7d3d746c4d Further changes to SettingsFragment.kt 2022-01-09 18:03:21 +00:00
Arty Bishop a9a4aa9737 Switched to a generic LocationHandler.kt 2022-01-09 18:03:21 +00:00
Arty Bishop eebe691419 Fixed satellite track exception crash 2022-01-09 18:03:20 +00:00
Arty Bishop 6926f87747 Further changes to layouts and fragments 2022-01-09 18:01:48 +00:00
Arty Bishop 971379b773 Added stale data cleanup on every data update 2022-01-09 18:01:48 +00:00
Arty Bishop ad3bdff823 Further changes to fragments and layouts 2022-01-09 18:01:45 +00:00
Arty Bishop 9c137ada9f Moved DataSources storage to SharedPreferences 2022-01-09 18:00:25 +00:00
Arty Bishop 0f4a70b204 Further changes to layouts and fragments 2022-01-09 17:59:24 +00:00
Arty Bishop f58f851151 Added PassesPrefsDialog.kt with extra, will cleanup later 2022-01-09 17:59:24 +00:00
Arty Bishop be26b3e1dc Restored Preferences fragment functionality 2022-01-09 17:59:24 +00:00
Arty Bishop 9ca8ec65c4 Replaced PreferencesAPI fragment with a regular one 2022-01-09 17:59:10 +00:00
191 changed files with 7504 additions and 4492 deletions

No files matched your search

+15 -12
View File
@@ -3,18 +3,20 @@ name: Look4Sat CI
on: on:
push: push:
tags: tags:
- '*' - v**
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout latest commit - name: Checkout Latest Commit
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Setup java environment - name: Setup java environment
uses: actions/setup-java@v1 uses: actions/setup-java@v4
with: { java-version: 11 } with:
distribution: 'temurin'
java-version: '17'
- name: Assemble APK and Bundle - name: Assemble APK and Bundle
run: | run: |
@@ -32,7 +34,7 @@ jobs:
alias: ${{ secrets.KEY_ALIAS }} alias: ${{ secrets.KEY_ALIAS }}
keyPassword: ${{ secrets.KEY_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }}
env: env:
BUILD_TOOLS_VERSION: "31.0.0" BUILD_TOOLS_VERSION: "35.0.1"
- name: Sign Bundle - name: Sign Bundle
uses: r0adkll/sign-android-release@v1 uses: r0adkll/sign-android-release@v1
@@ -44,24 +46,25 @@ jobs:
alias: ${{ secrets.KEY_ALIAS }} alias: ${{ secrets.KEY_ALIAS }}
keyPassword: ${{ secrets.KEY_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }}
env: env:
BUILD_TOOLS_VERSION: "31.0.0" BUILD_TOOLS_VERSION: "35.0.1"
- name: Rename APK - name: Rename APK and Bundle
run: | run: |
mv ${{steps.sign_apk.outputs.signedReleaseFile}} app/build/outputs/apk/release/look4sat.apk mv ${{steps.sign_apk.outputs.signedReleaseFile}} app/build/outputs/apk/release/look4sat.apk
mv ${{steps.sign_bundle.outputs.signedReleaseFile}} app/build/outputs/apk/release/look4sat.aab
- name: Deploy Bundle to Play Store - name: Deploy Bundle to Play Store
uses: r0adkll/upload-google-play@v1.0.15 uses: r0adkll/upload-google-play@v1
with: with:
serviceAccountJsonPlainText: ${{secrets.SERVICE_ACCOUNT_JSON}} serviceAccountJsonPlainText: ${{secrets.SERVICE_ACCOUNT_JSON}}
packageName: com.rtbishop.look4sat packageName: com.rtbishop.look4sat
track: production releaseFiles: app/build/outputs/apk/release/look4sat.aab
track: beta
whatsNewDirectory: fastlane/metadata/android/en-US/whatsnew whatsNewDirectory: fastlane/metadata/android/en-US/whatsnew
releaseFiles: ${{steps.sign_bundle.outputs.signedReleaseFile}}
- name: Create release - name: Create release
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
artifacts: app/build/outputs/apk/release/look4sat.apk artifacts: "app/build/outputs/apk/release/look4sat.apk,app/build/outputs/apk/release/look4sat.aab"
bodyFile: fastlane/metadata/android/en-US/whatsnew/whatsnew-en-US bodyFile: fastlane/metadata/android/en-US/whatsnew/whatsnew-en-US
token: ${{ secrets.RELEASE_TOKEN }} token: ${{ secrets.RELEASE_TOKEN }}
+8 -6
View File
@@ -8,23 +8,25 @@
### Radio satellite tracker and pass predictor for Android, inspired by Gpredict ### Radio satellite tracker and pass predictor for Android, inspired by Gpredict
<p float="left"> <p float="left">
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="160"/> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="180"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="160"/> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="180"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" width="160"/> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" width="180"/>
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" width="160"> <img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" width="180">
</p> </p>
### Track satellite passes with ease! ### Track satellite passes with ease!
Thanks to the huge database provided by Celestrak and SatNOGS you have access to more than 3000 active satellites orbiting Earth. You can search the entire DB by satellite name or by NORAD catnum. Thanks to the huge database provided by [Celestrak](https://celestrak.com/) and [SatNOGS](https://satnogs.org/) you have access to more than 5000 active satellites orbiting Earth. You can search the entire DB by satellite name or by NORAD catnum.
Satellite positions and passes are calculated relative to your location. To get reliable info make sure to set the observation position using GPS or QTH Locator in the Settings menu. Satellite positions and passes are calculated relative to your location. To get reliable info make sure to set the observation position using GPS or QTH Locator in the Settings menu.
The application is built using Kotlin, Coroutines, Architecture Components and Jetpack Navigation. It is now and always will be completely ad-free and open-source. The application is built using Kotlin, Coroutines, Architecture Components and Jetpack Navigation. It is now and always will be completely ad-free and open-source.
Huge thanks to [DownloadAstro](https://appoftheday.downloadastro.com/app/look4sat-satellite-tracker/) team for their interest to the app and the interview published.
## Main features: ## Main features:
* Predicting satellite positions and passes for up to 4 days (96 hours) * Predicting satellite positions and passes for up to a week
* Showing the list of currently active and upcoming satellite passes * Showing the list of currently active and upcoming satellite passes
* Showing the active pass progress, polar trajectory and transceivers info * Showing the active pass progress, polar trajectory and transceivers info
* Showing the satellite positional data, footprint and ground track on a map * Showing the satellite positional data, footprint and ground track on a map
+71 -40
View File
@@ -1,21 +1,22 @@
plugins { plugins {
id "com.android.application" id "com.android.application"
id "kotlin-android" id "org.jetbrains.kotlin.android"
id "kotlin-kapt" id "org.jetbrains.kotlin.kapt"
id "dagger.hilt.android.plugin" id "dagger.hilt.android.plugin"
id "androidx.navigation.safeargs.kotlin"
} }
android { android {
compileSdkVersion 31 namespace 'com.rtbishop.look4sat'
buildToolsVersion "31.0.0" compileSdk 35
defaultConfig { defaultConfig {
applicationId "com.rtbishop.look4sat" applicationId "com.rtbishop.look4sat"
minSdkVersion 21 minSdk 21
resConfigs "en,ru" targetSdk 35
targetSdkVersion 31 versionCode 323
versionCode 260 versionName '3.2.3'
versionName "2.6.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt { kapt {
arguments { arguments {
@@ -25,59 +26,89 @@ android {
} }
} }
} }
// def props = new Properties()
// file("../securityKeys.properties").withInputStream { props.load(it) }
buildTypes { buildTypes {
release { release {
// buildConfigField "String", "ApiKey", props.getProperty("apiKey") minifyEnabled true
// minifyEnabled true shrinkResources true
// shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
postprocessing {
removeUnusedCode true
removeUnusedResources true
obfuscate false
optimizeCode true
}
} }
} }
buildFeatures { buildFeatures {
// compose true
viewBinding true viewBinding true
} }
composeOptions {
kotlinCompilerExtensionVersion compose_compiler_version
}
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString() jvmTarget = JavaVersion.VERSION_17.toString()
} }
packagingOptions { testOptions {
resources.excludes += "DebugProbesKt.bin" animationsDisabled true
unitTests.includeAndroidResources true
} }
androidResources {
generateLocaleConfig true
}
}
kotlin {
jvmToolchain(17)
} }
dependencies { dependencies {
implementation project(':core') implementation project(":base")
implementation "androidx.core:core-splashscreen:$splash_screen_version" implementation "androidx.core:core-ktx:$core_ktx_version"
implementation "androidx.constraintlayout:constraintlayout:$constraint_layout_version" implementation "androidx.core:core-splashscreen:$core_splashscreen_version"
implementation "androidx.constraintlayout:constraintlayout:$constraint_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.navigation:navigation-runtime-ktx:$navigation_version"
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version" implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version" implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"
implementation "androidx.preference:preference-ktx:$preference_version"
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version" kapt "androidx.room:room-compiler:$room_version"
implementation "com.google.android.material:material:$material_version"
implementation "com.google.dagger:hilt-android:$hilt_version" implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version" kapt "com.google.dagger:hilt-compiler:$hilt_version"
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "org.osmdroid:osmdroid-android:$osmdroid_version" implementation "org.osmdroid:osmdroid-android:$osmdroid_version"
implementation "com.jakewharton.timber:timber:$timber_version"
// implementation "androidx.compose.ui:ui:$compose_version"
// implementation "androidx.compose.ui:ui-tooling:$compose_version"
// implementation "androidx.compose.runtime:runtime:$compose_version"
// implementation "androidx.compose.runtime:runtime-livedata:$compose_version"
// implementation "androidx.compose.compiler:compiler:$compose_compiler_version"
// implementation "androidx.compose.animation:animation:$compose_version"
// implementation "androidx.compose.foundation:foundation:$compose_foundation_version"
// implementation "androidx.activity:activity-compose:$activity_compose_version"
// implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
// implementation "androidx.compose.material:material:$compose_material_version"
// implementation "androidx.compose.material:material-icons-extended:$compose_material_version"
// implementation "com.google.android.material:compose-theme-adapter:$material_adapter_version"
// debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
debugImplementation "androidx.fragment:fragment-testing:$fragment_test_version"
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanary_version"
testImplementation "junit:junit:$junit_version" testImplementation "junit:junit:$junit_version"
testImplementation "org.mockito:mockito-android:$mockito_version" testImplementation "org.mockito:mockito-core:$mockito_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" testImplementation "org.robolectric:robolectric:$robolectric_version"
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leak_canary_version" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_test_version"
androidTestImplementation "androidx.test:core:$androidx_test_version"
androidTestImplementation "androidx.test:runner:$androidx_test_version"
androidTestImplementation "androidx.test:rules:$androidx_test_version"
androidTestImplementation "androidx.test.ext:junit:$androidx_junit_version"
androidTestImplementation "androidx.test.ext:truth:$androidx_test_version"
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version"
// androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
androidTestImplementation "org.mockito:mockito-android:$mockito_version"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_test_version"
} }
+11 -21
View File
@@ -1,21 +1,11 @@
# Add project specific ProGuard rules here. # Please add these rules to your existing keep rules in order to suppress warnings.
# You can control the set of applied configuration files using the # This is generated automatically by the Android Gradle plugin.
# proguardFiles setting in build.gradle. -dontwarn org.bouncycastle.jsse.BCSSLParameters
# -dontwarn org.bouncycastle.jsse.BCSSLSocket
# For more details, see -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
# http://developer.android.com/guide/developing/tools/proguard.html -dontwarn org.conscrypt.Conscrypt$Version
-dontwarn org.conscrypt.Conscrypt
# If your project uses WebView with JS, uncomment the following -dontwarn org.conscrypt.ConscryptHostnameVerifier
# and specify the fully qualified class name to the JavaScript interface -dontwarn org.openjsse.javax.net.ssl.SSLParameters
# class: -dontwarn org.openjsse.javax.net.ssl.SSLSocket
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -dontwarn org.openjsse.net.ssl.OpenJSSE
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -1,111 +1,111 @@
{ {
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 4, "version": 1,
"identityHash": "0f08defa24a78647801005e9277faac7", "identityHash": "834981e960b712254e56b633f3772115",
"entities": [ "entities": [
{ {
"tableName": "entries", "tableName": "entries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`isSelected` INTEGER NOT NULL, `name` TEXT NOT NULL, `epoch` REAL NOT NULL, `meanmo` REAL NOT NULL, `eccn` REAL NOT NULL, `incl` REAL NOT NULL, `raan` REAL NOT NULL, `argper` REAL NOT NULL, `meanan` REAL NOT NULL, `catnum` INTEGER NOT NULL, `bstar` REAL NOT NULL, `xincl` REAL NOT NULL, `xnodeo` REAL NOT NULL, `omegao` REAL NOT NULL, `xmo` REAL NOT NULL, `xno` REAL NOT NULL, `isDeepspace` INTEGER NOT NULL, PRIMARY KEY(`catnum`))", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`comment` TEXT, `name` TEXT NOT NULL, `epoch` REAL NOT NULL, `meanmo` REAL NOT NULL, `eccn` REAL NOT NULL, `incl` REAL NOT NULL, `raan` REAL NOT NULL, `argper` REAL NOT NULL, `meanan` REAL NOT NULL, `catnum` INTEGER NOT NULL, `bstar` REAL NOT NULL, `xincl` REAL NOT NULL, `xnodeo` REAL NOT NULL, `omegao` REAL NOT NULL, `xmo` REAL NOT NULL, `xno` REAL NOT NULL, `isDeepspace` INTEGER NOT NULL, PRIMARY KEY(`catnum`))",
"fields": [ "fields": [
{ {
"fieldPath": "isSelected", "fieldPath": "comment",
"columnName": "isSelected", "columnName": "comment",
"affinity": "INTEGER", "affinity": "TEXT",
"notNull": true "notNull": false
}, },
{ {
"fieldPath": "tle.name", "fieldPath": "data.name",
"columnName": "name", "columnName": "name",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.epoch", "fieldPath": "data.epoch",
"columnName": "epoch", "columnName": "epoch",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.meanmo", "fieldPath": "data.meanmo",
"columnName": "meanmo", "columnName": "meanmo",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.eccn", "fieldPath": "data.eccn",
"columnName": "eccn", "columnName": "eccn",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.incl", "fieldPath": "data.incl",
"columnName": "incl", "columnName": "incl",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.raan", "fieldPath": "data.raan",
"columnName": "raan", "columnName": "raan",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.argper", "fieldPath": "data.argper",
"columnName": "argper", "columnName": "argper",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.meanan", "fieldPath": "data.meanan",
"columnName": "meanan", "columnName": "meanan",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.catnum", "fieldPath": "data.catnum",
"columnName": "catnum", "columnName": "catnum",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.bstar", "fieldPath": "data.bstar",
"columnName": "bstar", "columnName": "bstar",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.xincl", "fieldPath": "data.xincl",
"columnName": "xincl", "columnName": "xincl",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.xnodeo", "fieldPath": "data.xnodeo",
"columnName": "xnodeo", "columnName": "xnodeo",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.omegao", "fieldPath": "data.omegao",
"columnName": "omegao", "columnName": "omegao",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.xmo", "fieldPath": "data.xmo",
"columnName": "xmo", "columnName": "xmo",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.xno", "fieldPath": "data.xno",
"columnName": "xno", "columnName": "xno",
"affinity": "REAL", "affinity": "REAL",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "tle.isDeepspace", "fieldPath": "data.isDeepspace",
"columnName": "isDeepspace", "columnName": "isDeepspace",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": true
@@ -121,8 +121,8 @@
"foreignKeys": [] "foreignKeys": []
}, },
{ {
"tableName": "transmitters", "tableName": "radios",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `info` TEXT NOT NULL, `isAlive` INTEGER NOT NULL, `downlink` INTEGER, `uplink` INTEGER, `mode` TEXT, `isInverted` INTEGER NOT NULL, `catnum` INTEGER, PRIMARY KEY(`uuid`))", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `info` TEXT NOT NULL, `isAlive` INTEGER NOT NULL, `downlink` INTEGER, `uplink` INTEGER, `mode` TEXT, `isInverted` INTEGER NOT NULL, `catnum` INTEGER, `comment` TEXT, PRIMARY KEY(`uuid`))",
"fields": [ "fields": [
{ {
"fieldPath": "uuid", "fieldPath": "uuid",
@@ -171,6 +171,12 @@
"columnName": "catnum", "columnName": "catnum",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": false "notNull": false
},
{
"fieldPath": "comment",
"columnName": "comment",
"affinity": "TEXT",
"notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@@ -181,32 +187,12 @@
}, },
"indices": [], "indices": [],
"foreignKeys": [] "foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, PRIMARY KEY(`sourceUrl`))",
"fields": [
{
"fieldPath": "sourceUrl",
"columnName": "sourceUrl",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"sourceUrl"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
} }
], ],
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0f08defa24a78647801005e9277faac7')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '834981e960b712254e56b633f3772115')"
] ]
} }
} }
@@ -0,0 +1,204 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "f43ab966fb7b0508057c359530582ff5",
"entities": [
{
"tableName": "entries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`comment` TEXT, `name` TEXT NOT NULL, `epoch` REAL NOT NULL, `meanmo` REAL NOT NULL, `eccn` REAL NOT NULL, `incl` REAL NOT NULL, `raan` REAL NOT NULL, `argper` REAL NOT NULL, `meanan` REAL NOT NULL, `catnum` INTEGER NOT NULL, `bstar` REAL NOT NULL, `xincl` REAL NOT NULL, `xnodeo` REAL NOT NULL, `omegao` REAL NOT NULL, `xmo` REAL NOT NULL, `xno` REAL NOT NULL, `orbitalPeriod` REAL NOT NULL, `isDeepSpace` INTEGER NOT NULL, PRIMARY KEY(`catnum`))",
"fields": [
{
"fieldPath": "comment",
"columnName": "comment",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "data.name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "data.epoch",
"columnName": "epoch",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.meanmo",
"columnName": "meanmo",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.eccn",
"columnName": "eccn",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.incl",
"columnName": "incl",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.raan",
"columnName": "raan",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.argper",
"columnName": "argper",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.meanan",
"columnName": "meanan",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.catnum",
"columnName": "catnum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "data.bstar",
"columnName": "bstar",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.xincl",
"columnName": "xincl",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.xnodeo",
"columnName": "xnodeo",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.omegao",
"columnName": "omegao",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.xmo",
"columnName": "xmo",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.xno",
"columnName": "xno",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.orbitalPeriod",
"columnName": "orbitalPeriod",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "data.isDeepSpace",
"columnName": "isDeepSpace",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"catnum"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "radios",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `info` TEXT NOT NULL, `isAlive` INTEGER NOT NULL, `downlink` INTEGER, `uplink` INTEGER, `mode` TEXT, `isInverted` INTEGER NOT NULL, `catnum` INTEGER, `comment` TEXT, PRIMARY KEY(`uuid`))",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "info",
"columnName": "info",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlive",
"columnName": "isAlive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downlink",
"columnName": "downlink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uplink",
"columnName": "uplink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mode",
"columnName": "mode",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isInverted",
"columnName": "isInverted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "catnum",
"columnName": "catnum",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "comment",
"columnName": "comment",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uuid"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f43ab966fb7b0508057c359530582ff5')"
]
}
}
@@ -1,134 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "555494c464b5a29285eb5493cc8aa5f6",
"entities": [
{
"tableName": "entries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tle` TEXT NOT NULL, `catNum` INTEGER NOT NULL, `name` TEXT NOT NULL, `isSelected` INTEGER NOT NULL, PRIMARY KEY(`catNum`))",
"fields": [
{
"fieldPath": "tle",
"columnName": "tle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "catNum",
"columnName": "catNum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isSelected",
"columnName": "isSelected",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"catNum"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "transmitters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `info` TEXT NOT NULL, `isAlive` INTEGER NOT NULL, `downlink` INTEGER, `uplink` INTEGER, `mode` TEXT, `isInverted` INTEGER NOT NULL, `catNum` INTEGER, PRIMARY KEY(`uuid`))",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "info",
"columnName": "info",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlive",
"columnName": "isAlive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downlink",
"columnName": "downlink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uplink",
"columnName": "uplink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mode",
"columnName": "mode",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isInverted",
"columnName": "isInverted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "catNum",
"columnName": "catNum",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uuid"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, PRIMARY KEY(`sourceUrl`))",
"fields": [
{
"fieldPath": "sourceUrl",
"columnName": "sourceUrl",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"sourceUrl"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '555494c464b5a29285eb5493cc8aa5f6')"
]
}
}
@@ -1,134 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "555494c464b5a29285eb5493cc8aa5f6",
"entities": [
{
"tableName": "entries",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tle` TEXT NOT NULL, `catNum` INTEGER NOT NULL, `name` TEXT NOT NULL, `isSelected` INTEGER NOT NULL, PRIMARY KEY(`catNum`))",
"fields": [
{
"fieldPath": "tle",
"columnName": "tle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "catNum",
"columnName": "catNum",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isSelected",
"columnName": "isSelected",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"catNum"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "transmitters",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uuid` TEXT NOT NULL, `info` TEXT NOT NULL, `isAlive` INTEGER NOT NULL, `downlink` INTEGER, `uplink` INTEGER, `mode` TEXT, `isInverted` INTEGER NOT NULL, `catNum` INTEGER, PRIMARY KEY(`uuid`))",
"fields": [
{
"fieldPath": "uuid",
"columnName": "uuid",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "info",
"columnName": "info",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isAlive",
"columnName": "isAlive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downlink",
"columnName": "downlink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "uplink",
"columnName": "uplink",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "mode",
"columnName": "mode",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isInverted",
"columnName": "isInverted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "catNum",
"columnName": "catNum",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uuid"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "sources",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, PRIMARY KEY(`sourceUrl`))",
"fields": [
{
"fieldPath": "sourceUrl",
"columnName": "sourceUrl",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"sourceUrl"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '555494c464b5a29285eb5493cc8aa5f6')"
]
}
}
@@ -0,0 +1,17 @@
package com.rtbishop.look4sat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.rtbishop.bbctestapp", appContext.packageName)
}
}
+13 -7
View File
@@ -1,19 +1,25 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.rtbishop.look4sat">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".presentation.Look4SatApplication" android:name=".presentation.MainApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Look4Sat.Main"> android:theme="@style/Theme.Look4Sat.Main">
<activity <activity
android:name=".presentation.Look4SatActivity" android:name=".presentation.MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.Look4Sat.SplashScreen"> android:theme="@style/Theme.Look4Sat.SplashScreen">
<intent-filter> <intent-filter>
@@ -23,4 +29,4 @@
</activity> </activity>
</application> </application>
</manifest> </manifest>
@@ -1,47 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework
import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem
import com.rtbishop.look4sat.domain.model.Transmitter as DomainTransmitter
import com.rtbishop.look4sat.framework.model.SatEntry as FrameworkEntry
import com.rtbishop.look4sat.framework.model.SatItem as FrameworkItem
import com.rtbishop.look4sat.framework.model.Transmitter as FrameworkTransmitter
fun DomainEntry.toFramework() = FrameworkEntry(this.tle, this.isSelected)
fun DomainTransmitter.toFramework() = FrameworkTransmitter(
this.uuid, this.info, this.isAlive, this.downlink,
this.uplink, this.mode, this.isInverted, this.catnum
)
fun FrameworkItem.toDomain() = DomainItem(this.catnum, this.name, this.isSelected, this.modes)
fun FrameworkTransmitter.toDomain() = DomainTransmitter(
this.uuid, this.info, this.isAlive, this.downlink,
this.uplink, this.mode, this.isInverted, this.catnum
)
fun List<DomainEntry>.toFrameworkEntries() = this.map { it.toFramework() }
fun List<DomainTransmitter>.toFramework() = this.map { it.toFramework() }
fun List<FrameworkItem>.toDomainItems() = this.map { it.toDomain() }
fun List<FrameworkTransmitter>.toDomain() = this.map { it.toDomain() }
@@ -0,0 +1,139 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import androidx.core.content.ContextCompat
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.ILocationManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.round
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LocationManager @Inject constructor(
@ApplicationContext private val context: Context,
private val manager: LocationManager,
private val settings: ISettingsManager,
) : LocationListener, ILocationManager {
private val providerDef = LocationManager.PASSIVE_PROVIDER
private val providerNet = LocationManager.NETWORK_PROVIDER
private val providerGps = LocationManager.GPS_PROVIDER
private val locationCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
private val locationFine = Manifest.permission.ACCESS_FINE_LOCATION
private val _stationPosition = MutableStateFlow<DataState<GeoPos>>(DataState.Handled)
private var currentLocator = settings.loadStationLocator()
private var currentPosition = settings.loadStationPosition()
override val stationPosition: StateFlow<DataState<GeoPos>> = _stationPosition
override fun getStationLocator(): String = currentLocator
override fun getStationPosition(): GeoPos = currentPosition
override fun setStationPosition(latitude: Double, longitude: Double) {
if (QthConverter.isValidPosition(latitude, longitude)) {
val tempLon = if (longitude > 180.0) longitude - 180 else longitude
val newLat = latitude.round(4)
val newLon = tempLon.round(4)
QthConverter.positionToQth(newLat, newLon)?.let { locator ->
currentLocator = locator
currentPosition = GeoPos(newLat, newLon)
settings.saveStationLocator(locator)
settings.saveStationPosition(currentPosition)
_stationPosition.value = DataState.Success(currentPosition)
}
} else {
_stationPosition.value =
DataState.Error(context.getString(R.string.location_manual_error))
}
}
override fun setPositionFromGps() {
val result = ContextCompat.checkSelfPermission(context, locationFine)
if (manager.isProviderEnabled(providerGps) && result == PackageManager.PERMISSION_GRANTED) {
val location = manager.getLastKnownLocation(providerDef)
if (location != null) {
setStationPosition(location.latitude, location.longitude)
} else {
_stationPosition.value = DataState.Loading
manager.requestLocationUpdates(providerGps, 0L, 0f, this)
}
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.location_null))
}
}
override fun setPositionFromNet() {
val result = ContextCompat.checkSelfPermission(context, locationCoarse)
if (manager.isProviderEnabled(providerNet) && result == PackageManager.PERMISSION_GRANTED) {
val location = manager.getLastKnownLocation(providerDef)
if (location != null) {
setStationPosition(location.latitude, location.longitude)
} else {
_stationPosition.value = DataState.Loading
manager.requestLocationUpdates(providerNet, 0L, 0f, this)
}
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.location_null))
}
}
override fun setPositionFromQth(locator: String) {
val position = QthConverter.qthToPosition(locator)
if (position != null) {
currentLocator = locator
currentPosition = position
settings.saveStationLocator(locator)
settings.saveStationPosition(currentPosition)
_stationPosition.value = DataState.Success(currentPosition)
} else {
_stationPosition.value = DataState.Error(context.getString(R.string.location_qth_error))
}
}
override fun setPositionHandled() {
_stationPosition.value = DataState.Handled
}
override fun onLocationChanged(location: Location) {
manager.removeUpdates(this)
setStationPosition(location.latitude, location.longitude)
}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
@Deprecated("Deprecated in Java")
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
}
@@ -1,75 +0,0 @@
package com.rtbishop.look4sat.framework
import android.Manifest
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.hardware.GeomagneticField
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import androidx.core.content.ContextCompat
import com.rtbishop.look4sat.domain.LocationProvider
import com.rtbishop.look4sat.domain.QthConverter
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.presentation.round
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LocationSource @Inject constructor(
@ApplicationContext private val context: Context,
private val preferences: SharedPreferences
) : LocationListener, LocationProvider {
private val locManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
private val _updatedLocation = MutableSharedFlow<GeoPos?>()
override val updatedLocation: SharedFlow<GeoPos?> = _updatedLocation
fun getMagDeclination(stationPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
val lat = stationPos.latitude.toFloat()
val lon = stationPos.longitude.toFloat()
return GeomagneticField(lat, lon, 0f, time).declination
}
fun updatePosition(latitude: Double, longitude: Double) {
if (QthConverter.isValidPosition(latitude, longitude)) {
_updatedLocation.tryEmit(GeoPos(latitude, longitude))
} else _updatedLocation.tryEmit(null)
}
fun updatePositionFromQth(qthString: String) {
val position = QthConverter.qthToPosition(qthString)
if (position != null) {
_updatedLocation.tryEmit(GeoPos(position.latitude, position.longitude))
} else _updatedLocation.tryEmit(null)
}
fun updatePositionFromGps() {
val provider = LocationManager.GPS_PROVIDER
val permission = Manifest.permission.ACCESS_FINE_LOCATION
val result = ContextCompat.checkSelfPermission(context, permission)
if (locManager.isProviderEnabled(provider) && result == PackageManager.PERMISSION_GRANTED) {
locManager.requestLocationUpdates(provider, 0L, 0f, this)
} else _updatedLocation.tryEmit(null)
}
fun updatePositionFromNetwork() {
val provider = LocationManager.NETWORK_PROVIDER
val permission = Manifest.permission.ACCESS_COARSE_LOCATION
val result = ContextCompat.checkSelfPermission(context, permission)
if (locManager.isProviderEnabled(provider) && result == PackageManager.PERMISSION_GRANTED) {
locManager.requestLocationUpdates(provider, 0L, 0f, this)
} else _updatedLocation.tryEmit(null)
}
override fun onLocationChanged(location: Location) {
locManager.removeUpdates(this)
val latitude = location.latitude.round(4)
val longitude = location.longitude.round(4)
_updatedLocation.tryEmit(GeoPos(latitude, longitude))
}
}
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -21,12 +21,13 @@ import android.hardware.Sensor
import android.hardware.SensorEvent import android.hardware.SensorEvent
import android.hardware.SensorEventListener import android.hardware.SensorEventListener
import android.hardware.SensorManager import android.hardware.SensorManager
import com.rtbishop.look4sat.domain.predict.RAD2DEG
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.math.round import kotlin.math.round
@Singleton @Singleton
class OrientationSource @Inject constructor(private val sensorManager: SensorManager) : class OrientationManager @Inject constructor(private val sensorManager: SensorManager) :
SensorEventListener { SensorEventListener {
interface OrientationListener { interface OrientationListener {
@@ -36,7 +37,6 @@ class OrientationSource @Inject constructor(private val sensorManager: SensorMan
private val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) private val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
private val rotationMatrix = FloatArray(9) private val rotationMatrix = FloatArray(9)
private val orientationValues = FloatArray(3) private val orientationValues = FloatArray(3)
private val oneRadDegrees = 57.295779513f
private var sensorAccuracy: Int = SensorManager.SENSOR_STATUS_UNRELIABLE private var sensorAccuracy: Int = SensorManager.SENSOR_STATUS_UNRELIABLE
private var orientationListener: OrientationListener? = null private var orientationListener: OrientationListener? = null
@@ -67,9 +67,9 @@ class OrientationSource @Inject constructor(private val sensorManager: SensorMan
private fun updateOrientation(rotationVector: FloatArray) { private fun updateOrientation(rotationVector: FloatArray) {
SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector) SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector)
SensorManager.getOrientation(rotationMatrix, orientationValues) SensorManager.getOrientation(rotationMatrix, orientationValues)
val azimuth = orientationValues[0] * oneRadDegrees val azimuth = (orientationValues[0] * RAD2DEG).toFloat()
val pitch = orientationValues[1] * oneRadDegrees val pitch = (orientationValues[1] * RAD2DEG).toFloat()
val roll = orientationValues[2] * oneRadDegrees val roll = (orientationValues[2] * RAD2DEG).toFloat()
val magneticAzimuth = (azimuth + 360f) % 360f val magneticAzimuth = (azimuth + 360f) % 360f
val roundedAzimuth = round(magneticAzimuth * 10) / 10 val roundedAzimuth = round(magneticAzimuth * 10) / 10
val roundedPitch = round(pitch * 10) / 10 val roundedPitch = round(pitch * 10) / 10
@@ -1,155 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework
import android.content.SharedPreferences
import android.hardware.GeomagneticField
import android.location.LocationManager
import androidx.core.content.edit
import com.rtbishop.look4sat.BuildConfig
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.QthConverter
import com.rtbishop.look4sat.presentation.round
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PreferencesSource @Inject constructor(
private val locationManager: LocationManager,
private val preferences: SharedPreferences
) {
private val keyInitialSetup = "${BuildConfig.VERSION_NAME}update"
companion object {
const val keyModes = "satModes"
const val keyCompass = "compass"
const val keyRadarSweep = "radarSweep"
const val keyTextLabels = "shouldUseTextLabels"
const val keyTimeUTC = "timeUTC"
const val keyHoursAhead = "hoursAhead"
const val keyMinElevation = "minElevation"
const val keyRotator = "isRotatorEnabled"
const val keyRotatorAddress = "rotatorAddress"
const val keyRotatorPort = "rotatorPort"
const val keyLatitude = "stationLat"
const val keyLongitude = "stationLon"
const val keyPositionGPS = "setPositionGPS"
const val keyPositionQTH = "setPositionQTH"
}
fun loadStationPosition(): GeoPos {
val defaultSP = "0.0"
val latitude = preferences.getString(keyLatitude, null) ?: defaultSP
val longitude = preferences.getString(keyLongitude, null) ?: defaultSP
return GeoPos(latitude.toDouble(), longitude.toDouble())
}
private fun saveStationPosition(pos: GeoPos) {
preferences.edit {
putString(keyLatitude, pos.latitude.toString())
putString(keyLongitude, pos.longitude.toString())
}
}
fun updatePositionFromGPS(): Boolean {
return try {
val location = locationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER)
if (location == null) false
else {
val latitude = location.latitude.round(4)
val longitude = location.longitude.round(4)
val stationPosition = GeoPos(latitude, longitude)
saveStationPosition(stationPosition)
return true
}
} catch (exception: SecurityException) {
false
}
}
fun updatePositionFromQTH(qthString: String): Boolean {
val position = QthConverter.qthToPosition(qthString) ?: return false
val stationPosition = GeoPos(position.latitude, position.longitude)
saveStationPosition(stationPosition)
return true
}
fun getMagDeclination(): Float {
val stationPosition = loadStationPosition()
val lat = stationPosition.latitude.toFloat()
val lon = stationPosition.longitude.toFloat()
return GeomagneticField(lat, lon, 0f, System.currentTimeMillis()).declination
}
fun getHoursAhead(): Int {
return preferences.getInt(keyHoursAhead, 8)
}
fun getMinElevation(): Double {
return preferences.getInt(keyMinElevation, 16).toDouble()
}
fun shouldUseTextLabels(): Boolean {
return preferences.getBoolean(keyTextLabels, false)
}
fun shouldUseUTC(): Boolean {
return preferences.getBoolean(keyTimeUTC, false)
}
fun shouldUseCompass(): Boolean {
return preferences.getBoolean(keyCompass, true)
}
fun shouldShowSweep(): Boolean {
return preferences.getBoolean(keyRadarSweep, true)
}
fun isSetupDone(): Boolean {
return preferences.getBoolean(keyInitialSetup, false)
}
fun setSetupDone() {
preferences.edit { putBoolean(keyInitialSetup, true) }
}
fun saveModesSelection(modes: List<String>) {
val modesSet = modes.toSet()
preferences.edit {
putStringSet(keyModes, modesSet)
}
}
fun loadModesSelection(): List<String> {
preferences.getStringSet(keyModes, setOf())?.let { modesSet ->
return modesSet.toList().sorted()
}
return emptyList()
}
fun isRotatorEnabled(): Boolean {
return preferences.getBoolean(keyRotator, false)
}
fun getRotatorServer(): Pair<String, Int> {
val address = preferences.getString(keyRotatorAddress, null) ?: "127.0.0.1"
val port = preferences.getString(keyRotatorPort, null) ?: "4533"
return Pair(address, port.toInt())
}
}
@@ -0,0 +1,221 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework
import android.content.SharedPreferences
import androidx.core.content.edit
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.predict.GeoPos
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SettingsManager @Inject constructor(private val prefs: SharedPreferences) : ISettingsManager {
companion object {
const val keyModes = "satModes"
const val keyCompass = "compass"
const val keyRadarSweep = "radarSweep"
const val keyTimeUTC = "timeUTC"
const val keyLastUpdateTime = "lastUpdateTime"
const val keyAutoUpdateEnabled = "autoUpdateEnabled"
const val keyHoursAhead = "hoursAhead"
const val keyMinElevation = "minElevation"
const val keyRotator = "isRotatorEnabled"
const val keyRotatorAddress = "rotatorAddress"
const val keyRotatorPort = "rotatorPort"
const val keyBTEnabled = "isBTEnabled"
const val keyBTDeviceName = "BTDeviceName"
const val keyBTDeviceAddr = "BTDeviceAddr"
const val keyBTFormat = "BTFormat"
const val keyLatitude = "stationLat"
const val keyLongitude = "stationLon"
const val keyLocator = "stationQTH"
const val keySelection = "selection"
}
override fun loadStationLocator(): String {
return prefs.getString(keyLocator, null) ?: "null"
}
override fun saveStationLocator(locator: String) {
prefs.edit { putString(keyLocator, locator) }
}
override fun loadStationPosition(): GeoPos {
val defaultSP = "0.0"
val latitude = prefs.getString(keyLatitude, null) ?: defaultSP
val longitude = prefs.getString(keyLongitude, null) ?: defaultSP
return GeoPos(latitude.toDouble(), longitude.toDouble())
}
override fun saveStationPosition(position: GeoPos) {
prefs.edit {
putString(keyLatitude, position.lat.toString())
putString(keyLongitude, position.lon.toString())
}
}
override fun saveEntriesSelection(catnums: List<Int>) {
val stringList = catnums.map { catnum -> catnum.toString() }
prefs.edit { putStringSet(keySelection, stringList.toSet()) }
}
override fun loadEntriesSelection(): List<Int> {
val catnums = prefs.getStringSet(keySelection, emptySet())?.map { catnum -> catnum.toInt() }
return catnums?.sorted() ?: emptyList()
}
override fun saveSatType(type: String, catnums: List<Int>) {
val stringList = catnums.map { catnum -> catnum.toString() }
prefs.edit { putStringSet("type$type", stringList.toSet()) }
}
override fun loadSatType(type: String): List<Int> {
val catnums = prefs.getStringSet("type$type", emptySet())?.map { catnum -> catnum.toInt() }
return catnums ?: emptyList()
}
override fun getHoursAhead(): Int {
return prefs.getInt(keyHoursAhead, 24)
}
override fun setHoursAhead(hoursAhead: Int) {
prefs.edit { putInt(keyHoursAhead, hoursAhead) }
}
override fun getMinElevation(): Double {
return prefs.getDouble(keyMinElevation, 16.0)
}
override fun setMinElevation(minElevation: Double) {
prefs.edit { putDouble(keyMinElevation, minElevation) }
}
override fun getUseUTC(): Boolean {
return prefs.getBoolean(keyTimeUTC, false)
}
override fun setUseUTC(value: Boolean) {
prefs.edit { putBoolean(keyTimeUTC, value) }
}
override fun getLastUpdateTime(): Long {
return prefs.getLong(keyLastUpdateTime, 0L)
}
override fun setLastUpdateTime(updateTime: Long) {
prefs.edit { putLong(keyLastUpdateTime, updateTime) }
}
override fun getAutoUpdateEnabled(): Boolean {
return prefs.getBoolean(keyAutoUpdateEnabled, true)
}
override fun setAutoUpdateEnabled(value: Boolean) {
prefs.edit { putBoolean(keyAutoUpdateEnabled, value) }
}
override fun getUseCompass(): Boolean {
return prefs.getBoolean(keyCompass, true)
}
override fun setUseCompass(value: Boolean) {
prefs.edit { putBoolean(keyCompass, value) }
}
override fun getShowSweep(): Boolean {
return prefs.getBoolean(keyRadarSweep, true)
}
override fun setShowSweep(value: Boolean) {
prefs.edit { putBoolean(keyRadarSweep, value) }
}
override fun saveModesSelection(modes: List<String>) {
prefs.edit { putStringSet(keyModes, modes.toSet()) }
}
override fun loadModesSelection(): List<String> {
return prefs.getStringSet(keyModes, null)?.toList()?.sorted() ?: emptyList()
}
override fun getRotatorEnabled(): Boolean {
return prefs.getBoolean(keyRotator, false)
}
override fun setRotatorEnabled(value: Boolean) {
prefs.edit { putBoolean(keyRotator, value) }
}
override fun getRotatorServer(): String {
return prefs.getString(keyRotatorAddress, null) ?: "127.0.0.1"
}
override fun setRotatorServer(value: String) {
prefs.edit { putString(keyRotatorAddress, value) }
}
override fun getRotatorPort(): String {
return prefs.getString(keyRotatorPort, null) ?: "4533"
}
override fun setRotatorPort(value: String) {
prefs.edit { putString(keyRotatorPort, value) }
}
override fun getBTEnabled(): Boolean {
return prefs.getBoolean(keyBTEnabled, false)
}
override fun setBTEnabled(value: Boolean) {
prefs.edit { putBoolean(keyBTEnabled, value) }
}
override fun getBTDeviceAddr(): String {
return prefs.getString(keyBTDeviceAddr, null) ?: "00:0C:BF:13:80:5D"
}
override fun setBTDeviceAddr(value: String) {
prefs.edit { putString(keyBTDeviceAddr, value) }
}
override fun getBTDeviceName(): String {
return prefs.getString(keyBTDeviceName, null) ?: "Default"
}
override fun setBTDeviceName(value: String) {
prefs.edit { putString(keyBTDeviceName, value) }
}
override fun getBTFormat(): String {
return prefs.getString(keyBTFormat, null) ?: "W\$AZ \$EL"
}
override fun setBTFormat(value: String) {
prefs.edit { putString(keyBTFormat, value) }
}
private fun SharedPreferences.getDouble(key: String, default: Double): Double {
return Double.fromBits(getLong(key, default.toRawBits()))
}
private fun SharedPreferences.Editor.putDouble(key: String, double: Double) {
putLong(key, double.toRawBits())
}
}
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -15,18 +15,22 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.rtbishop.look4sat.presentation package com.rtbishop.look4sat.framework.data
import android.app.Application import android.content.ContentResolver
import com.rtbishop.look4sat.BuildConfig import android.net.Uri
import dagger.hilt.android.HiltAndroidApp import com.rtbishop.look4sat.domain.data.IFileDataSource
import timber.log.Timber import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import java.io.InputStream
@HiltAndroidApp class FileDataSource(
class Look4SatApplication : Application() { private val contentResolver: ContentResolver,
private val ioDispatcher: CoroutineDispatcher
) : IFileDataSource {
override fun onCreate() { @Suppress("BlockingMethodInNonBlockingContext")
super.onCreate() override suspend fun getDataStream(uri: String): InputStream? {
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) return withContext(ioDispatcher) { contentResolver.openInputStream(Uri.parse(uri)) }
} }
} }
@@ -0,0 +1,44 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.rtbishop.look4sat.framework.data.dao.EntriesDao
import com.rtbishop.look4sat.framework.data.dao.RadiosDao
import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatRadio
@Database(entities = [SatEntry::class, SatRadio::class], version = 2, exportSchema = true)
abstract class LocalDatabase : RoomDatabase() {
abstract fun entriesDao(): EntriesDao
abstract fun radiosDao(): RadiosDao
}
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE entries_backup (name TEXT NOT NULL, epoch REAL NOT NULL, meanmo REAL NOT NULL, eccn REAL NOT NULL, incl REAL NOT NULL, raan REAL NOT NULL, argper REAL NOT NULL, meanan REAL NOT NULL, catnum INTEGER NOT NULL, bstar REAL NOT NULL, xincl REAL NOT NULL, xnodeo REAL NOT NULL, omegao REAL NOT NULL, xmo REAL NOT NULL, xno REAL NOT NULL, orbitalPeriod REAL NOT NULL, isDeepSpace INTEGER NOT NULL, comment TEXT, PRIMARY KEY(catnum))")
database.execSQL("INSERT INTO entries_backup (name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar, xincl, xnodeo, omegao, xmo, xno, orbitalPeriod, isDeepSpace, comment) SELECT name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar, xincl, xnodeo, omegao, xmo, xno, 1440 / meanmo, 1440 / meanmo >= 225.0, comment FROM entries")
database.execSQL("DROP TABLE entries")
database.execSQL("ALTER TABLE entries_backup RENAME TO entries")
}
}
@@ -0,0 +1,58 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data
import com.rtbishop.look4sat.domain.data.ILocalEntrySource
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.framework.data.dao.EntriesDao
import com.rtbishop.look4sat.domain.model.SatEntry as DomainEntry
import com.rtbishop.look4sat.domain.model.SatItem as DomainItem
import com.rtbishop.look4sat.framework.model.SatEntry as FrameworkEntry
import com.rtbishop.look4sat.framework.model.SatItem as FrameworkItem
class LocalEntrySource(private val entriesDao: EntriesDao) : ILocalEntrySource {
override fun getEntriesTotal() = entriesDao.getEntriesTotal()
override suspend fun getEntriesWithModes(): List<DomainItem> {
return entriesDao.getEntriesWithModes().toDomainItems()
}
override suspend fun getEntriesWithIds(ids: List<Int>): List<Satellite> {
val selectedSatellites = mutableListOf<Satellite>()
ids.chunked(999).forEach { idsPart ->
val entries = entriesDao.getEntriesWithIds(idsPart)
selectedSatellites.addAll(entries.map { entry -> entry.data.getSatellite() })
}
return selectedSatellites
}
override suspend fun insertEntries(entries: List<DomainEntry>) {
entriesDao.insertEntries(entries.toFrameworkEntries())
}
override suspend fun deleteEntries() = entriesDao.deleteEntries()
private fun DomainEntry.toFramework() = FrameworkEntry(this.data, this.comment)
private fun FrameworkItem.toDomain() = DomainItem(this.catnum, this.name, this.modes, false)
private fun List<DomainEntry>.toFrameworkEntries() = this.map { entry -> entry.toFramework() }
private fun List<FrameworkItem>.toDomainItems() = this.map { item -> item.toDomain() }
}
@@ -0,0 +1,53 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data
import com.rtbishop.look4sat.domain.model.SatRadio as DomainRadio
import com.rtbishop.look4sat.framework.model.SatRadio as FrameworkRadio
import com.rtbishop.look4sat.domain.data.ILocalRadioSource
import com.rtbishop.look4sat.framework.data.dao.RadiosDao
class LocalRadioSource(private val radiosDao: RadiosDao) : ILocalRadioSource {
override fun getRadiosTotal() = radiosDao.getRadiosTotal()
override suspend fun getRadiosWithId(id: Int): List<DomainRadio> {
return radiosDao.getRadiosWithId(id).toDomainRadios()
}
override suspend fun insertRadios(radios: List<DomainRadio>) {
radiosDao.deleteRadios()
radiosDao.insertRadios(radios.toFrameworkRadios())
}
override suspend fun deleteRadios() = radiosDao.deleteRadios()
private fun DomainRadio.toFramework() = FrameworkRadio(
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
this.mode, this.isInverted, this.catnum, this.comment
)
private fun FrameworkRadio.toDomain() = DomainRadio(
this.uuid, this.info, this.isAlive, this.downlink, this.uplink,
this.mode, this.isInverted, this.catnum, this.comment
)
private fun List<DomainRadio>.toFrameworkRadios() = this.map { radio -> radio.toFramework() }
private fun List<FrameworkRadio>.toDomainRadios() = this.map { radio -> radio.toDomain() }
}
@@ -0,0 +1,39 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data
import com.rtbishop.look4sat.domain.data.IRemoteDataSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.InputStream
class RemoteDataSource(
private val httpClient: OkHttpClient, private val ioDispatcher: CoroutineDispatcher
) : IRemoteDataSource {
override suspend fun getDataStream(url: String): InputStream? = withContext(ioDispatcher) {
try {
val request = Request.Builder().url(url).build()
httpClient.newCall(request).execute().body?.byteStream()
} catch (exception: Exception) {
null
}
}
}
@@ -0,0 +1,44 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.data.dao
import androidx.room.*
import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatItem
import kotlinx.coroutines.flow.Flow
@Dao
interface EntriesDao {
@Query("SELECT COUNT(*) FROM entries")
fun getEntriesTotal(): Flow<Int>
@Transaction
@Query("SELECT catnum, name FROM entries ORDER BY name ASC")
suspend fun getEntriesWithModes(): List<SatItem>
@Transaction
@Query("SELECT * FROM entries WHERE catnum IN (:selectedIds)")
suspend fun getEntriesWithIds(selectedIds: List<Int>): List<SatEntry>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertEntries(entries: List<SatEntry>)
@Query("DELETE FROM entries")
suspend fun deleteEntries()
}
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -15,26 +15,25 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.rtbishop.look4sat.framework.local package com.rtbishop.look4sat.framework.data.dao
import androidx.room.* import androidx.room.*
import com.rtbishop.look4sat.framework.model.Source import com.rtbishop.look4sat.framework.model.SatRadio
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface SourcesDao { interface RadiosDao {
@Query("SELECT sourceUrl FROM sources") @Query("SELECT COUNT(*) FROM radios")
suspend fun getSources(): List<String> fun getRadiosTotal(): Flow<Int>
@Transaction @Transaction
suspend fun updateSources(sources: List<Source>) { @Query("SELECT * FROM radios WHERE catnum = :id AND isAlive = 1")
deleteSources() suspend fun getRadiosWithId(id: Int): List<SatRadio>
insertSources(sources)
}
@Query("DELETE from sources")
suspend fun deleteSources()
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSources(sources: List<Source>) suspend fun insertRadios(radios: List<SatRadio>)
@Query("DELETE FROM radios")
suspend fun deleteRadios()
} }
@@ -1,60 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.local
import androidx.room.*
import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.SatItem
import kotlinx.coroutines.flow.Flow
@Dao
interface EntriesDao {
@Transaction
@Query("SELECT catnum, name, isSelected FROM entries ORDER BY name ASC")
fun getSatelliteItems(): Flow<List<SatItem>>
@Query("SELECT * FROM entries WHERE isSelected = 1")
suspend fun getSelectedSatellites(): List<SatEntry>
@Transaction
suspend fun updateEntries(entries: List<SatEntry>) {
val entriesSelection = getEntriesSelection()
insertEntries(entries)
restoreSelection(entriesSelection, true)
}
@Query("SELECT catnum FROM entries WHERE isSelected = 1")
suspend fun getEntriesSelection(): List<Int>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertEntries(entries: List<SatEntry>)
@Transaction
suspend fun restoreSelection(catnums: List<Int>, isSelected: Boolean) {
updateEntriesSelection(catnums, isSelected)
}
@Transaction
suspend fun updateEntriesSelection(catnums: List<Int>, isSelected: Boolean) {
catnums.forEach { catnum -> updateEntrySelection(catnum, isSelected) }
}
@Query("UPDATE entries SET isSelected = :isSelected WHERE catnum = :catnum")
suspend fun updateEntrySelection(catnum: Int, isSelected: Boolean)
}
@@ -1,65 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.local
import com.rtbishop.look4sat.data.LocalDataSource
import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.model.Transmitter
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.framework.*
import com.rtbishop.look4sat.framework.model.Source
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class LocalSource(
private val entriesDao: EntriesDao,
private val sourcesDao: SourcesDao,
private val transmittersDao: TransmittersDao
) : LocalDataSource {
override fun getSatelliteItems(): Flow<List<SatItem>> {
return entriesDao.getSatelliteItems().map { items -> items.toDomainItems() }
}
override suspend fun getSelectedSatellites(): List<Satellite> {
return entriesDao.getSelectedSatellites().map { entry -> entry.tle.createSat() }
}
override suspend fun getSources() = sourcesDao.getSources()
override suspend fun getTransmitters(catnum: Int): List<Transmitter> {
return transmittersDao.getTransmitters(catnum).toDomain()
}
override suspend fun updateEntries(entries: List<SatEntry>) {
entriesDao.updateEntries(entries.toFrameworkEntries())
}
override suspend fun updateEntriesSelection(catnums: List<Int>, isSelected: Boolean) {
entriesDao.updateEntriesSelection(catnums, isSelected)
}
override suspend fun updateSources(sources: List<String>) {
sourcesDao.updateSources(sources.map { sourceUrl -> Source(sourceUrl) })
}
override suspend fun updateTransmitters(transmitters: List<Transmitter>) {
transmittersDao.updateTransmitters(transmitters.toFramework())
}
}
@@ -1,68 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.framework.local
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.rtbishop.look4sat.framework.model.Source
import com.rtbishop.look4sat.framework.model.SatEntry
import com.rtbishop.look4sat.framework.model.Transmitter
@Database(
entities = [SatEntry::class, Source::class, Transmitter::class],
version = 4,
exportSchema = true
)
abstract class SatelliteDb : RoomDatabase() {
abstract fun entriesDao(): EntriesDao
abstract fun sourcesDao(): SourcesDao
abstract fun transmittersDao(): TransmittersDao
}
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE trans_backup (uuid TEXT NOT NULL, info TEXT NOT NULL, isAlive INTEGER NOT NULL, downlink INTEGER, uplink INTEGER, mode TEXT, isInverted INTEGER NOT NULL, catNum INTEGER, PRIMARY KEY(uuid))")
database.execSQL("INSERT INTO trans_backup (uuid, info, isAlive, downlink, uplink, mode, isInverted, catNum) SELECT uuid, info, isAlive, downlink, uplink, mode, isInverted, catNum FROM transmitters")
database.execSQL("DROP TABLE transmitters")
database.execSQL("ALTER TABLE trans_backup RENAME TO transmitters")
}
}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE sources (sourceUrl TEXT NOT NULL, PRIMARY KEY(sourceUrl))")
}
}
val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE entries_backup (isSelected INTEGER NOT NULL, name TEXT NOT NULL, epoch REAL NOT NULL, meanmo REAL NOT NULL, eccn REAL NOT NULL, incl REAL NOT NULL, raan REAL NOT NULL, argper REAL NOT NULL, meanan REAL NOT NULL, catnum INTEGER NOT NULL, bstar REAL NOT NULL, xincl REAL NOT NULL, xnodeo REAL NOT NULL, omegao REAL NOT NULL, xmo REAL NOT NULL, xno REAL NOT NULL, isDeepspace INTEGER NOT NULL, PRIMARY KEY(catnum))")
database.execSQL("INSERT INTO entries_backup (isSelected, name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar, xincl, xnodeo, omegao, xmo, xno, isDeepspace) SELECT isSelected, 'name', 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, catNum, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0 FROM entries")
database.execSQL("DROP TABLE entries")
database.execSQL("ALTER TABLE entries_backup RENAME TO entries")
database.execSQL("CREATE TABLE trans_backup (uuid TEXT NOT NULL, info TEXT NOT NULL, isAlive INTEGER NOT NULL, downlink INTEGER, uplink INTEGER, mode TEXT, isInverted INTEGER NOT NULL, catnum INTEGER, PRIMARY KEY(uuid))")
database.execSQL("INSERT INTO trans_backup (uuid, info, isAlive, downlink, uplink, mode, isInverted, catnum) SELECT uuid, info, isAlive, downlink, uplink, mode, isInverted, catNum FROM transmitters")
database.execSQL("DROP TABLE transmitters")
database.execSQL("ALTER TABLE trans_backup RENAME TO transmitters")
}
}
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -17,8 +17,4 @@
*/ */
package com.rtbishop.look4sat.framework.model package com.rtbishop.look4sat.framework.model
import androidx.room.Entity data class DataSource(var url: String = String())
import androidx.room.PrimaryKey
@Entity(tableName = "sources")
data class Source(@PrimaryKey var sourceUrl: String = String())
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -19,11 +19,7 @@ package com.rtbishop.look4sat.framework.model
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Entity import androidx.room.Entity
import com.rtbishop.look4sat.domain.predict.TLE import com.rtbishop.look4sat.domain.predict.OrbitalData
@Entity(tableName = "entries", primaryKeys = ["catnum"]) @Entity(tableName = "entries", primaryKeys = ["catnum"])
data class SatEntry( data class SatEntry(@Embedded val data: OrbitalData, var comment: String? = null)
@Embedded
val tle: TLE,
var isSelected: Boolean = false
)
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -22,10 +22,9 @@ import androidx.room.Relation
data class SatItem( data class SatItem(
val catnum: Int, val catnum: Int,
val name: String, val name: String,
var isSelected: Boolean,
@Relation( @Relation(
parentColumn = "catnum", parentColumn = "catnum",
entity = Transmitter::class, entity = SatRadio::class,
entityColumn = "catnum", entityColumn = "catnum",
projection = ["mode"] projection = ["mode"]
) )
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -20,8 +20,8 @@ package com.rtbishop.look4sat.framework.model
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@Entity(tableName = "transmitters") @Entity(tableName = "radios")
data class Transmitter( data class SatRadio(
@PrimaryKey val uuid: String, @PrimaryKey val uuid: String,
val info: String, val info: String,
val isAlive: Boolean, val isAlive: Boolean,
@@ -29,5 +29,6 @@ data class Transmitter(
var uplink: Long?, var uplink: Long?,
val mode: String?, val mode: String?,
val isInverted: Boolean, val isInverted: Boolean,
val catnum: Int? val catnum: Int?,
var comment: String? = null
) )
@@ -1,19 +0,0 @@
package com.rtbishop.look4sat.framework.remote
import com.rtbishop.look4sat.data.RemoteDataSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.net.URL
class RemoteSource(private val ioDispatcher: CoroutineDispatcher) : RemoteDataSource {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun fetchFileStream(url: String): InputStream? {
return try {
withContext(ioDispatcher) { URL(url).openStream() }
} catch (e: Exception) {
null
}
}
}
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -17,12 +17,11 @@
*/ */
package com.rtbishop.look4sat.injection package com.rtbishop.look4sat.injection
import android.content.ContentResolver import android.bluetooth.BluetoothManager
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.hardware.SensorManager import android.hardware.SensorManager
import android.location.LocationManager import android.location.LocationManager
import androidx.preference.PreferenceManager
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@@ -36,8 +35,8 @@ object AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideContentResolver(@ApplicationContext context: Context): ContentResolver { fun provideBluetoothManager(@ApplicationContext context: Context): BluetoothManager {
return context.contentResolver return context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
} }
@Provides @Provides
@@ -55,6 +54,6 @@ object AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences { fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context) return context.getSharedPreferences("default", Context.MODE_PRIVATE)
} }
} }
@@ -0,0 +1,93 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.injection
import android.bluetooth.BluetoothManager
import android.content.Context
import androidx.room.Room
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationManager
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.data.DataRepository
import com.rtbishop.look4sat.domain.predict.SatelliteManager
import com.rtbishop.look4sat.framework.LocationManager
import com.rtbishop.look4sat.framework.SettingsManager
import com.rtbishop.look4sat.framework.data.FileDataSource
import com.rtbishop.look4sat.framework.data.LocalDatabase
import com.rtbishop.look4sat.framework.data.LocalEntrySource
import com.rtbishop.look4sat.framework.data.LocalRadioSource
import com.rtbishop.look4sat.framework.data.MIGRATION_1_2
import com.rtbishop.look4sat.framework.data.RemoteDataSource
import com.rtbishop.look4sat.presentation.radarScreen.BTReporter
import com.rtbishop.look4sat.utility.DataParser
import com.rtbishop.look4sat.utility.DataReporter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import okhttp3.OkHttpClient
@Module
@InstallIn(SingletonComponent::class)
object BaseModule {
@Provides
@Singleton
fun provideDataRepository(
@ApplicationContext context: Context,
settings: ISettingsManager
): IDataRepository {
val db = Room.databaseBuilder(context, LocalDatabase::class.java, "Look4SatDb")
.addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build()
val parser = DataParser(Dispatchers.Default)
val fileSource = FileDataSource(context.contentResolver, Dispatchers.IO)
val entries = LocalEntrySource(db.entriesDao())
val radios = LocalRadioSource(db.radiosDao())
val remoteSource = RemoteDataSource(OkHttpClient.Builder().build(), Dispatchers.IO)
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
return DataRepository(parser, fileSource, entries, radios, remoteSource, scope, settings)
}
@Provides
@Singleton
fun provideDataReporter(): DataReporter = DataReporter(CoroutineScope(Dispatchers.IO))
@Provides
@Singleton
fun provideBTReporter(manager: BluetoothManager): BTReporter {
return BTReporter(manager, CoroutineScope(Dispatchers.IO))
}
@Provides
@Singleton
fun provideLocationManager(manager: LocationManager): ILocationManager = manager
@Provides
@Singleton
fun provideSatelliteManager(): ISatelliteManager = SatelliteManager(Dispatchers.Default)
@Provides
@Singleton
fun provideSettingsManager(manager: SettingsManager): ISettingsManager = manager
}
@@ -1,67 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.injection
import android.content.Context
import androidx.room.Room
import com.rtbishop.look4sat.data.DefaultRepository
import com.rtbishop.look4sat.domain.DataParser
import com.rtbishop.look4sat.domain.DataRepository
import com.rtbishop.look4sat.domain.DataReporter
import com.rtbishop.look4sat.domain.predict.Predictor
import com.rtbishop.look4sat.framework.local.*
import com.rtbishop.look4sat.framework.remote.RemoteSource
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object CoreModule {
@Provides
@Singleton
fun provideSatelliteRepo(
@ApplicationContext context: Context,
@IoDispatcher ioDispatcher: CoroutineDispatcher,
@DefaultDispatcher defaultDispatcher: CoroutineDispatcher
): DataRepository {
val dataParser = DataParser(defaultDispatcher)
val db = Room.databaseBuilder(context, SatelliteDb::class.java, "SatelliteDb")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4).build()
val localSource = LocalSource(db.entriesDao(), db.sourcesDao(), db.transmittersDao())
val remoteSource = RemoteSource(ioDispatcher)
return DefaultRepository(dataParser, localSource, remoteSource)
}
@Provides
@Singleton
fun providePredictor(@DefaultDispatcher dispatcher: CoroutineDispatcher): Predictor {
return Predictor(dispatcher)
}
@Provides
@Singleton
fun provideDataReporter(@IoDispatcher dispatcher: CoroutineDispatcher): DataReporter {
return DataReporter(dispatcher)
}
}
@@ -1,55 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.injection
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import javax.inject.Qualifier
@Module
@InstallIn(SingletonComponent::class)
object KotlinModule {
@Provides
@DefaultDispatcher
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
@Provides
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@Provides
@MainDispatcher
fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DefaultDispatcher
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class MainDispatcher
@@ -1,108 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.widget.Toast
import androidx.annotation.IdRes
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import androidx.navigation.Navigator
import androidx.navigation.fragment.findNavController
import java.security.MessageDigest
import java.util.concurrent.TimeUnit
import java.util.regex.Pattern
fun Context.toast(text: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, text, duration).apply { show() }
}
fun SharedPreferences.getDouble(key: String, default: Double): Double {
return Double.fromBits(getLong(key, default.toRawBits()))
}
fun SharedPreferences.Editor.putDouble(key: String, double: Double) {
putLong(key, double.toRawBits())
}
fun NavController.navigateSafe(
@IdRes resId: Int,
args: Bundle? = null,
options: NavOptions? = null,
extras: Navigator.Extras? = null
) {
val action = currentDestination?.getAction(resId) ?: graph.getAction(resId)
if (action != null && currentDestination?.id != action.destinationId) {
navigate(resId, args, options, extras)
}
}
fun <T> Fragment.getNavResult(@IdRes id: Int, key: String, onResult: (result: T) -> Unit) {
val backStackEntry = findNavController().getBackStackEntry(id)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME && backStackEntry.savedStateHandle.contains(key)) {
backStackEntry.savedStateHandle.get<T>(key)?.let(onResult)
backStackEntry.savedStateHandle.remove<T>(key)
}
}
backStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) backStackEntry.lifecycle.removeObserver(observer)
})
}
fun <T> Fragment.setNavResult(key: String, value: T) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, value)
}
fun Long.toTimerString(): String {
val format = "%02d:%02d:%02d"
val hours = TimeUnit.MILLISECONDS.toHours(this)
val minutes = TimeUnit.MILLISECONDS.toMinutes(this) % 60
val seconds = TimeUnit.MILLISECONDS.toSeconds(this) % 60
return String.format(format, hours, minutes, seconds)
}
fun Double.round(decimals: Int): Double {
var multiplier = 1.0
repeat(decimals) { multiplier *= 10 }
return kotlin.math.round(this * multiplier) / multiplier
}
fun String.getHash(type: String = "SHA-256"): String {
val hexChars = "0123456789ABCDEF"
val bytes = MessageDigest.getInstance(type).digest(this.toByteArray())
val result = StringBuilder(bytes.size * 2)
bytes.forEach {
val i = it.toInt()
result.append(hexChars[i shr 4 and 0x0f])
result.append(hexChars[i and 0x0f])
}
return result.toString()
}
fun String.isEmailValid(): Boolean {
val expression = "^[\\w.-]+@([\\w\\-]+\\.)+[A-Z]{2,8}$"
val pattern = Pattern.compile(expression, Pattern.CASE_INSENSITIVE)
return pattern.matcher(this).matches()
}
@@ -1,44 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation
import android.graphics.Canvas
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
class ItemDivider(private val resId: Int) : RecyclerView.ItemDecoration() {
override fun onDrawOver(canvas: Canvas, parent: RecyclerView) {
ContextCompat.getDrawable(parent.context, resId)?.let { drawable ->
val leftPos = parent.paddingLeft
val rightPos = parent.width - parent.paddingRight
parent.adapter?.itemCount?.let { childCount ->
for (i in 0 until childCount) {
if (i == (childCount - 1)) continue
parent.getChildAt(i)?.let { view ->
val params = view.layoutParams as RecyclerView.LayoutParams
val topPos = view.bottom + params.bottomMargin
val bottomPos = topPos + drawable.intrinsicHeight
drawable.setBounds(leftPos, topPos, rightPos, bottomPos)
drawable.draw(canvas)
}
}
}
}
}
}
@@ -1,44 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation
import android.annotation.SuppressLint
import android.content.pm.ActivityInfo
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class Look4SatActivity : AppCompatActivity() {
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
super.onCreate(savedInstanceState)
installSplashScreen()
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHost = supportFragmentManager.findFragmentById(R.id.main_nav_host) as NavHostFragment
binding.mainNavBottom.setupWithNavController(navHost.navController)
}
}
@@ -0,0 +1,84 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.os.Bundle
import android.os.SystemClock
import android.view.View
import androidx.annotation.IdRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.navigation.fragment.findNavController
import com.rtbishop.look4sat.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
installSplashScreen()
super.onCreate(savedInstanceState)
setContentView(ActivityMainBinding.inflate(layoutInflater).root)
}
override fun attachBaseContext(newBase: Context?) {
val newConfig = Configuration(newBase?.resources?.configuration)
newConfig.fontScale = 1.0f
applyOverrideConfiguration(newConfig)
super.attachBaseContext(newBase)
}
}
fun <T> Fragment.getNavResult(@IdRes id: Int, key: String, onResult: (result: T) -> Unit) {
val backStackEntry = findNavController().getBackStackEntry(id)
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME && backStackEntry.savedStateHandle.contains(key)) {
backStackEntry.savedStateHandle.get<T>(key)?.let(onResult)
backStackEntry.savedStateHandle.remove<T>(key)
}
}
backStackEntry.lifecycle.addObserver(observer)
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) backStackEntry.lifecycle.removeObserver(observer)
})
}
fun <T> Fragment.setNavResult(key: String, value: T) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(key, value)
}
fun View.clickWithDebounce(debounceTime: Long = 875L, action: () -> Unit) {
this.setOnClickListener(object : View.OnClickListener {
private var lastClickTime: Long = 0
override fun onClick(v: View) {
if (SystemClock.elapsedRealtime() - lastClickTime < debounceTime) return
else action()
lastClickTime = SystemClock.elapsedRealtime()
}
})
}
@@ -0,0 +1,53 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation
import android.app.Application
import android.util.Log
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.framework.SettingsManager
import dagger.hilt.android.HiltAndroidApp
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
@HiltAndroidApp
class MainApplication : Application() {
@Inject
lateinit var repository: IDataRepository
@Inject
lateinit var settingsManager: SettingsManager
override fun onCreate() {
super.onCreate()
checkAutoUpdate()
}
private fun checkAutoUpdate() {
if (settingsManager.getAutoUpdateEnabled()) {
val timeDelta = System.currentTimeMillis() - settingsManager.getLastUpdateTime()
if (timeDelta > 172800000) { // 48 hours in ms
val sdf = SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault())
Log.d("AutoUpdate", "Started periodic data update on ${sdf.format(Date())}")
repository.updateFromWeb()
}
}
}
}
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -22,8 +22,8 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.databinding.ItemEntryBinding import com.rtbishop.look4sat.databinding.ItemEntryBinding
import com.rtbishop.look4sat.domain.model.SatItem
class EntriesAdapter(private val clickListener: EntriesClickListener) : class EntriesAdapter(private val clickListener: EntriesClickListener) :
RecyclerView.Adapter<EntriesAdapter.SatItemHolder>() { RecyclerView.Adapter<EntriesAdapter.SatItemHolder>() {
@@ -59,7 +59,7 @@ class EntriesAdapter(private val clickListener: EntriesClickListener) :
fun bind(item: SatItem, listener: EntriesClickListener) { fun bind(item: SatItem, listener: EntriesClickListener) {
binding.entryCheckbox.text = item.name binding.entryCheckbox.text = item.name
binding.entryCheckbox.isChecked = item.isSelected binding.entryCheckbox.isChecked = item.isSelected
itemView.setOnClickListener { binding.entryCheckbox.setOnClickListener {
listener.updateSelection(listOf(item.catnum), item.isSelected.not()) listener.updateSelection(listOf(item.catnum), item.isSelected.not())
} }
} }
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -19,117 +19,99 @@ package com.rtbishop.look4sat.presentation.entriesScreen
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.snackbar.Snackbar
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentEntriesBinding import com.rtbishop.look4sat.databinding.FragmentEntriesBinding
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.presentation.ItemDivider import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.presentation.clickWithDebounce
import com.rtbishop.look4sat.presentation.getNavResult import com.rtbishop.look4sat.presentation.getNavResult
import com.rtbishop.look4sat.presentation.navigateSafe import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class EntriesFragment : Fragment(R.layout.fragment_entries) { class EntriesFragment : Fragment(R.layout.fragment_entries) {
private val viewModel: EntriesViewModel by viewModels() private val viewModel: EntriesViewModel by viewModels()
private val contentContract = ActivityResultContracts.GetContent() private lateinit var binding: FragmentEntriesBinding
private val filePicker = registerForActivityResult(contentContract) { uri ->
uri?.let { viewModel.updateDataFromFile(uri) }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupComponents(view)
}
private fun setupComponents(view: View) {
val entriesAdapter = EntriesAdapter(viewModel) val entriesAdapter = EntriesAdapter(viewModel)
val binding = FragmentEntriesBinding.bind(view).apply { binding = FragmentEntriesBinding.bind(view).apply {
entriesRecycler.apply { entriesRecycler.apply {
setHasFixedSize(true) setHasFixedSize(true)
adapter = entriesAdapter adapter = entriesAdapter
layoutManager = GridLayoutManager(requireContext(), 2) layoutManager = GridLayoutManager(requireContext(), 2)
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
addItemDecoration(ItemDivider(R.drawable.rec_divider_light)) addItemDecoration(DividerItemDecoration(requireContext(), 1))
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0 && entriesFab.visibility == View.VISIBLE) entriesFab.hide()
else if (dy < 0 && entriesFab.visibility != View.VISIBLE) entriesFab.show()
}
})
} }
entriesImportWeb.setOnClickListener { entriesBtnBack.clickWithDebounce { findNavController().navigateUp() }
findNavController().navigateSafe(R.id.action_entries_to_sources) entriesSearch.doOnTextChanged { text, _, _, _ -> viewModel.setQuery(text.toString()) }
entriesBtnModes.clickWithDebounce {
val direction = EntriesFragmentDirections.entriesToModes()
findNavController().navigate(direction)
} }
entriesImportFile.setOnClickListener { filePicker.launch("*/*") } entriesBtnSelect.clickWithDebounce { viewModel.selectCurrentItems(true) }
entriesSelectMode.setOnClickListener { showModesDialog() } entriesBtnClear.clickWithDebounce { viewModel.selectCurrentItems(false) }
entriesSelectAll.setOnClickListener { viewModel.selectCurrentItems() } val typeMessageFormat = requireContext().getString(R.string.types_message)
entriesSearchBar.setOnQueryTextListener(viewModel) val type = if (viewModel.getSatType().isNullOrBlank()) "All" else viewModel.getSatType()
entriesSearchBar.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI entriesTypeMessage.text = String.format(typeMessageFormat, type)
entriesTypeCard.setOnClickListener { showSelectTypeDialog() }
} }
viewModel.satData.observe(viewLifecycleOwner, { satData -> viewModel.satData.observe(viewLifecycleOwner) { satData ->
handleSatData(satData, binding, entriesAdapter) handleSatData(satData, entriesAdapter)
}) binding.entriesFab.clickWithDebounce {
getNavResult<List<String>>(R.id.nav_entries, "sources") { sources -> setNavResult("selection", viewModel.saveSelection())
viewModel.updateDataFromWeb(sources) findNavController().popBackStack()
}
}
getNavResult<List<String>>(R.id.nav_entries, "modes") { modes ->
viewModel.saveSelectedModes(modes)
} }
} }
private fun handleSatData( private fun handleSatData(dataState: DataState<List<SatItem>>, entriesAdapter: EntriesAdapter) {
dataState: DataState<List<SatItem>>,
binding: FragmentEntriesBinding,
entriesAdapter: EntriesAdapter
) {
when (dataState) { when (dataState) {
is DataState.Success -> { is DataState.Success -> {
entriesAdapter.submitList(dataState.data) entriesAdapter.submitList(dataState.data)
binding.entriesProgress.visibility = View.INVISIBLE binding.entriesProgress.visibility = View.INVISIBLE
binding.entriesRecycler.visibility = View.VISIBLE
} }
is DataState.Loading -> { is DataState.Loading -> {
binding.entriesProgress.visibility = View.VISIBLE binding.entriesProgress.visibility = View.VISIBLE
binding.entriesRecycler.visibility = View.INVISIBLE
}
is DataState.Error -> {
binding.entriesProgress.visibility = View.INVISIBLE
binding.entriesRecycler.visibility = View.VISIBLE
val message = getString(R.string.entries_update_error)
Snackbar.make(requireView(), message, Snackbar.LENGTH_SHORT).show()
}
else -> {
} }
else -> {}
} }
} }
private fun showModesDialog() { private fun showSelectTypeDialog() {
val modes = arrayOf( val satelliteTypes = viewModel.satTypes.toTypedArray()
"AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3", val selectedValue = satelliteTypes.indexOf(viewModel.getSatType())
"CERTO", "CW", "DQPSK", "DSTAR", "DUV", "FFSK", "FM", "FMN", "FSK", "FSK AX.100 Mode 5", val typeFormat = requireContext().getString(R.string.types_message)
"FSK AX.100 Mode 6", "FSK AX.25 G3RUH", "GFSK", "GFSK Rktr", "GMSK", "HRPT", "LoRa",
"LRPT", "LSB", "MFSK", "MSK", "MSK AX.100 Mode 5", "MSK AX.100 Mode 6", "OFDM", "OQPSK",
"PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT"
)
val savedModes = BooleanArray(modes.size)
val selectedModes = viewModel.loadSelectedModes().toMutableList()
selectedModes.forEach { savedModes[modes.indexOf(it)] = true }
AlertDialog.Builder(requireContext()).apply { AlertDialog.Builder(requireContext()).apply {
setTitle(context.getString(R.string.modes_title)) setTitle(R.string.types_title)
setMultiChoiceItems(modes, savedModes) { _, which, isChecked -> setSingleChoiceItems(satelliteTypes, selectedValue) { dialog, index ->
when { val selectedItem = satelliteTypes[index]
isChecked -> selectedModes.add(modes[which]) binding.entriesTypeMessage.text = String.format(typeFormat, selectedItem)
selectedModes.contains(modes[which]) -> selectedModes.remove(modes[which]) viewModel.setSatType(selectedItem)
}
}
setPositiveButton(context.getString(android.R.string.ok)) { _, _ ->
viewModel.saveSelectedModes(selectedModes)
}
setNeutralButton(context.getString(android.R.string.cancel)) { dialog, _ ->
dialog.dismiss() dialog.dismiss()
} }
create().show() create()
show()
} }
} }
} }
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -17,93 +17,88 @@
*/ */
package com.rtbishop.look4sat.presentation.entriesScreen package com.rtbishop.look4sat.presentation.entriesScreen
import android.content.ContentResolver
import android.net.Uri
import android.widget.SearchView
import androidx.lifecycle.* import androidx.lifecycle.*
import com.rtbishop.look4sat.domain.model.SatItem import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.DataRepository import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.framework.PreferencesSource
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class EntriesViewModel @Inject constructor( class EntriesViewModel @Inject constructor(
private val preferences: PreferencesSource, private val repository: IDataRepository,
private val resolver: ContentResolver, private val settings: ISettingsManager
private val dataRepository: DataRepository ) : ViewModel(), EntriesAdapter.EntriesClickListener {
) : ViewModel(), SearchView.OnQueryTextListener, EntriesAdapter.EntriesClickListener {
private val coroutineHandler = CoroutineExceptionHandler { _, throwable -> private val satType = MutableLiveData(String())
Timber.d(throwable) private val transModes = MutableLiveData(settings.loadModesSelection())
_satData.value = DataState.Error(throwable)
}
private val transModes = MutableLiveData(preferences.loadModesSelection())
private val currentQuery = MutableLiveData(String()) private val currentQuery = MutableLiveData(String())
private val itemsFromRepo = liveData {
delay(125)
emit(loadEntriesWithSelection())
} as MutableLiveData
private val itemsWithType = satType.switchMap { type ->
itemsFromRepo.map { items -> filterByType(items, type) }
}
private val itemsWithModes = transModes.switchMap { modes -> private val itemsWithModes = transModes.switchMap { modes ->
liveData { dataRepository.getSatelliteItems().collect { emit(filterByModes(it, modes)) } } itemsWithType.map { items -> filterByModes(items, modes) }
} }
private val itemsWithQuery = currentQuery.switchMap { query -> private val itemsWithQuery = currentQuery.switchMap { query ->
itemsWithModes.map { items -> DataState.Success(filterByQuery(items, query)) } itemsWithModes.map { items -> filterByQuery(items, query) }
} }
private val _satData = MediatorLiveData<DataState<List<SatItem>>>().apply { val satData = itemsWithQuery.map { items -> DataState.Success(items) }
addSource(itemsWithQuery) { value -> this.value = value } val satTypes: List<String> = settings.sourcesMap.keys.sorted()
}
private var shouldSelectAll = true
val satData: LiveData<DataState<List<SatItem>>> = _satData
fun updateDataFromFile(uri: Uri) { fun getSatType(): String? = satType.value
viewModelScope.launch(coroutineHandler) {
_satData.value = DataState.Loading fun setSatType(type: String) {
runCatching { satType.value = type
resolver.openInputStream(uri)?.use { stream -> }
dataRepository.updateDataFromFile(stream)
} fun selectCurrentItems(selectAll: Boolean) {
}.onFailure { _satData.value = DataState.Error(it) } itemsWithQuery.value?.let { itemsWithQuery ->
updateSelection(itemsWithQuery.map { item -> item.catnum }, selectAll)
} }
} }
fun updateDataFromWeb(sources: List<String>) {
viewModelScope.launch(coroutineHandler) {
_satData.value = DataState.Loading
dataRepository.updateDataFromWeb(sources)
}
}
fun selectCurrentItems() {
val currentValue = _satData.value
if (currentValue is DataState.Success) {
updateSelection(currentValue.data.map { it.catnum }, shouldSelectAll)
shouldSelectAll = shouldSelectAll.not()
}
}
fun loadSelectedModes(): List<String> {
return preferences.loadModesSelection()
}
fun saveSelectedModes(modes: List<String>) { fun saveSelectedModes(modes: List<String>) {
transModes.value = modes transModes.value = modes
preferences.saveModesSelection(modes) settings.saveModesSelection(modes)
} }
override fun onQueryTextSubmit(query: String): Boolean { fun setQuery(query: String) {
return true currentQuery.value = query
} }
override fun onQueryTextChange(newText: String): Boolean { fun saveSelection(): List<Int> {
currentQuery.value = newText return itemsFromRepo.value?.let { itemsAll ->
return true itemsAll.filter { item -> item.isSelected }.map { item -> item.catnum }
} ?: emptyList()
} }
override fun updateSelection(catNums: List<Int>, isSelected: Boolean) { override fun updateSelection(catNums: List<Int>, isSelected: Boolean) {
viewModelScope.launch { dataRepository.updateSelection(catNums, isSelected) } itemsFromRepo.value?.let { itemsAll ->
val copiedList = itemsAll.map { item -> item.copy() }
catNums.forEach { catnum ->
copiedList.find { item -> item.catnum == catnum }?.isSelected = isSelected
}
itemsFromRepo.value = copiedList
}
}
private suspend fun loadEntriesWithSelection(): List<SatItem> {
val selectedIds = settings.loadEntriesSelection()
return repository.getEntriesWithModes().onEach { it.isSelected = it.catnum in selectedIds }
}
private fun filterByType(items: List<SatItem>, type: String): List<SatItem> {
if (type == "All") return items
val catnums = settings.loadSatType(type)
if (catnums.isEmpty()) return items
return items.filter { item -> item.catnum in catnums }
} }
private fun filterByModes(items: List<SatItem>, modes: List<String>): List<SatItem> { private fun filterByModes(items: List<SatItem>, modes: List<String>): List<SatItem> {
@@ -0,0 +1,79 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.entriesScreen
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.databinding.ItemModeBinding
class ModesAdapter(private val clickListener: ModesClickListener) :
RecyclerView.Adapter<ModesAdapter.ModeHolder>() {
private val diffCallback = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
return oldItem == newItem
}
}
private val differ = AsyncListDiffer(this, diffCallback)
private var selectedModes = listOf<String>()
fun submitModes(items: List<String>, selected: List<String>) {
differ.submitList(items)
selectedModes = selected
}
override fun getItemCount(): Int = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ModeHolder {
return ModeHolder.from(parent)
}
override fun onBindViewHolder(holder: ModeHolder, position: Int) {
val currentItem = differ.currentList[position]
holder.bind(currentItem, selectedModes.contains(currentItem), clickListener)
}
class ModeHolder(private val binding: ItemModeBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(mode: String, isSelected: Boolean, listener: ModesClickListener) {
binding.modeCheckbox.text = mode
binding.modeCheckbox.isChecked = isSelected
binding.modeCheckbox.setOnClickListener {
listener.onModeClicked(mode, isSelected.not())
}
}
companion object {
fun from(parent: ViewGroup): ModeHolder {
val inflater = LayoutInflater.from(parent.context)
return ModeHolder(ItemModeBinding.inflate(inflater, parent, false))
}
}
}
interface ModesClickListener {
fun onModeClicked(mode: String, isSelected: Boolean)
}
}
@@ -0,0 +1,89 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.entriesScreen
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.DialogModesBinding
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class ModesDialog : AppCompatDialogFragment(), ModesAdapter.ModesClickListener {
@Inject
lateinit var preferences: ISettingsManager
private lateinit var binding: DialogModesBinding
private val allModes = listOf(
"AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3",
"CERTO", "CW", "DQPSK", "DSTAR", "DUV", "FFSK", "FM", "FMN", "FSK", "FSK AX.100 Mode 5",
"FSK AX.100 Mode 6", "FSK AX.25 G3RUH", "GFSK", "GFSK Rktr", "GMSK", "HRPT", "LoRa",
"LRPT", "LSB", "MFSK", "MSK", "MSK AX.100 Mode 5", "MSK AX.100 Mode 6", "OFDM", "OQPSK",
"PSK", "PSK31", "PSK63", "QPSK", "QPSK31", "QPSK63", "SSTV", "USB", "WSJT"
)
private val selectedModes = mutableListOf<String>()
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View {
binding = DialogModesBinding.inflate(inflater, group, false)
return binding.root
}
override fun onViewCreated(view: View, state: Bundle?) {
super.onViewCreated(view, state)
dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
dialog?.window?.setLayout(
(resources.displayMetrics.widthPixels * 0.94).toInt(),
(resources.displayMetrics.heightPixels * 0.76).toInt()
)
val modesAdapter = ModesAdapter(this@ModesDialog)
val layoutManager = GridLayoutManager(context, 2)
val itemDecoration = DividerItemDecoration(context, layoutManager.orientation)
selectedModes.addAll(preferences.loadModesSelection())
modesAdapter.submitModes(allModes, selectedModes)
binding.run {
modesRecycler.apply {
setHasFixedSize(true)
this.adapter = modesAdapter
this.layoutManager = layoutManager
addItemDecoration(itemDecoration)
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
modesBtnNeg.setOnClickListener { dismiss() }
modesBtnPos.setOnClickListener {
setNavResult("modes", selectedModes.toList())
dismiss()
}
}
}
override fun onModeClicked(mode: String, isSelected: Boolean) {
when {
isSelected -> selectedModes.add(mode)
selectedModes.contains(mode) -> selectedModes.remove(mode)
}
}
}
@@ -1,55 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.infoScreen
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.fragment.app.Fragment
import com.rtbishop.look4sat.BuildConfig
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentInfoBinding
class InfoFragment : Fragment(R.layout.fragment_info) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
FragmentInfoBinding.bind(view).apply {
infoVersion.text =
String.format(getString(R.string.info_version), BuildConfig.VERSION_NAME)
infoBtnGithub.setOnClickListener {
gotoUrl("https://github.com/rt-bishop/Look4Sat/")
}
infoBtnFdroid.setOnClickListener {
gotoUrl("https://f-droid.org/en/packages/com.rtbishop.look4sat/")
}
infoBtnDonate.setOnClickListener {
gotoUrl("https://www.buymeacoffee.com/rtbishop")
}
val moveMethod = LinkMovementMethod.getInstance()
infoThanks.movementMethod = moveMethod
infoWarranty.movementMethod = moveMethod
}
}
private fun gotoUrl(url: String) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
}
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -18,15 +18,19 @@
package com.rtbishop.look4sat.presentation.mapScreen package com.rtbishop.look4sat.presentation.mapScreen
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.Satellite
data class MapData( data class MapData(
val pass: Satellite,
val catNum: Int, val catNum: Int,
val name: String, val name: String,
val aosTime: String,
val azimuth: Double,
val elevation: Double,
val range: Double, val range: Double,
val altitude: Double, val altitude: Double,
val velocity: Double, val velocity: Double,
val qthLoc: String, val qthLoc: String,
val osmPos: GeoPos val osmPos: GeoPos,
val period: Double,
val phase: Double,
val eclipsed: Boolean
) )
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -17,56 +17,73 @@
*/ */
package com.rtbishop.look4sat.presentation.mapScreen package com.rtbishop.look4sat.presentation.mapScreen
import android.content.SharedPreferences import android.content.Context
import android.graphics.Color import android.graphics.*
import android.graphics.ColorMatrix import android.graphics.drawable.BitmapDrawable
import android.graphics.ColorMatrixColorFilter import android.graphics.drawable.Drawable
import android.graphics.Paint
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentMapBinding import com.rtbishop.look4sat.databinding.FragmentMapBinding
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.presentation.clickWithDebounce
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.osmdroid.config.Configuration import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.util.GeoPoint import org.osmdroid.util.GeoPoint
import org.osmdroid.views.CustomZoomButtonsController import org.osmdroid.views.CustomZoomButtonsController
import org.osmdroid.views.MapView import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.* import org.osmdroid.views.overlay.FolderOverlay
import timber.log.Timber import org.osmdroid.views.overlay.Marker
import java.util.* import org.osmdroid.views.overlay.Polyline
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class MapFragment : Fragment(R.layout.fragment_map) { class MapFragment : Fragment(R.layout.fragment_map) {
@Inject
lateinit var sharedPreferences: SharedPreferences
private val viewModel: MapViewModel by viewModels() private val viewModel: MapViewModel by viewModels()
private val navArgs: MapFragmentArgs by navArgs()
private val minLat = MapView.getTileSystem().minLatitude private val minLat = MapView.getTileSystem().minLatitude
private val maxLat = MapView.getTileSystem().maxLatitude private val maxLat = MapView.getTileSystem().maxLatitude
private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f strokeWidth = 3f
style = Paint.Style.STROKE style = Paint.Style.STROKE
color = Color.RED color = Color.RED
strokeCap = Paint.Cap.ROUND strokeCap = Paint.Cap.ROUND
strokeJoin = Paint.Join.ROUND strokeJoin = Paint.Join.ROUND
} }
private val footprintPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private val footprintPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 3f
style = Paint.Style.FILL_AND_STROKE style = Paint.Style.FILL_AND_STROKE
color = Color.parseColor("#26FFE082") color = Color.parseColor("#FFE082")
} }
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
textSize = 36f
style = Paint.Style.FILL
color = Color.parseColor("#CCFFFFFF")
setShadowLayer(3f, 3f, 3f, Color.BLACK)
}
private val labelRect = Rect()
private lateinit var binding: FragmentMapBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
Configuration.getInstance().load(requireContext(), sharedPreferences) binding = FragmentMapBinding.bind(view)
val binding = FragmentMapBinding.bind(view).apply { setupComponents()
setupObservers()
}
private fun setupComponents() {
val context = requireContext()
val preferences = context.getSharedPreferences("default", Context.MODE_PRIVATE)
Configuration.getInstance().load(context, preferences)
binding.run {
mapView.apply { mapView.apply {
setMultiTouchControls(true) setMultiTouchControls(true)
setTileSource(TileSourceFactory.WIKIMEDIA) setTileSource(TileSourceFactory.WIKIMEDIA)
@@ -81,50 +98,46 @@ class MapFragment : Fragment(R.layout.fragment_map) {
// add overlays: 0 - GSP, 1 - SatTrack, 2 - SatFootprint, 3 - SatIcons // add overlays: 0 - GSP, 1 - SatTrack, 2 - SatFootprint, 3 - SatIcons
overlays.addAll(Array(4) { FolderOverlay() }) overlays.addAll(Array(4) { FolderOverlay() })
} }
mapBtnBack.clickWithDebounce { findNavController().navigateUp() }
mapBtnPrev.setOnClickListener { viewModel.scrollSelection(true) }
mapBtnNext.setOnClickListener { viewModel.scrollSelection(false) }
} }
binding.mapBtnPrev.setOnClickListener { viewModel.scrollSelection(true) }
binding.mapBtnNext.setOnClickListener { viewModel.scrollSelection(false) }
setupObservers(binding)
} }
private fun setupObservers(binding: FragmentMapBinding) { private fun setupObservers() {
viewModel.selectDefaultSatellite(arguments?.getInt("catNum")) viewModel.selectDefaultSatellite(navArgs.catNum)
viewModel.stationPosLiveData.observe(viewLifecycleOwner, { renderStationPos(it, binding) }) viewModel.stationPos.observe(viewLifecycleOwner) { handleStationPos(it) }
viewModel.satPositions.observe(viewLifecycleOwner, { renderSatPositions(it, binding) }) viewModel.positions.observe(viewLifecycleOwner) { handlePositions(it) }
viewModel.satTrack.observe(viewLifecycleOwner, { renderSatTrack(it, binding) }) viewModel.track.observe(viewLifecycleOwner) { handleTrack(it) }
viewModel.satFootprint.observe(viewLifecycleOwner, { renderSatFootprint(it, binding) }) viewModel.footprint.observe(viewLifecycleOwner) { handleFootprint(it) }
viewModel.mapData.observe(viewLifecycleOwner, { renderSatData(it, binding) }) viewModel.mapData.observe(viewLifecycleOwner) { handleMapData(it) }
} }
private fun renderStationPos(stationPos: GeoPos, binding: FragmentMapBinding) { private fun handleStationPos(stationPos: GeoPos) {
binding.apply { binding.apply {
Marker(mapView).apply { Marker(mapView).apply {
setInfoWindow(null) setInfoWindow(null)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_map_pos) icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_position)
position = GeoPoint(stationPos.latitude, stationPos.longitude) position = GeoPoint(stationPos.lat, stationPos.lon)
mapView.overlays[0] = this mapView.overlays[0] = this
mapView.invalidate() mapView.invalidate()
} }
} }
} }
private fun renderSatPositions(posMap: Map<Satellite, GeoPos>, binding: FragmentMapBinding) { private fun handlePositions(posMap: Map<Satellite, GeoPos>) {
binding.apply { binding.apply {
val markers = FolderOverlay() val markers = FolderOverlay()
posMap.entries.forEach { posMap.entries.forEach {
Marker(mapView).apply { Marker(mapView).apply {
setInfoWindow(null) setInfoWindow(null)
textLabelFontSize = 24
textLabelBackgroundColor = Color.TRANSPARENT
textLabelForegroundColor =
ContextCompat.getColor(requireContext(), R.color.themeLight)
setTextIcon(it.key.params.name)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
icon = getCustomTextIcon(it.key.data.name)
try { try {
position = GeoPoint(it.value.latitude, it.value.longitude) position = GeoPoint(it.value.lat, it.value.lon)
} catch (exception: IllegalArgumentException) { } catch (exception: IllegalArgumentException) {
Timber.d(exception) println(exception.stackTraceToString())
} }
setOnMarkerClickListener { _, _ -> setOnMarkerClickListener { _, _ ->
viewModel.selectSatellite(it.key) viewModel.selectSatellite(it.key)
@@ -138,49 +151,71 @@ class MapFragment : Fragment(R.layout.fragment_map) {
} }
} }
private fun renderSatTrack(satTrack: List<List<GeoPos>>, binding: FragmentMapBinding) { private fun getCustomTextIcon(textLabel: String): Drawable {
textPaint.getTextBounds(textLabel, 0, textLabel.length, labelRect)
val iconSize = 8f
val width = labelRect.width() + iconSize * 2f
val height = textPaint.textSize * 3f + iconSize * 2f
val bitmap = Bitmap.createBitmap(width.toInt(), height.toInt(), Bitmap.Config.ARGB_8888)
Canvas(bitmap).run {
drawCircle(width / 2f, height / 2f, iconSize, textPaint)
drawText(textLabel, iconSize / 2f, height - iconSize, textPaint)
}
return BitmapDrawable(binding.mapView.context.resources, bitmap)
}
private fun handleTrack(satTrack: List<List<GeoPos>>) {
val center = GeoPoint(satTrack[0][0].lat, satTrack[0][0].lon)
val trackOverlay = FolderOverlay() val trackOverlay = FolderOverlay()
satTrack.forEach { track -> satTrack.forEach { track ->
val trackPoints = track.map { GeoPoint(it.latitude, it.longitude) } val trackPoints = track.map { GeoPoint(it.lat, it.lon) }
Polyline().apply { Polyline().apply {
try { try {
setPoints(trackPoints) setPoints(trackPoints)
outlinePaint.set(trackPaint) outlinePaint.set(trackPaint)
trackOverlay.add(this) trackOverlay.add(this)
} catch (exception: IllegalArgumentException) { } catch (exception: IllegalArgumentException) {
Timber.d(exception) println(exception.stackTraceToString())
} }
} }
} }
binding.mapView.overlays[1] = trackOverlay binding.mapView.overlays[1] = trackOverlay
binding.mapView.controller.animateTo(center)
} }
private fun renderSatFootprint(satFootprint: List<GeoPos>, binding: FragmentMapBinding) { private fun handleFootprint(satPos: SatPos) {
val footprintPoints = satFootprint.map { GeoPoint(it.latitude, it.longitude) } val footprintPoints = satPos.getRangeCircle().map { GeoPoint(it.lat, it.lon) }
val footprintOverlay = Polygon().apply { val footprintOverlay = Polyline().apply {
fillPaint.set(footprintPaint)
outlinePaint.set(footprintPaint) outlinePaint.set(footprintPaint)
try { try {
this.points = footprintPoints setPoints(footprintPoints)
} catch (exception: IllegalArgumentException) { } catch (exception: IllegalArgumentException) {
Timber.d(exception) println(exception.stackTraceToString())
} }
} }
binding.mapView.overlays[2] = footprintOverlay binding.mapView.overlays[2] = footprintOverlay
} }
private fun renderSatData(mapData: MapData, binding: FragmentMapBinding) { private fun handleMapData(mapData: MapData) {
binding.apply { binding.apply {
mapDataName.text = mapTimer.text = mapData.aosTime
String.format(getString(R.string.pat_osm_idName), mapData.catNum, mapData.name) mapDataPeriod.text = String.format(getString(R.string.map_period, mapData.period))
mapDataQth.text = String.format(getString(R.string.map_qth), mapData.qthLoc) mapDataPhase.text = String.format(getString(R.string.map_phase), mapData.phase)
mapDataAlt.text = String.format(getString(R.string.pat_altitude), mapData.altitude) mapAzimuth.text = String.format(getString(R.string.map_azimuth), mapData.azimuth)
mapDataDst.text = String.format(getString(R.string.pat_distance), mapData.range) mapElevation.text = String.format(getString(R.string.map_elevation), mapData.elevation)
mapDataVel.text = String.format(getString(R.string.pat_osm_vel), mapData.velocity) mapDataAlt.text = String.format(getString(R.string.map_altitude), mapData.altitude)
mapDataDst.text = String.format(getString(R.string.map_distance), mapData.range)
mapDataLat.text = mapDataLat.text =
String.format(getString(R.string.pat_osm_lat), mapData.osmPos.latitude) String.format(getString(R.string.map_latitude), mapData.osmPos.lat)
mapDataLon.text = mapDataLon.text =
String.format(getString(R.string.pat_osm_lon), mapData.osmPos.longitude) String.format(getString(R.string.map_longitude), mapData.osmPos.lon)
mapDataQth.text = String.format(getString(R.string.map_qth), mapData.qthLoc)
mapDataVel.text = String.format(getString(R.string.map_velocity), mapData.velocity)
if (mapData.eclipsed) {
mapDataVisibility.text = getString(R.string.map_eclipsed)
} else {
mapDataVisibility.text = getString(R.string.map_visible)
}
} }
binding.mapView.invalidate() binding.mapView.invalidate()
} }
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -18,12 +18,15 @@
package com.rtbishop.look4sat.presentation.mapScreen package com.rtbishop.look4sat.presentation.mapScreen
import androidx.lifecycle.* import androidx.lifecycle.*
import com.rtbishop.look4sat.domain.predict.Predictor import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.DataRepository import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.framework.PreferencesSource import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.predict.GeoPos import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.QthConverter import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.utility.QthConverter
import com.rtbishop.look4sat.utility.toDegrees
import com.rtbishop.look4sat.utility.toTimerString
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.* import java.util.*
@@ -33,78 +36,78 @@ import kotlin.math.min
@HiltViewModel @HiltViewModel
class MapViewModel @Inject constructor( class MapViewModel @Inject constructor(
private val dataRepository: DataRepository, private val satelliteManager: ISatelliteManager,
private val predictor: Predictor, private val repository: IDataRepository,
private val preferences: PreferencesSource, private val settings: ISettingsManager,
) : ViewModel() { ) : ViewModel() {
private val stationPos = preferences.loadStationPosition() private val stationPosition = settings.loadStationPosition()
private var allPasses = satelliteManager.getPasses()
private var allSatellites = listOf<Satellite>()
private var dataUpdateJob: Job? = null private var dataUpdateJob: Job? = null
private var allSatList = listOf<Satellite>() private var dataUpdateRate = 1000L
private lateinit var selectedSat: Satellite private var selectedSatellite: Satellite? = null
val stationPosLiveData = liveData { val stationPos = liveData {
val osmLat = clipLat(stationPos.latitude) val osmLat = clipLat(stationPosition.lat)
val osmLon = clipLon(stationPos.longitude) val osmLon = clipLon(stationPosition.lon)
emit(GeoPos(osmLat, osmLon)) emit(GeoPos(osmLat, osmLon))
} }
private val _satTrack = MutableLiveData<List<List<GeoPos>>>() private val _track = MutableLiveData<List<List<GeoPos>>>()
val satTrack: LiveData<List<List<GeoPos>>> = _satTrack val track: LiveData<List<List<GeoPos>>> = _track
private val _satFootprint = MutableLiveData<List<GeoPos>>() private val _footprint = MutableLiveData<SatPos>()
val satFootprint: LiveData<List<GeoPos>> = _satFootprint val footprint: LiveData<SatPos> = _footprint
private val _satData = MutableLiveData<MapData>() private val _mapData = MutableLiveData<MapData>()
val mapData: LiveData<MapData> = this._satData val mapData: LiveData<MapData> = this._mapData
private val _satPositions = MutableLiveData<Map<Satellite, GeoPos>>() private val _positions = MutableLiveData<Map<Satellite, GeoPos>>()
val satPositions: LiveData<Map<Satellite, GeoPos>> = _satPositions val positions: LiveData<Map<Satellite, GeoPos>> = _positions
fun shouldUseTextLabels(): Boolean {
return preferences.shouldUseTextLabels()
}
fun scrollSelection(decrement: Boolean) { fun scrollSelection(decrement: Boolean) {
if (allSatList.isNotEmpty()) { if (allSatellites.isNotEmpty()) {
val index = allSatList.indexOf(selectedSat) val index = allSatellites.indexOf(selectedSatellite)
if (decrement) { if (decrement) {
if (index > 0) selectSatellite(allSatList[index - 1]) if (index > 0) selectSatellite(allSatellites[index - 1])
else selectSatellite(allSatList[allSatList.size - 1]) else selectSatellite(allSatellites[allSatellites.size - 1])
} else { } else {
if (index < allSatList.size - 1) selectSatellite(allSatList[index + 1]) if (index < allSatellites.size - 1) selectSatellite(allSatellites[index + 1])
else selectSatellite(allSatList[0]) else selectSatellite(allSatellites[0])
} }
} }
} }
fun selectDefaultSatellite(catnum: Int?) { fun selectDefaultSatellite(catnum: Int) {
viewModelScope.launch { viewModelScope.launch {
dataRepository.getSelectedSatellites().also { satellites -> val selectedIds = settings.loadEntriesSelection()
repository.getEntriesWithIds(selectedIds).also { satellites ->
if (satellites.isNotEmpty()) { if (satellites.isNotEmpty()) {
allSatList = satellites allSatellites = satellites
if (catnum == null) { if (catnum == -1) {
selectSatellite(satellites.first()) allPasses.find { pass -> pass.progress < 100 && !pass.isDeepSpace }
?.let { pass -> selectSatellite(pass.satellite) }
} else { } else {
satellites.find { it.params.catnum == catnum }?.let { selectSatellite(it) } satellites.find { it.data.catnum == catnum }?.let { selectSatellite(it) }
} }
} }
} }
} }
} }
fun selectSatellite(satellite: Satellite, updateFreq: Long = 2000) { fun selectSatellite(satellite: Satellite, updateFreq: Long = dataUpdateRate) {
selectedSat = satellite selectedSatellite = satellite
viewModelScope.launch { viewModelScope.launch {
dataUpdateJob?.cancelAndJoin() dataUpdateJob?.cancelAndJoin()
dataUpdateJob = launch { dataUpdateJob = launch {
val dateNow = Date() val dateNow = Date()
getSatTrack(selectedSat, stationPos, dateNow) getSatTrack(satellite, stationPosition, dateNow)
while (isActive) { while (isActive) {
dateNow.time = System.currentTimeMillis() dateNow.time = System.currentTimeMillis()
getPositions(allSatList, stationPos, dateNow) getPositions(allSatellites, stationPosition, dateNow)
getSatFootprint(selectedSat, stationPos, dateNow) getSatFootprint(satellite, stationPosition, dateNow)
getSatData(selectedSat, stationPos, dateNow) getSatData(satellite, stationPosition, dateNow)
delay(updateFreq) delay(updateFreq)
} }
} }
@@ -114,64 +117,77 @@ class MapViewModel @Inject constructor(
private suspend fun getSatTrack(satellite: Satellite, pos: GeoPos, date: Date) { private suspend fun getSatTrack(satellite: Satellite, pos: GeoPos, date: Date) {
val satTracks = mutableListOf<List<GeoPos>>() val satTracks = mutableListOf<List<GeoPos>>()
val currentTrack = mutableListOf<GeoPos>() val currentTrack = mutableListOf<GeoPos>()
val endDate = Date(date.time + (satellite.orbitalPeriod * 2.4 * 60000L).toLong()) val endDate = Date(date.time + (satellite.data.orbitalPeriod * 2.4 * 60000L).toLong())
var oldLongitude = 0.0 var oldLongitude = 0.0
predictor.getSatTrack(satellite, pos, date.time, endDate.time).forEach { satPos -> satelliteManager.getTrack(satellite, pos, date.time, endDate.time).forEach { satPos ->
val osmLat = clipLat(Math.toDegrees(satPos.latitude)) val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(Math.toDegrees(satPos.longitude)) val osmLon = clipLon(satPos.longitude.toDegrees())
val currentPosition = GeoPos(osmLat, osmLon) val currentPosition = GeoPos(osmLat, osmLon)
if (oldLongitude < -170.0 && currentPosition.longitude > 170.0) { if (oldLongitude < -170.0 && currentPosition.lon > 170.0) {
// adding left terminal position // adding left terminal position
currentTrack.add(GeoPos(osmLat, -180.0)) currentTrack.add(GeoPos(osmLat, -180.0))
val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) } val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) }
satTracks.add(finishedTrack) satTracks.add(finishedTrack)
currentTrack.clear() currentTrack.clear()
} else if (oldLongitude > 170.0 && currentPosition.longitude < -170.0) { } else if (oldLongitude > 170.0 && currentPosition.lon < -170.0) {
// adding right terminal position // adding right terminal position
currentTrack.add(GeoPos(osmLat, 180.0)) currentTrack.add(GeoPos(osmLat, 180.0))
val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) } val finishedTrack = mutableListOf<GeoPos>().apply { addAll(currentTrack) }
satTracks.add(finishedTrack) satTracks.add(finishedTrack)
currentTrack.clear() currentTrack.clear()
} }
oldLongitude = currentPosition.longitude oldLongitude = currentPosition.lon
currentTrack.add(currentPosition) currentTrack.add(currentPosition)
} }
satTracks.add(currentTrack) satTracks.add(currentTrack)
_satTrack.postValue(satTracks) _track.postValue(satTracks)
} }
private suspend fun getPositions(satellites: List<Satellite>, pos: GeoPos, date: Date) { private suspend fun getPositions(satellites: List<Satellite>, pos: GeoPos, date: Date) {
val positions = mutableMapOf<Satellite, GeoPos>() val positions = mutableMapOf<Satellite, GeoPos>()
satellites.forEach { satellite -> satellites.forEach { satellite ->
val satPos = predictor.getSatPos(satellite, pos, date.time) val satPos = satelliteManager.getPosition(satellite, pos, date.time)
val osmLat = clipLat(Math.toDegrees(satPos.latitude)) val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(Math.toDegrees(satPos.longitude)) val osmLon = clipLon(satPos.longitude.toDegrees())
positions[satellite] = GeoPos(osmLat, osmLon) positions[satellite] = GeoPos(osmLat, osmLon)
} }
_satPositions.postValue(positions) _positions.postValue(positions)
} }
private suspend fun getSatFootprint(satellite: Satellite, pos: GeoPos, date: Date) { private suspend fun getSatFootprint(satellite: Satellite, pos: GeoPos, date: Date) {
val satPos = predictor.getSatPos(satellite, pos, date.time) val satPos = satelliteManager.getPosition(satellite, pos, date.time)
val satFootprint = satPos.getRangeCircle().map { rangePos -> _footprint.postValue(satPos)
val osmLat = clipLat(rangePos.latitude)
val osmLon = clipLon(rangePos.longitude)
GeoPos(osmLat, osmLon)
}
_satFootprint.postValue(satFootprint)
} }
private suspend fun getSatData(satellite: Satellite, pos: GeoPos, date: Date) { private suspend fun getSatData(sat: Satellite, pos: GeoPos, date: Date) {
val satPos = predictor.getSatPos(satellite, pos, date.time) var aosTime = 0L.toTimerString()
val osmLat = clipLat(Math.toDegrees(satPos.latitude)) allPasses.find { pass -> pass.catNum == sat.data.catnum && pass.progress < 100 }
val osmLon = clipLon(Math.toDegrees(satPos.longitude)) ?.let { satPass ->
if (!satPass.isDeepSpace) {
aosTime = if (date.time < satPass.aosTime) {
val millisBeforeStart = satPass.aosTime.minus(date.time)
millisBeforeStart.toTimerString()
} else {
val millisBeforeEnd = satPass.losTime.minus(date.time)
millisBeforeEnd.toTimerString()
}
}
}
val satPos = satelliteManager.getPosition(sat, pos, date.time)
val azimuth = satPos.azimuth.toDegrees()
val elevation = satPos.elevation.toDegrees()
val osmLat = clipLat(satPos.latitude.toDegrees())
val osmLon = clipLon(satPos.longitude.toDegrees())
val osmPos = GeoPos(osmLat, osmLon) val osmPos = GeoPos(osmLat, osmLon)
val qthLoc = QthConverter.positionToQth(osmPos.latitude, osmPos.longitude) ?: "-- --" val qthLoc = QthConverter.positionToQth(osmPos.lat, osmPos.lon) ?: "-- --"
val velocity = satPos.getOrbitalVelocity()
val phase = satPos.phase.toDegrees()
val visibility = satPos.eclipsed
val satData = MapData( val satData = MapData(
satellite, satellite.params.catnum, satellite.params.name, satPos.range, sat.data.catnum, sat.data.name, aosTime, azimuth, elevation, satPos.distance,
satPos.altitude, satPos.getOrbitalVelocity(), qthLoc, osmPos satPos.altitude, velocity, qthLoc, osmPos, sat.data.orbitalPeriod, phase, visibility
) )
_satData.postValue(satData) _mapData.postValue(satData)
} }
private fun clipLat(latitude: Double): Double { private fun clipLat(latitude: Double): Double {
@@ -0,0 +1,70 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.passesScreen
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.DialogFilterBinding
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class FilterDialog : AppCompatDialogFragment() {
@Inject
lateinit var preferences: ISettingsManager
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
return inflater.inflate(R.layout.dialog_filter, group, false)
}
override fun onViewCreated(view: View, state: Bundle?) {
super.onViewCreated(view, state)
DialogFilterBinding.bind(view).apply {
dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
dialog?.window?.setLayout(
(resources.displayMetrics.widthPixels * 0.94).toInt(),
WindowManager.LayoutParams.WRAP_CONTENT
)
filterHoursEdit.setText(preferences.getHoursAhead().toString())
filterElevEdit.setText(preferences.getMinElevation().toString())
filterBtnPos.setOnClickListener {
val hoursAhead = try {
filterHoursEdit.text.toString().toInt()
} catch (exception: Exception) {
24
}
val minElevation = try {
filterElevEdit.text.toString().toDouble()
} catch (exception: Exception) {
16.0
}
setNavResult("prefs", Pair(hoursAhead, minElevation))
dismiss()
}
filterBtnNeg.setOnClickListener { dismiss() }
}
}
}
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -23,13 +23,12 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.ItemPassGeoBinding import com.rtbishop.look4sat.databinding.ItemPassBinding
import com.rtbishop.look4sat.databinding.ItemPassLeoBinding
import com.rtbishop.look4sat.domain.predict.SatPass import com.rtbishop.look4sat.domain.predict.SatPass
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
class PassesAdapter(private val isUTC: Boolean, private val clickListener: PassesClickListener) : class PassesAdapter(private val clickListener: PassesClickListener) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() { RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val diffCallback = object : DiffUtil.ItemCallback<SatPass>() { private val diffCallback = object : DiffUtil.ItemCallback<SatPass>() {
@@ -42,6 +41,7 @@ class PassesAdapter(private val isUTC: Boolean, private val clickListener: Passe
} }
} }
private val differ = AsyncListDiffer(this, diffCallback) private val differ = AsyncListDiffer(this, diffCallback)
private var isUTC: Boolean = false
interface PassesClickListener { interface PassesClickListener {
fun navigateToPass(satPass: SatPass) fun navigateToPass(satPass: SatPass)
@@ -51,58 +51,62 @@ class PassesAdapter(private val isUTC: Boolean, private val clickListener: Passe
differ.submitList(passes) differ.submitList(passes)
} }
override fun getItemCount() = differ.currentList.size fun setUTC(isUTC: Boolean) {
this.isUTC = isUTC
override fun getItemViewType(position: Int): Int {
return if (differ.currentList[position].isDeepSpace) 1
else 0
} }
override fun getItemCount() = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == 0) { return PassHolder.from(parent)
SatPassLeoHolder.from(parent)
} else {
SatPassGeoHolder.from(parent)
}
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder.itemViewType == 0) { (holder as PassHolder).bind(differ.currentList[position], clickListener, isUTC)
(holder as SatPassLeoHolder).bind(differ.currentList[position], clickListener, isUTC)
} else {
(holder as SatPassGeoHolder).bind(differ.currentList[position], clickListener)
}
} }
class SatPassLeoHolder private constructor(private val binding: ItemPassLeoBinding) : class PassHolder private constructor(private val binding: ItemPassBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
private val satIdFormat = itemView.context.getString(R.string.pass_satId) private val satIdFormat = itemView.context.getString(R.string.pass_satId)
private val tcaAzFormat = itemView.context.getString(R.string.pass_tcaAz)
private val aosAzFormat = itemView.context.getString(R.string.pass_aosAz) private val aosAzFormat = itemView.context.getString(R.string.pass_aosAz)
private val maxElFormat = itemView.context.getString(R.string.pass_maxEl) private val altFormat = itemView.context.getString(R.string.pass_altitude)
private val losAzFormat = itemView.context.getString(R.string.pass_losAz) private val losAzFormat = itemView.context.getString(R.string.pass_los)
private val startTimeFormat = itemView.context.getString(R.string.pass_startTime) private val startTimeFormat = itemView.context.getString(R.string.pass_startTime)
private val elevFormat = itemView.context.getString(R.string.pass_elevation)
private val endTimeFormat = itemView.context.getString(R.string.pass_endTime) private val endTimeFormat = itemView.context.getString(R.string.pass_endTime)
private val placeholder = itemView.context.getString(R.string.pass_placeholder)
private val timeZoneUTC = TimeZone.getTimeZone("UTC") private val timeZoneUTC = TimeZone.getTimeZone("UTC")
private val startFormat = SimpleDateFormat(startTimeFormat, Locale.getDefault()) private val startFormat = SimpleDateFormat(startTimeFormat, Locale.getDefault())
private val endFormat = SimpleDateFormat(endTimeFormat, Locale.getDefault()) private val endFormat = SimpleDateFormat(endTimeFormat, Locale.getDefault())
fun bind(satPass: SatPass, listener: PassesClickListener, shouldUseUTC: Boolean) { fun bind(satPass: SatPass, listener: PassesClickListener, shouldUseUTC: Boolean) {
binding.apply { binding.apply {
if (shouldUseUTC) { if (satPass.isDeepSpace) {
startFormat.timeZone = timeZoneUTC passName.text = satPass.name
endFormat.timeZone = timeZoneUTC passId.text = String.format(satIdFormat, satPass.catNum)
passAos.text = String.format(aosAzFormat, satPass.aosAzimuth)
passAltitude.text = String.format(altFormat, satPass.altitude)
passLos.text = String.format(losAzFormat, satPass.losAzimuth)
passStart.text = placeholder
passElev.text = String.format(elevFormat, satPass.maxElevation)
passEnd.text = placeholder
passProgress.progress = 100
} else {
if (shouldUseUTC) {
startFormat.timeZone = timeZoneUTC
endFormat.timeZone = timeZoneUTC
}
passName.text = satPass.name
passId.text = String.format(satIdFormat, satPass.catNum)
passAos.text = String.format(aosAzFormat, satPass.aosAzimuth)
passAltitude.text = String.format(altFormat, satPass.altitude)
passLos.text = String.format(losAzFormat, satPass.losAzimuth)
passStart.text = startFormat.format(Date(satPass.aosTime))
passElev.text = String.format(elevFormat, satPass.maxElevation)
passEnd.text = endFormat.format(Date(satPass.losTime))
passProgress.progress = satPass.progress
} }
passLeoName.text = satPass.name
passLeoId.text = String.format(satIdFormat, satPass.catNum)
passLeoAos.text = String.format(aosAzFormat, satPass.aosAzimuth)
passLeoElev.text = String.format(maxElFormat, satPass.maxElevation)
passLeoLos.text = String.format(losAzFormat, satPass.losAzimuth)
passLeoStart.text = startFormat.format(Date(satPass.aosTime))
passLeoTca.text = String.format(tcaAzFormat, satPass.tcaAzimuth)
passLeoEnd.text = endFormat.format(Date(satPass.losTime))
passLeoProgress.progress = satPass.progress
} }
itemView.setOnClickListener { itemView.setOnClickListener {
@@ -111,39 +115,9 @@ class PassesAdapter(private val isUTC: Boolean, private val clickListener: Passe
} }
companion object { companion object {
fun from(parent: ViewGroup): SatPassLeoHolder { fun from(parent: ViewGroup): PassHolder {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return SatPassLeoHolder(ItemPassLeoBinding.inflate(inflater, parent, false)) return PassHolder(ItemPassBinding.inflate(inflater, parent, false))
}
}
}
class SatPassGeoHolder private constructor(private val binding: ItemPassGeoBinding) :
RecyclerView.ViewHolder(binding.root) {
private val satIdFormat = itemView.context.getString(R.string.pass_satId)
private val azFormat = itemView.context.getString(R.string.pat_azimuth)
private val altFormat = itemView.context.getString(R.string.pat_altitude)
private val elevFormat = itemView.context.getString(R.string.pat_elevation)
fun bind(satPass: SatPass, listener: PassesClickListener) {
binding.apply {
passGeoName.text = satPass.name
passGeoId.text = String.format(satIdFormat, satPass.catNum)
passGeoAz.text = String.format(azFormat, satPass.tcaAzimuth)
passGeoAlt.text = String.format(altFormat, satPass.altitude)
passGeoEl.text = String.format(elevFormat, satPass.maxElevation)
}
itemView.setOnClickListener {
listener.navigateToPass(satPass)
}
}
companion object {
fun from(parent: ViewGroup): SatPassGeoHolder {
val inflater = LayoutInflater.from(parent.context)
return SatPassGeoHolder(ItemPassGeoBinding.inflate(inflater, parent, false))
} }
} }
} }
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -17,121 +17,151 @@
*/ */
package com.rtbishop.look4sat.presentation.passesScreen package com.rtbishop.look4sat.presentation.passesScreen
import android.Manifest
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.activity.result.contract.ActivityResultContracts import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentPassesBinding import com.rtbishop.look4sat.databinding.FragmentPassesBinding
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.presentation.ItemDivider import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.presentation.navigateSafe import com.rtbishop.look4sat.presentation.clickWithDebounce
import com.rtbishop.look4sat.presentation.toTimerString import com.rtbishop.look4sat.presentation.getNavResult
import com.rtbishop.look4sat.utility.toTimerString
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.util.*
@AndroidEntryPoint @AndroidEntryPoint
class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesClickListener { class PassesFragment : Fragment(R.layout.fragment_passes), PassesAdapter.PassesClickListener {
private val passesViewModel: PassesViewModel by viewModels() private val viewModel: PassesViewModel by viewModels()
private val permReqContract = ActivityResultContracts.RequestMultiplePermissions() private val passesAdapter = PassesAdapter(this)
private val locPermFine = Manifest.permission.ACCESS_FINE_LOCATION private var binding: FragmentPassesBinding? = null
private val locPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
private val locPermReq = registerForActivityResult(permReqContract) { permissions ->
when {
permissions[locPermFine] == true -> passesViewModel.triggerInitialSetup()
permissions[locPermCoarse] == true -> passesViewModel.triggerInitialSetup()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupComponents(view) binding = FragmentPassesBinding.bind(view).apply {
} passesAdapter.setUTC(viewModel.shouldUseUTC())
private fun setupComponents(view: View) {
val passesAdapter = PassesAdapter(passesViewModel.shouldUseUTC(), this)
val binding = FragmentPassesBinding.bind(view).apply {
passesRecycler.apply { passesRecycler.apply {
setHasFixedSize(true) setHasFixedSize(true)
adapter = passesAdapter adapter = passesAdapter
isVerticalScrollBarEnabled = false layoutManager = LinearLayoutManager(requireContext())
layoutManager = LinearLayoutManager(context)
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
addItemDecoration(ItemDivider(R.drawable.rec_divider_dark)) addItemDecoration(DividerItemDecoration(requireContext(), 1))
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0 && passesFab.visibility == View.VISIBLE) passesFab.hide()
else if (dy < 0 && passesFab.visibility != View.VISIBLE) passesFab.show()
}
})
} }
passesRefresh.setOnClickListener { passesViewModel.forceCalculation() } passesBtnRefresh.clickWithDebounce { viewModel.calculatePasses() }
} passesBtnMap.clickWithDebounce {
passesViewModel.passes.observe(viewLifecycleOwner, { passesResult -> val dir = PassesFragmentDirections.globalToMap()
handleNewPasses(passesResult, passesAdapter, binding) findNavController().navigate(dir)
})
passesViewModel.isFirstLaunchDone.observe(viewLifecycleOwner, { setupDone ->
if (!setupDone) locPermReq.launch(arrayOf(locPermFine, locPermCoarse))
})
}
private fun handleNewPasses(
dataState: DataState<List<SatPass>>,
passesAdapter: PassesAdapter,
binding: FragmentPassesBinding
) {
when (dataState) {
is DataState.Success -> {
passesAdapter.submitList(dataState.data)
binding.apply {
passesError.visibility = View.INVISIBLE
passesProgress.visibility = View.INVISIBLE
passesRecycler.visibility = View.VISIBLE
}
tickMainTimer(dataState.data, binding)
} }
is DataState.Loading -> { passesBtnFilter.clickWithDebounce {
binding.apply { val dir = PassesFragmentDirections.passesToFilter()
passesTimer.text = 0L.toTimerString() findNavController().navigate(dir)
passesError.visibility = View.INVISIBLE
passesRecycler.visibility = View.INVISIBLE
passesProgress.visibility = View.VISIBLE
}
} }
else -> { passesBtnSettings.clickWithDebounce {
binding.apply { val dir = PassesFragmentDirections.globalToSettings()
passesTimer.text = 0L.toTimerString() findNavController().navigate(dir)
passesProgress.visibility = View.INVISIBLE
passesRecycler.visibility = View.INVISIBLE
passesError.visibility = View.VISIBLE
}
} }
} setupObservers()
}
private fun tickMainTimer(passes: List<SatPass>, binding: FragmentPassesBinding) {
if (passes.isNotEmpty()) {
val timeNow = System.currentTimeMillis()
try {
val nextPass = passes.first { it.aosTime.minus(timeNow) > 0 }
val millisBeforeStart = nextPass.aosTime.minus(timeNow)
binding.passesTimer.text = millisBeforeStart.toTimerString()
} catch (e: NoSuchElementException) {
val lastPass = passes.last()
val millisBeforeEnd = lastPass.losTime.minus(timeNow)
binding.passesTimer.text = millisBeforeEnd.toTimerString()
}
} else {
binding.passesTimer.text = 0L.toTimerString()
} }
} }
override fun navigateToPass(satPass: SatPass) { override fun navigateToPass(satPass: SatPass) {
if (satPass.progress < 100) { if (satPass.progress < 100) satPass.run {
val bundle = bundleOf("catNum" to satPass.catNum, "aosTime" to satPass.aosTime) val dir = PassesFragmentDirections.globalToRadar(this.catNum, this.aosTime)
findNavController().navigateSafe(R.id.action_passes_to_radar, bundle) findNavController().navigate(dir)
}
}
override fun onDestroyView() {
binding?.passesRecycler?.adapter = null
binding = null
super.onDestroyView()
}
private fun setupObservers() {
viewModel.entriesTotal.observe(viewLifecycleOwner) { number ->
handleEntriesTotal(number)
}
viewModel.passes.observe(viewLifecycleOwner) { passesResult ->
handleNewPasses(passesResult)
}
getNavResult<Pair<Int, Double>>(R.id.nav_passes, "prefs") { prefs ->
viewModel.calculatePasses(prefs.first, prefs.second)
}
getNavResult<List<Int>>(R.id.nav_passes, "selection") { items ->
viewModel.calculatePasses(selection = items)
}
}
private fun handleEntriesTotal(number: Int) {
binding?.run {
if (number > 0) {
passesFab.clickWithDebounce {
val direction = PassesFragmentDirections.globalToEntries()
findNavController().navigate(direction)
}
} else {
passesFab.clickWithDebounce {
val errorMessage = getString(R.string.passes_empty_db)
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun handleNewPasses(state: DataState<List<SatPass>>) {
binding?.run {
when (state) {
is DataState.Success -> {
passesAdapter.submitList(state.data)
tickMainTimer(state.data)
if (state.data.isNotEmpty()) { // show new passes list
passesEmpty.visibility = View.INVISIBLE
passesProgress.visibility = View.INVISIBLE
} else { // show no passes message
passesEmpty.visibility = View.VISIBLE
passesProgress.visibility = View.INVISIBLE
}
}
is DataState.Loading -> {
passesEmpty.visibility = View.INVISIBLE
passesProgress.visibility = View.VISIBLE
passesTimer.text = 0L.toTimerString()
}
else -> {}
}
}
}
private fun tickMainTimer(passes: List<SatPass>) {
binding?.run {
if (passes.isNotEmpty()) {
val timeNow = System.currentTimeMillis()
try {
val nextPass = passes.first { it.aosTime.minus(timeNow) > 0 }
val millisBeforeStart = nextPass.aosTime.minus(timeNow)
passesTimer.text = millisBeforeStart.toTimerString()
} catch (e: NoSuchElementException) {
val lastPass = passes.last()
val millisBeforeEnd = lastPass.losTime.minus(timeNow)
passesTimer.text = millisBeforeEnd.toTimerString()
}
} else {
passesTimer.text = 0L.toTimerString()
}
passesBtnRefresh.isEnabled = true
} }
} }
} }
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -17,106 +17,68 @@
*/ */
package com.rtbishop.look4sat.presentation.passesScreen package com.rtbishop.look4sat.presentation.passesScreen
import androidx.lifecycle.LiveData import androidx.lifecycle.*
import androidx.lifecycle.MutableLiveData import com.rtbishop.look4sat.domain.IDataRepository
import androidx.lifecycle.ViewModel import com.rtbishop.look4sat.domain.ISatelliteManager
import androidx.lifecycle.viewModelScope import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.predict.Predictor
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.model.DataState import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.DataRepository import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.framework.PreferencesSource
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
import java.util.*
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class PassesViewModel @Inject constructor( class PassesViewModel @Inject constructor(
private val dataRepository: DataRepository, private val satelliteManager: ISatelliteManager,
private val predictor: Predictor, private val repository: IDataRepository,
private val preferences: PreferencesSource private val settings: ISettingsManager
) : ViewModel() { ) : ViewModel() {
private val _passes = MutableLiveData<DataState<List<SatPass>>>(DataState.Loading)
private val _isFirstLaunchDone = MutableLiveData<Boolean>()
private var passesProcessing: Job? = null private var passesProcessing: Job? = null
private val _passes = MutableLiveData<DataState<List<SatPass>>>(DataState.Loading)
val passes: LiveData<DataState<List<SatPass>>> = _passes val passes: LiveData<DataState<List<SatPass>>> = _passes
val isFirstLaunchDone: LiveData<Boolean> = _isFirstLaunchDone val entriesTotal: LiveData<Int> = repository.getEntriesTotal().asLiveData()
init { init {
if (preferences.isSetupDone()) {
viewModelScope.launch {
_passes.postValue(DataState.Loading)
val timeNow = System.currentTimeMillis()
val satellites = dataRepository.getSelectedSatellites()
val stationPos = preferences.loadStationPosition()
val hoursAhead = preferences.getHoursAhead()
val minElev = preferences.getMinElevation()
predictor.triggerCalculation(satellites, stationPos, timeNow, hoursAhead, minElev)
}
} else {
_isFirstLaunchDone.value = false
}
viewModelScope.launch { viewModelScope.launch {
predictor.passes.collect { passes -> satelliteManager.calculatedPasses.collect { passes ->
passesProcessing?.cancelAndJoin() passesProcessing?.cancelAndJoin()
passesProcessing = viewModelScope.launch { tickPasses(passes) } passesProcessing = viewModelScope.launch {
} while (isActive) {
} val time = System.currentTimeMillis()
} val newPasses = satelliteManager.processPasses(passes, time)
_passes.postValue(DataState.Success(newPasses))
fun triggerInitialSetup() { delay(1000)
preferences.updatePositionFromGPS()
viewModelScope.launch {
_passes.postValue(DataState.Loading)
val satellites = dataRepository.getSelectedSatellites()
val stationPos = preferences.loadStationPosition()
val hoursAhead = preferences.getHoursAhead()
val minElev = preferences.getMinElevation()
dataRepository.updateDataFromWeb()
dataRepository.updateSelection(isSelected = true)
predictor.forceCalculation(satellites, stationPos, Date().time, hoursAhead, minElev)
preferences.setSetupDone()
_isFirstLaunchDone.value = true
}
}
fun forceCalculation() {
viewModelScope.launch {
_passes.postValue(DataState.Loading)
passesProcessing?.cancelAndJoin()
val timeNow = System.currentTimeMillis()
val satellites = dataRepository.getSelectedSatellites()
val stationPos = preferences.loadStationPosition()
val hoursAhead = preferences.getHoursAhead()
val minElev = preferences.getMinElevation()
predictor.forceCalculation(satellites, stationPos, timeNow, hoursAhead, minElev)
}
}
fun shouldUseUTC(): Boolean {
return preferences.shouldUseUTC()
}
private suspend fun tickPasses(passes: List<SatPass>) = withContext(Dispatchers.Default) {
var currentPasses = passes
while (isActive) {
val timeNow = System.currentTimeMillis()
currentPasses.forEach { pass ->
if (!pass.isDeepSpace) {
val timeStart = pass.aosTime
if (timeNow > timeStart) {
val deltaNow = timeNow.minus(timeStart).toFloat()
val deltaTotal = pass.losTime.minus(timeStart).toFloat()
pass.progress = ((deltaNow / deltaTotal) * 100).toInt()
} }
} }
} }
currentPasses = currentPasses.filter { it.progress < 100 } }
_passes.postValue(DataState.Success(currentPasses.map { it.copy() })) viewModelScope.launch {
delay(1000) repository.updateState.collect { updateState ->
if (updateState is DataState.Success) calculatePasses()
}
}
calculatePasses()
}
fun shouldUseUTC() = settings.getUseUTC()
fun calculatePasses(
hoursAhead: Int = settings.getHoursAhead(),
minElevation: Double = settings.getMinElevation(),
timeRef: Long = System.currentTimeMillis(),
selection: List<Int>? = null
) {
viewModelScope.launch {
_passes.postValue(DataState.Loading)
passesProcessing?.cancelAndJoin()
selection?.let { items -> settings.saveEntriesSelection(items) }
settings.setHoursAhead(hoursAhead)
settings.setMinElevation(minElevation)
val stationPos = settings.loadStationPosition()
val selectedIds = settings.loadEntriesSelection()
val satellites = repository.getEntriesWithIds(selectedIds)
satelliteManager.calculatePasses(satellites, stationPos, timeRef, hoursAhead, minElevation)
} }
} }
} }
@@ -0,0 +1,82 @@
package com.rtbishop.look4sat.presentation.radarScreen
import android.bluetooth.BluetoothManager
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.OutputStream
import java.util.*
import kotlin.math.abs
class BTReporter(private val manager: BluetoothManager, private val reporterScope: CoroutineScope) {
private val tag = "BTReporter"
private val sppid: UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")
private lateinit var outputStream: OutputStream
private var rotationConnectionJob: Job? = null
private var rotationReportingJob: Job? = null
private var connected = false
private var connecting = false
fun isConnected(): Boolean = connected
fun isConnecting(): Boolean = connecting
fun connectBTDevice(deviceId: String) {
if (!connected) {
rotationConnectionJob = reporterScope.launch {
try {
manager.adapter.getRemoteDevice(deviceId)?.let { device ->
device.createInsecureRfcommSocketToServiceRecord(sppid)?.let { socket ->
connecting = true
socket.connect()
outputStream = socket.outputStream
connected = true
connecting = false
Log.i(tag, "$tag: Connected!")
}
}
} catch (e: SecurityException) {
Log.e(tag, "$tag: ${e.message}")
} catch (e: Exception) {
Log.e(tag, "$tag: ${e.message}")
}
}
}
}
fun reportRotation(format: String, azimuth: Int, elevation: Int) {
if (connected) {
rotationReportingJob = reporterScope.launch {
val newElevation = if (elevation > 0) elevation else 0
try {
val azimuthString = intToStringWithLeadingZeroes(azimuth)
val elevationString = intToStringWithLeadingZeroes(newElevation)
val crChar = '\r'
val nlChar = '\n'
val tbChar = '\t'
var buffer = format.replace("\$AZ", azimuthString)
buffer = buffer.replace("\$EL", elevationString)
buffer = buffer.replace("\\r", crChar.toString())
buffer = buffer.replace("\\n", nlChar.toString())
buffer = buffer.replace("\\t", tbChar.toString())
Log.i(tag, "$tag: Sending $buffer")
if (connected) outputStream.write(buffer.toByteArray())
} catch (e: Exception) {
Log.e(tag, "$tag: ${e.message}")
connected = false
}
}
}
}
private fun intToStringWithLeadingZeroes(value: Int): String {
return if (value > 0) {
if (value < 10) "00$value" else if (value < 100) "0$value" else "$value"
} else {
val absValue = abs(value)
if (value > -10) "-00$absValue" else if (value > -100) "-0$absValue" else "-$absValue"
}
}
}
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -17,46 +17,46 @@
*/ */
package com.rtbishop.look4sat.presentation.radarScreen package com.rtbishop.look4sat.presentation.radarScreen
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.provider.CalendarContract
import android.view.View import android.view.View
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.FragmentRadarBinding import com.rtbishop.look4sat.databinding.FragmentRadarBinding
import com.rtbishop.look4sat.framework.PreferencesSource
import com.rtbishop.look4sat.domain.predict.SatPass import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.predict.SatPos import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.presentation.ItemDivider import com.rtbishop.look4sat.presentation.clickWithDebounce
import com.rtbishop.look4sat.presentation.navigateSafe import com.rtbishop.look4sat.utility.toDegrees
import com.rtbishop.look4sat.presentation.toTimerString import com.rtbishop.look4sat.utility.toTimerString
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class RadarFragment : Fragment(R.layout.fragment_radar) { class RadarFragment : Fragment(R.layout.fragment_radar) {
@Inject
lateinit var preferences: PreferencesSource
private val viewModel: RadarViewModel by viewModels() private val viewModel: RadarViewModel by viewModels()
private val navArgs: RadarFragmentArgs by navArgs()
private val radioAdapter = RadioAdapter()
private var binding: FragmentRadarBinding? = null
private var radarView: RadarView? = null private var radarView: RadarView? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
FragmentRadarBinding.bind(view).apply { binding = FragmentRadarBinding.bind(view).apply {
val transAdapter = TransmittersAdapter()
radarRecycler.apply { radarRecycler.apply {
setHasFixedSize(true) setHasFixedSize(true)
adapter = transAdapter this.adapter = radioAdapter
isVerticalScrollBarEnabled = false this.layoutManager = LinearLayoutManager(requireContext())
layoutManager = LinearLayoutManager(context)
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
addItemDecoration(ItemDivider(R.drawable.rec_divider_dark)) addItemDecoration(DividerItemDecoration(requireContext(), 1))
} }
setupObservers(transAdapter, this) setupObservers()
} }
} }
@@ -70,98 +70,90 @@ class RadarFragment : Fragment(R.layout.fragment_radar) {
viewModel.disableSensor() viewModel.disableSensor()
} }
private fun setupObservers( override fun onDestroyView() {
transmittersAdapter: TransmittersAdapter, binding?.radarRecycler?.adapter = null
binding: FragmentRadarBinding radarView = null
) { binding = null
val catNum = requireArguments().getInt("catNum") super.onDestroyView()
val aosTime = requireArguments().getLong("aosTime")
viewModel.getPass(catNum, aosTime).observe(viewLifecycleOwner) { pass ->
radarView = RadarView(requireContext()).apply {
setShowAim(preferences.shouldUseCompass())
setScanning(preferences.shouldShowSweep())
}
binding.radarFrame.addView(radarView)
viewModel.radarData.observe(viewLifecycleOwner, { passData ->
radarView?.setPosition(passData.satPos)
radarView?.setPositions(passData.satTrack)
setPassText(pass, passData.satPos, binding)
})
viewModel.transmitters.observe(viewLifecycleOwner, { list ->
if (list.isNotEmpty()) {
transmittersAdapter.submitList(list)
binding.radarRecycler.visibility = View.VISIBLE
binding.radarRecyclerMsg.visibility = View.INVISIBLE
} else {
binding.radarRecycler.visibility = View.INVISIBLE
binding.radarRecyclerMsg.visibility = View.VISIBLE
}
radarView?.invalidate()
})
viewModel.orientation.observe(viewLifecycleOwner, { orientation ->
radarView?.setOrientation(
orientation.first,
orientation.second,
orientation.third
)
})
binding.radarToMapButton.setOnClickListener {
val bundle = bundleOf("catNum" to pass.catNum)
findNavController().navigateSafe(R.id.action_radar_to_map, bundle)
}
}
// binding.radarNotifyButton.setOnClickListener {
// this.context?.let { context ->
//
// val choicesList = arrayOf(
// getString(R.string.notification_on_start_pass),
// getString(R.string.notification_5_min_before),
// getString(R.string.notification_10_min_before),
// getString(R.string.notification_15_min_before),
// )
//
// MaterialAlertDialogBuilder(context)
// .setTitle(getString(R.string.notification_title))
// .setSingleChoiceItems(choicesList, -1) { dialog, which ->
// // TODO in this place should set a notification by NotificationBuilder
// Toast.makeText(context, "Reminder was saved", Toast.LENGTH_SHORT).show()
// dialog.dismiss()
// }
// .setNegativeButton(getString(R.string.btn_cancel)) { _, _ ->
// // noop
// }
// .show()
// }
// }
} }
private fun setPassText(satPass: SatPass, satPos: SatPos, binding: FragmentRadarBinding) { private fun setupObservers() {
val timeNow = System.currentTimeMillis() viewModel.getPass(navArgs.catNum, navArgs.aosTime).observe(viewLifecycleOwner) { pass ->
val polarAz = getString(R.string.pat_azimuth) binding?.run {
val polarEl = getString(R.string.pat_elevation) radarView = RadarView(requireContext()).apply {
val polarRng = getString(R.string.pat_distance) setShowAim(viewModel.getUseCompass())
val polarAlt = getString(R.string.pat_altitude) setScanning(viewModel.getShowSweep())
binding.radarAz.text = String.format(polarAz, Math.toDegrees(satPos.azimuth)) }
binding.radarEl.text = String.format(polarEl, Math.toDegrees(satPos.elevation)) radarCard.addView(radarView)
binding.radarDst.text = String.format(polarRng, satPos.range) viewModel.radarData.observe(viewLifecycleOwner) { passData ->
binding.radarAlt.text = String.format(polarAlt, satPos.altitude) radarView?.setPosition(passData.satPos)
binding.radarName.text = satPass.name radarView?.setPositions(passData.satTrack)
setPassText(pass, passData.satPos)
if (!satPass.isDeepSpace) { }
if (timeNow < satPass.aosTime) { viewModel.transmitters.observe(viewLifecycleOwner) { list ->
val millisBeforeStart = satPass.aosTime.minus(timeNow) if (list.isNotEmpty()) {
binding.radarTimer.text = millisBeforeStart.toTimerString() radioAdapter.submitList(list)
} else { radarProgress.visibility = View.INVISIBLE
val millisBeforeEnd = satPass.losTime.minus(timeNow) } else {
binding.radarTimer.text = millisBeforeEnd.toTimerString() radarProgress.visibility = View.INVISIBLE
if (timeNow > satPass.losTime) { radarEmptyLayout.visibility = View.VISIBLE
binding.radarTimer.text = 0L.toTimerString() }
findNavController().navigateSafe(R.id.action_radar_to_passes) radarView?.invalidate()
}
viewModel.orientation.observe(viewLifecycleOwner) { value ->
radarView?.setOrientation(value.first, value.second, value.third)
}
radarBtnBack.clickWithDebounce { findNavController().navigateUp() }
radarBtnMap.clickWithDebounce {
val direction = RadarFragmentDirections.globalToMap(pass.catNum)
findNavController().navigate(direction)
}
radarBtnNotify.clickWithDebounce {
val intent = Intent(Intent.ACTION_INSERT)
.setData(CalendarContract.Events.CONTENT_URI)
.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, pass.aosTime)
.putExtra(CalendarContract.EXTRA_EVENT_END_TIME, pass.losTime)
.putExtra(CalendarContract.Events.TITLE, pass.name)
.putExtra(CalendarContract.Events.DESCRIPTION, "Look4Sat")
startActivity(intent)
}
radarBtnSettings.clickWithDebounce {
val direction = RadarFragmentDirections.globalToSettings()
findNavController().navigate(direction)
} }
} }
} else { }
binding.radarTimer.text = 0L.toTimerString() }
private fun setPassText(satPass: SatPass, satPos: SatPos) {
binding?.run {
val timeNow = System.currentTimeMillis()
val radarAzim = getString(R.string.radar_az_value)
val radarElev = getString(R.string.radar_el_value)
val radarAlt = getString(R.string.radar_alt_value)
val radarDist = getString(R.string.radar_dist_value)
radarAzValue.text = String.format(radarAzim, satPos.azimuth.toDegrees())
radarElValue.text = String.format(radarElev, satPos.elevation.toDegrees())
radarAltValue.text = String.format(radarAlt, satPos.altitude)
radarDstValue.text = String.format(radarDist, satPos.distance)
if (satPos.eclipsed) {
radarVisibility.text = getText(R.string.radar_eclipsed)
} else {
radarVisibility.text = getText(R.string.radar_visible)
}
if (!satPass.isDeepSpace) {
if (timeNow < satPass.aosTime) {
val millisBeforeStart = satPass.aosTime.minus(timeNow)
radarTimer.text = millisBeforeStart.toTimerString()
} else {
val millisBeforeEnd = satPass.losTime.minus(timeNow)
radarTimer.text = millisBeforeEnd.toTimerString()
if (timeNow > satPass.losTime) {
radarTimer.text = 0L.toTimerString()
findNavController().navigateUp()
}
}
} else radarTimer.text = 0L.toTimerString()
} }
} }
} }
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -22,23 +22,23 @@ import android.graphics.*
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.domain.predict.PI_2
import com.rtbishop.look4sat.domain.predict.SatPos import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.TWO_PI
import com.rtbishop.look4sat.utility.toRadians
import kotlin.math.cos import kotlin.math.cos
import kotlin.math.min import kotlin.math.min
import kotlin.math.sin import kotlin.math.sin
class RadarView(context: Context) : View(context) { class RadarView(context: Context) : View(context) {
private val defaultColor = ContextCompat.getColor(context, R.color.themeLight) private val defaultColor = ContextCompat.getColor(context, R.color.accent)
private val scale = resources.displayMetrics.density private val scale = resources.displayMetrics.density
private val radarWidth = resources.displayMetrics.widthPixels
private val radarRadius = radarWidth * 0.48f
private val piDiv2 = Math.PI / 2.0
private val strokeSize = scale * 2f private val strokeSize = scale * 2f
private var position: SatPos? = null private var position: SatPos? = null
private var positions: List<SatPos> = emptyList() private var positions: List<SatPos> = emptyList()
private var radarColor = ContextCompat.getColor(context, R.color.greyLight) private var radarColor = ContextCompat.getColor(context, R.color.textMain)
private var radarCircleNum = 3 private var radarCircleNum = 3
private var radarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private var radarPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = radarColor color = radarColor
@@ -49,7 +49,7 @@ class RadarView(context: Context) : View(context) {
private var shouldShowSweep = true private var shouldShowSweep = true
private var sweepColor = defaultColor private var sweepColor = defaultColor
private var sweepDegrees = 0f private var sweepDegrees = 0f
private var sweepSpeed = 30.0f private var sweepSpeed = 15.0f
private var sweepPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private var sweepPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = sweepColor color = sweepColor
} }
@@ -79,7 +79,7 @@ class RadarView(context: Context) : View(context) {
private val arrowPath = Path() private val arrowPath = Path()
private var arrowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { private var arrowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = ContextCompat.getColor(context, R.color.themeLight) color = ContextCompat.getColor(context, R.color.accent)
style = Paint.Style.FILL style = Paint.Style.FILL
strokeWidth = strokeSize strokeWidth = strokeSize
} }
@@ -125,17 +125,17 @@ class RadarView(context: Context) : View(context) {
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas) {
val radarWidth = width - paddingLeft - paddingRight val radarWidth = width - paddingLeft - paddingRight
val radarHeight = height - paddingTop - paddingBottom val radarHeight = height - paddingTop - paddingBottom
val radarRadius = min(width, height) * 0.49f val radarRadius = min(width, height) * 0.48f
val cx = paddingLeft + radarWidth / 2f val cx = paddingLeft + radarWidth / 2f
val cy = paddingTop + radarHeight / 2f val cy = paddingTop + radarHeight / 2f
if (positions.isNotEmpty() && !isTrackCreated) { if (positions.isNotEmpty() && !isTrackCreated) {
createPassTrajectory() createPassTrajectory(radarRadius)
createPassTrajectoryArrow() createPassTrajectoryArrow()
isTrackCreated = true isTrackCreated = true
} }
canvas.drawColor(ContextCompat.getColor(context, R.color.greyDark)) canvas.drawColor(ContextCompat.getColor(context, R.color.cardRegular))
drawRadarCircle(canvas, cx, cy, radarRadius) drawRadarCircle(canvas, cx, cy, radarRadius)
drawRadarCross(canvas, cx, cy, radarRadius) drawRadarCross(canvas, cx, cy, radarRadius)
drawRadarText(canvas, cx, radarRadius) drawRadarText(canvas, cx, radarRadius)
@@ -146,10 +146,10 @@ class RadarView(context: Context) : View(context) {
canvas.drawPath(trackPath, arrowPaint) canvas.drawPath(trackPath, arrowPaint)
} }
if (shouldShowBeacons) { if (shouldShowBeacons) {
drawSatellite(canvas) drawSatellite(canvas, radarRadius)
} }
if (shouldShowAim) { if (shouldShowAim) {
drawCrosshair(canvas, azimuth, pitch) drawCrosshair(canvas, azimuth, pitch, radarRadius)
} }
if (shouldShowSweep) { if (shouldShowSweep) {
canvas.translate(-cx, -cy) canvas.translate(-cx, -cy)
@@ -177,7 +177,7 @@ class RadarView(context: Context) : View(context) {
} }
} }
private fun drawSatellite(canvas: Canvas) { private fun drawSatellite(canvas: Canvas, radarRadius: Float) {
position?.let { satPos -> position?.let { satPos ->
if (satPos.elevation > 0) { if (satPos.elevation > 0) {
val satX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) val satX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
@@ -187,9 +187,9 @@ class RadarView(context: Context) : View(context) {
} }
} }
private fun drawCrosshair(canvas: Canvas, azimuth: Float, pitch: Float) { private fun drawCrosshair(canvas: Canvas, azimuth: Float, pitch: Float, radarRadius: Float) {
val azimuthRad = Math.toRadians(azimuth.toDouble()) val azimuthRad = azimuth.toDouble().toRadians()
val tmpElevation = Math.toRadians(pitch.toDouble()) val tmpElevation = pitch.toDouble().toRadians()
val elevationRad = if (tmpElevation > 0.0) 0.0 else tmpElevation val elevationRad = if (tmpElevation > 0.0) 0.0 else tmpElevation
val crossX = sph2CartX(azimuthRad, -elevationRad, radarRadius.toDouble()) val crossX = sph2CartX(azimuthRad, -elevationRad, radarRadius.toDouble())
val crossY = sph2CartY(azimuthRad, -elevationRad, radarRadius.toDouble()) val crossY = sph2CartY(azimuthRad, -elevationRad, radarRadius.toDouble())
@@ -199,16 +199,16 @@ class RadarView(context: Context) : View(context) {
} }
private fun sph2CartX(azimuth: Double, elevation: Double, r: Double): Float { private fun sph2CartX(azimuth: Double, elevation: Double, r: Double): Float {
val radius = r * (piDiv2 - elevation) / piDiv2 val radius = r * (PI_2 - elevation) / PI_2
return (radius * cos(piDiv2 - azimuth)).toFloat() return (radius * cos(PI_2 - azimuth)).toFloat()
} }
private fun sph2CartY(azimuth: Double, elevation: Double, r: Double): Float { private fun sph2CartY(azimuth: Double, elevation: Double, r: Double): Float {
val radius = r * (piDiv2 - elevation) / piDiv2 val radius = r * (PI_2 - elevation) / PI_2
return (radius * sin(piDiv2 - azimuth)).toFloat() return (radius * sin(PI_2 - azimuth)).toFloat()
} }
private fun createPassTrajectory() { private fun createPassTrajectory(radarRadius: Float) {
positions.forEachIndexed { index, satPos -> positions.forEachIndexed { index, satPos ->
val passX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) val passX = sph2CartX(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
val passY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble()) val passY = sph2CartY(satPos.azimuth, satPos.elevation, radarRadius.toDouble())
@@ -223,7 +223,7 @@ class RadarView(context: Context) : View(context) {
private fun createPassTrajectoryArrow() { private fun createPassTrajectoryArrow() {
val radius = beaconSize val radius = beaconSize
val sides = 3 val sides = 3
val angle = 2.0 * Math.PI / sides val angle = TWO_PI / sides
arrowPath.moveTo((radius * cos(angle)).toFloat(), (radius * sin(angle)).toFloat()) arrowPath.moveTo((radius * cos(angle)).toFloat(), (radius * sin(angle)).toFloat())
for (i in 1 until sides) { for (i in 1 until sides) {
val x = (radius * cos(angle - angle * i)).toFloat() val x = (radius * cos(angle - angle * i)).toFloat()
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -17,104 +17,130 @@
*/ */
package com.rtbishop.look4sat.presentation.radarScreen package com.rtbishop.look4sat.presentation.radarScreen
import androidx.lifecycle.* import android.hardware.GeomagneticField
import com.rtbishop.look4sat.domain.DataReporter import android.util.Log
import com.rtbishop.look4sat.domain.DataRepository import androidx.lifecycle.LiveData
import com.rtbishop.look4sat.domain.predict.Predictor import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPass import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.predict.SatPos import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.model.Transmitter import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.framework.OrientationSource import com.rtbishop.look4sat.framework.OrientationManager
import com.rtbishop.look4sat.framework.PreferencesSource import com.rtbishop.look4sat.utility.DataReporter
import com.rtbishop.look4sat.presentation.round import com.rtbishop.look4sat.utility.round
import com.rtbishop.look4sat.utility.toDegrees
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
import javax.inject.Inject
@HiltViewModel @HiltViewModel
class RadarViewModel @Inject constructor( class RadarViewModel @Inject constructor(
private val orientationSource: OrientationSource, private val orientationManager: OrientationManager,
private val preferences: PreferencesSource, private val reporter: DataReporter,
private val predictor: Predictor, private val btReporter: BTReporter,
private val dataRepository: DataRepository, private val satManager: ISatelliteManager,
private val dataReporter: DataReporter private val repository: IDataRepository,
) : ViewModel(), OrientationSource.OrientationListener { private val settings: ISettingsManager
) : ViewModel(), OrientationManager.OrientationListener {
private val stationPos = preferences.loadStationPosition() private val stationPos = settings.loadStationPosition()
private val _passData = MutableLiveData<RadarData>() private val _passData = MutableLiveData<RadarData>()
private val _transmitters = MutableLiveData<List<Transmitter>>() private val _transmitters = MutableLiveData<List<SatRadio>>()
private val _orientation = MutableLiveData<Triple<Float, Float, Float>>() private val _orientation = MutableLiveData<Triple<Float, Float, Float>>()
val radarData: LiveData<RadarData> = _passData val radarData: LiveData<RadarData> = _passData
val transmitters: LiveData<List<Transmitter>> = _transmitters val transmitters: LiveData<List<SatRadio>> = _transmitters
val orientation: LiveData<Triple<Float, Float, Float>> = _orientation val orientation: LiveData<Triple<Float, Float, Float>> = _orientation
fun getPass(catNum: Int, aosTime: Long) = liveData { fun getPass(catNum: Int, aosTime: Long) = liveData {
predictor.passes.collect { passes -> satManager.calculatedPasses.collect { passes ->
val pass = passes.find { it.catNum == catNum && it.aosTime == aosTime } val pass = passes.find { pass -> pass.catNum == catNum && pass.aosTime == aosTime }
pass?.let { satPass -> pass?.let { satPass ->
emit(satPass) emit(satPass)
sendPassData(satPass) val transmitters = repository.getRadiosWithId(satPass.catNum)
processTransmitters(satPass) viewModelScope.launch {
while (isActive) {
val timeNow = System.currentTimeMillis()
val satPos = satManager.getPosition(satPass.satellite, stationPos, timeNow)
sendPassData(satPass, satPos, satPass.satellite)
sendPassDataBT(satPos)
processRadios(transmitters, satPass.satellite, timeNow)
delay(1000)
}
}
} }
} }
} }
fun enableSensor() { fun enableSensor() {
if (preferences.shouldUseCompass()) orientationSource.startListening(this) if (settings.getUseCompass()) orientationManager.startListening(this)
} }
fun disableSensor() { fun disableSensor() {
if (preferences.shouldUseCompass()) orientationSource.stopListening() if (settings.getUseCompass()) orientationManager.stopListening()
} }
fun getUseCompass(): Boolean = settings.getUseCompass()
fun getShowSweep(): Boolean = settings.getShowSweep()
override fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float) { override fun onOrientationChanged(azimuth: Float, pitch: Float, roll: Float) {
_orientation.value = Triple(azimuth + preferences.getMagDeclination(), pitch, roll) _orientation.value = Triple(azimuth + getMagDeclination(stationPos), pitch, roll)
} }
private fun sendPassData(satPass: SatPass) { private fun getMagDeclination(geoPos: GeoPos, time: Long = System.currentTimeMillis()): Float {
val latitude = geoPos.lat.toFloat()
val longitude = geoPos.lon.toFloat()
return GeomagneticField(latitude, longitude, 0f, time).declination
}
private fun sendPassData(satPass: SatPass, satPos: SatPos, satellite: Satellite) {
viewModelScope.launch { viewModelScope.launch {
var satTrack: List<SatPos> = emptyList() var track: List<SatPos> = emptyList()
if (!satPass.isDeepSpace) { if (!satPass.isDeepSpace) {
val startDate = satPass.aosTime track = satManager.getTrack(satellite, stationPos, satPass.aosTime, satPass.losTime)
val endDate = satPass.losTime
satTrack = predictor.getSatTrack(satPass.satellite, stationPos, startDate, endDate)
} }
while (isActive) { if (settings.getRotatorEnabled()) {
val satPos = predictor.getSatPos(satPass.satellite, stationPos, Date().time) val server = settings.getRotatorServer()
if (preferences.isRotatorEnabled()) { val port = settings.getRotatorPort().toInt()
val server = preferences.getRotatorServer().first val azimuth = satPos.azimuth.toDegrees().round(2)
val port = preferences.getRotatorServer().second val elevation = satPos.elevation.toDegrees().round(2)
val azimuth = Math.toDegrees(satPos.azimuth).round(1) reporter.reportRotation(server, port, azimuth, elevation)
val elevation = Math.toDegrees(satPos.elevation).round(1) }
dataReporter.reportRotation(server, port, azimuth, elevation) _passData.postValue(RadarData(satPos, track))
}
}
private fun sendPassDataBT(satPos: SatPos) {
viewModelScope.launch {
if (settings.getBTEnabled()) {
val btDevice = settings.getBTDeviceAddr()
if (btReporter.isConnected()) {
val format = settings.getBTFormat()
val azimuth = satPos.azimuth.toDegrees().round(0).toInt()
val elevation = satPos.elevation.toDegrees().round(0).toInt()
btReporter.reportRotation(format, azimuth, elevation)
} else if (!btReporter.isConnecting()) {
Log.i("BTReporter", "BTReporter: Attempting to connect...")
btReporter.connectBTDevice(btDevice)
} }
_passData.postValue(RadarData(satPos, satTrack))
delay(1000)
} }
} }
} }
private fun processTransmitters(satPass: SatPass) { private fun processRadios(radios: List<SatRadio>, satellite: Satellite, time: Long) {
viewModelScope.launch { viewModelScope.launch {
val transmitters = dataRepository.getTransmitters(satPass.catNum) delay(125)
while (isActive) { val list = satManager.processRadios(satellite, stationPos, radios, time)
val satPos = predictor.getSatPos(satPass.satellite, stationPos, Date().time) _transmitters.postValue(list)
val copiedList = transmitters.map { it.copy() }
copiedList.forEach { transmitter ->
transmitter.downlink?.let {
transmitter.downlink = satPos.getDownlinkFreq(it)
}
transmitter.uplink?.let {
transmitter.uplink = satPos.getUplinkFreq(it)
}
}
_transmitters.postValue(copiedList.map { it.copy() })
delay(1000)
}
} }
} }
} }
@@ -0,0 +1,103 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.radarScreen
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.ItemRadioBinding
import com.rtbishop.look4sat.domain.model.SatRadio
import java.util.*
class RadioAdapter : RecyclerView.Adapter<RadioAdapter.TransHolder>() {
private val diffCallback = object : DiffUtil.ItemCallback<SatRadio>() {
override fun areItemsTheSame(oldItem: SatRadio, newItem: SatRadio): Boolean {
return oldItem.uuid == newItem.uuid
}
override fun areContentsTheSame(oldItem: SatRadio, newItem: SatRadio): Boolean {
return oldItem.downlink == newItem.downlink
}
}
private val differ = AsyncListDiffer(this, diffCallback)
fun submitList(items: List<SatRadio>) = differ.submitList(items)
override fun getItemCount() = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransHolder {
return TransHolder.from(parent)
}
override fun onBindViewHolder(holder: TransHolder, position: Int) {
holder.bind(differ.currentList[position])
}
class TransHolder private constructor(private val binding: ItemRadioBinding) :
RecyclerView.ViewHolder(binding.root) {
private val divider = 1000000f
private val stringYes = itemView.context.getString(R.string.radio_string_yes)
private val stringNo = itemView.context.getString(R.string.radio_string_no)
private val link = itemView.context.getString(R.string.radio_link_low)
private val linkNull = itemView.context.getString(R.string.radio_no_link)
private val mode = itemView.context.getString(R.string.radio_mode)
private val inverted = itemView.context.getString(R.string.radio_inverted)
fun bind(radio: SatRadio) {
binding.run {
radioInfo.text = radio.info
radio.downlink.let { downlink ->
if (downlink != null) {
radioDownlink.text = String.format(Locale.ENGLISH, link, downlink / divider)
} else {
radioDownlink.text = linkNull
}
}
radio.uplink.let { uplink ->
if (uplink != null) {
radioUplink.text = String.format(Locale.ENGLISH, link, uplink / divider)
} else {
radioUplink.text = linkNull
}
}
if (radio.mode != null) {
radioMode.text = String.format(mode, radio.mode)
} else {
radioMode.text = String.format(mode, stringNo)
}
if (radio.isInverted) {
radioInverted.text = String.format(inverted, stringYes)
} else {
radioInverted.text = String.format(inverted, stringNo)
}
}
}
companion object {
fun from(parent: ViewGroup): TransHolder {
val inflater = LayoutInflater.from(parent.context)
return TransHolder(ItemRadioBinding.inflate(inflater, parent, false))
}
}
}
}
@@ -1,110 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.radarScreen
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.ItemTransBinding
import com.rtbishop.look4sat.domain.model.Transmitter
import java.util.*
class TransmittersAdapter : RecyclerView.Adapter<TransmittersAdapter.TransHolder>() {
private val diffCallback = object : DiffUtil.ItemCallback<Transmitter>() {
override fun areItemsTheSame(oldItem: Transmitter, newItem: Transmitter): Boolean {
return oldItem.uuid == newItem.uuid
}
override fun areContentsTheSame(oldItem: Transmitter, newItem: Transmitter): Boolean {
return oldItem.downlink == newItem.downlink
}
}
private val differ = AsyncListDiffer(this, diffCallback)
fun submitList(transmitters: List<Transmitter>) {
differ.submitList(transmitters)
}
override fun getItemCount() = differ.currentList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TransHolder {
return TransHolder.from(parent)
}
override fun onBindViewHolder(holder: TransHolder, position: Int) {
holder.bind(differ.currentList[position])
}
class TransHolder private constructor(private val binding: ItemTransBinding) :
RecyclerView.ViewHolder(binding.root) {
private val divider = 1000000f
private val strNo = itemView.context.getString(R.string.no)
private val strYes = itemView.context.getString(R.string.yes)
private val mode = itemView.context.getString(R.string.trans_mode)
private val formatLink = itemView.context.getString(R.string.trans_link_low)
private val formatLinkNull = itemView.context.getString(R.string.trans_no_link)
private val isInverted = itemView.context.getString(R.string.trans_inverted)
fun bind(transmitter: Transmitter) {
binding.transDesc.text = transmitter.info
transmitter.downlink.let { downlink ->
if (downlink != null) {
val downlinkFreq = downlink / divider
binding.transDownlink.text =
String.format(Locale.ENGLISH, formatLink, downlinkFreq)
} else {
binding.transDownlink.text = formatLinkNull
}
}
transmitter.uplink.let { uplink ->
if (uplink != null) {
val uplinkFreq = uplink / divider
binding.transUplink.text = String.format(Locale.ENGLISH, formatLink, uplinkFreq)
} else {
binding.transUplink.text = formatLinkNull
}
}
if (transmitter.mode != null) {
binding.transMode.text = String.format(mode, transmitter.mode)
} else {
binding.transMode.text = String.format(mode, strNo)
}
if (transmitter.isInverted) {
binding.transInverted.text = String.format(isInverted, strYes)
} else {
binding.transInverted.text = String.format(isInverted, strNo)
}
}
companion object {
fun from(parent: ViewGroup): TransHolder {
val inflater = LayoutInflater.from(parent.context)
return TransHolder(ItemTransBinding.inflate(inflater, parent, false))
}
}
}
}
@@ -0,0 +1,59 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.settingsScreen
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.DialogLocatorBinding
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class LocatorDialog : AppCompatDialogFragment() {
@Inject
lateinit var preferences: ISettingsManager
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
return inflater.inflate(R.layout.dialog_locator, group, false)
}
override fun onViewCreated(view: View, state: Bundle?) {
super.onViewCreated(view, state)
DialogLocatorBinding.bind(view).run {
dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
dialog?.window?.setLayout(
(resources.displayMetrics.widthPixels * 0.94).toInt(),
WindowManager.LayoutParams.WRAP_CONTENT
)
locatorEdit.setText(preferences.loadStationLocator())
locatorBtnPos.setOnClickListener {
setNavResult("locator", locatorEdit.text.toString())
dismiss()
}
locatorBtnNeg.setOnClickListener { dismiss() }
}
}
}
@@ -0,0 +1,71 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.settingsScreen
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.DialogPositionBinding
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class PositionDialog : AppCompatDialogFragment() {
@Inject
lateinit var preferences: ISettingsManager
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
return inflater.inflate(R.layout.dialog_position, group, false)
}
override fun onViewCreated(view: View, state: Bundle?) {
super.onViewCreated(view, state)
DialogPositionBinding.bind(view).run {
dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
dialog?.window?.setLayout(
(resources.displayMetrics.widthPixels * 0.94).toInt(),
WindowManager.LayoutParams.WRAP_CONTENT
)
val location = preferences.loadStationPosition()
positionLatEdit.setText(location.lat.toString())
positionLonEdit.setText(location.lon.toString())
positionBtnPos.setOnClickListener {
val latitude = try {
positionLatEdit.text.toString().toDouble()
} catch (exception: Exception) {
180.0
}
val longitude = try {
positionLonEdit.text.toString().toDouble()
} catch (exception: Exception) {
400.0
}
setNavResult("position", Pair(latitude, longitude))
dismiss()
}
positionBtnNeg.setOnClickListener { dismiss() }
}
}
}
@@ -1,6 +1,6 @@
/* /*
* Look4Sat. Amateur radio satellite tracker and pass predictor. * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com) * Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -18,107 +18,290 @@
package com.rtbishop.look4sat.presentation.settingsScreen package com.rtbishop.look4sat.presentation.settingsScreen
import android.Manifest import android.Manifest
import android.content.pm.PackageManager import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.InputType import android.text.method.LinkMovementMethod
import android.view.View import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.widget.NestedScrollView
import androidx.preference.EditTextPreference import androidx.core.widget.doOnTextChanged
import androidx.preference.Preference import androidx.fragment.app.Fragment
import androidx.preference.PreferenceFragmentCompat import androidx.fragment.app.viewModels
import com.google.android.material.snackbar.Snackbar import androidx.lifecycle.asLiveData
import androidx.navigation.fragment.findNavController
import com.rtbishop.look4sat.BuildConfig
import com.rtbishop.look4sat.R import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.framework.PreferencesSource import com.rtbishop.look4sat.databinding.FragmentSettingsBinding
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.presentation.clickWithDebounce
import com.rtbishop.look4sat.presentation.getNavResult
import com.rtbishop.look4sat.utility.isValidIPv4
import com.rtbishop.look4sat.utility.isValidPort
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import java.text.SimpleDateFormat
import java.util.*
@AndroidEntryPoint @AndroidEntryPoint
class SettingsFragment : PreferenceFragmentCompat() { class SettingsFragment : Fragment(R.layout.fragment_settings) {
@Inject private val viewModel: SettingsViewModel by viewModels()
lateinit var preferences: PreferencesSource private val bluetooth = when {
Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> Manifest.permission.BLUETOOTH
private val permReqContract = ActivityResultContracts.RequestMultiplePermissions() else -> Manifest.permission.BLUETOOTH_CONNECT
private val locPermFine = Manifest.permission.ACCESS_FINE_LOCATION }
private val locPermCoarse = Manifest.permission.ACCESS_COARSE_LOCATION private val bluetoothContract = ActivityResultContracts.RequestPermission()
private val locPermReq = registerForActivityResult(permReqContract) { permissions -> private val bluetoothRequest = registerForActivityResult(bluetoothContract) { isGranted ->
when { if (!isGranted) {
permissions[locPermFine] == true -> updatePositionFromGPS() showToast(getString(R.string.BTremote_perm_error))
permissions[locPermCoarse] == true -> updatePositionFromGPS() toggleBTstate(isGranted)
else -> showSnack(getString(R.string.pref_pos_gps_error))
} }
} }
private val locationFine = Manifest.permission.ACCESS_FINE_LOCATION
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { private val locationCoarse = Manifest.permission.ACCESS_COARSE_LOCATION
setPreferencesFromResource(R.xml.preference, rootKey) private val locationContract = ActivityResultContracts.RequestMultiplePermissions()
private val locationRequest = registerForActivityResult(locationContract) { permissions ->
when {
permissions[locationFine] == true -> viewModel.setPositionFromGps()
permissions[locationCoarse] == true -> viewModel.setPositionFromNet()
else -> showToast(getString(R.string.location_gps_error))
}
} }
private val contentContract = ActivityResultContracts.GetContent()
private val contentRequest = registerForActivityResult(contentContract) { uri ->
uri?.let { viewModel.updateFromFile(uri.toString()) }
}
private lateinit var binding: FragmentSettingsBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentSettingsBinding.bind(view).apply {
findPreference<Preference>(PreferencesSource.keyPositionGPS)?.apply { settingsBtnBack.clickWithDebounce { findNavController().navigateUp() }
setOnPreferenceClickListener { settingsScroll.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, y, _, newY ->
updatePositionFromGPS() if (y > newY) settingsFab.hide() else settingsFab.show()
return@setOnPreferenceClickListener true })
settingsAbout.aboutVersion.text =
String.format(getString(R.string.app_version), BuildConfig.VERSION_NAME)
settingsBtnGithub.clickWithDebounce {
gotoUrl("https://github.com/rt-bishop/Look4Sat/")
}
settingsFab.clickWithDebounce {
gotoUrl("https://ko-fi.com/rt_bishop")
}
settingsBtnFdroid.clickWithDebounce {
gotoUrl("https://f-droid.org/en/packages/com.rtbishop.look4sat/")
} }
} }
setupLocationCard()
findPreference<Preference>(PreferencesSource.keyPositionQTH)?.apply { setupDataCard()
setOnPreferenceChangeListener { _, newValue -> setupRemoteCard()
updatePositionFromQth(newValue.toString()) setupBTCard()
} setupOtherCard()
setupOutroCard()
viewModel.stationPosition.asLiveData().observe(viewLifecycleOwner) { stationPos ->
stationPos?.let { handleStationPosition(stationPos) }
} }
viewModel.getUpdateState().asLiveData().observe(viewLifecycleOwner) { updateState ->
findPreference<EditTextPreference>(PreferencesSource.keyRotatorAddress)?.apply { updateState?.let { handleSatState(updateState) }
setOnPreferenceChangeListener { _, newValue ->
val ip4 = "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\\.(?!\$)|\$)){4}\$"
if (newValue.toString().matches(ip4.toRegex())) {
return@setOnPreferenceChangeListener true
} else {
showSnack(getString(R.string.tracking_rotator_address_invalid))
return@setOnPreferenceChangeListener false
}
}
} }
}
findPreference<EditTextPreference>(PreferencesSource.keyRotatorPort)?.apply { private fun setupLocationCard() {
setOnBindEditTextListener { it.inputType = InputType.TYPE_CLASS_NUMBER } binding.run {
setOnPreferenceChangeListener { _, newValue -> setPositionText(viewModel.getStationPosition())
val portValue = newValue.toString() settingsLocation.locationBtnGps.clickWithDebounce {
if (portValue.isNotEmpty() && portValue.toInt() in 1024..65535) { locationRequest.launch(arrayOf(locationFine, locationCoarse))
return@setOnPreferenceChangeListener true }
} else { settingsLocation.locationBtnManual.clickWithDebounce {
showSnack(getString(R.string.tracking_rotator_port_invalid)) val action = SettingsFragmentDirections.globalToPosition()
return@setOnPreferenceChangeListener false findNavController().navigate(action)
} }
settingsLocation.locationBtnQth.clickWithDebounce {
val action = SettingsFragmentDirections.globalToLocator()
findNavController().navigate(action)
}
getNavResult<Pair<Double, Double>>(R.id.nav_settings, "position") { position ->
viewModel.setStationPosition(position.first, position.second)
}
getNavResult<String>(R.id.nav_settings, "locator") { locator ->
viewModel.setPositionFromQth(locator)
} }
} }
} }
private fun updatePositionFromQth(qthString: String): Boolean { private fun setupDataCard() {
return if (preferences.updatePositionFromQTH(qthString)) { binding.run {
showSnack(getString(R.string.pref_pos_success)) setUpdateTime(viewModel.getLastUpdateTime())
true settingsData.dataBtnWeb.clickWithDebounce { viewModel.updateFromWeb() }
settingsData.dataBtnFile.clickWithDebounce { contentRequest.launch("*/*") }
settingsData.dataBtnClear.clickWithDebounce { viewModel.clearAllData() }
viewModel.entriesTotal.observe(viewLifecycleOwner) { number ->
val entriesFormat = getString(R.string.data_entries)
settingsData.dataEntries.text = String.format(entriesFormat, number)
}
viewModel.radiosTotal.observe(viewLifecycleOwner) { number ->
val radiosFormat = getString(R.string.data_radios)
settingsData.dataRadios.text = String.format(radiosFormat, number)
}
getNavResult<List<String>>(R.id.nav_settings, "sources") {
viewModel.updateFromWeb()
}
}
}
private fun setupRemoteCard() {
binding.run {
settingsRemote.remoteSwitch.apply {
isChecked = viewModel.getRotatorEnabled()
settingsRemote.remoteIp.isEnabled = isChecked
settingsRemote.remoteIpEdit.setText(viewModel.getRotatorServer())
settingsRemote.remotePort.isEnabled = isChecked
settingsRemote.remotePortEdit.setText(viewModel.getRotatorPort())
setOnCheckedChangeListener { _, isChecked ->
viewModel.setRotatorEnabled(isChecked)
settingsRemote.remoteIp.isEnabled = isChecked
settingsRemote.remotePort.isEnabled = isChecked
}
}
settingsRemote.remoteIpEdit.doOnTextChanged { text, _, _, _ ->
if (text.toString().isValidIPv4()) viewModel.setRotatorServer(text.toString())
}
settingsRemote.remotePortEdit.doOnTextChanged { text, _, _, _ ->
if (text.toString().isValidPort()) viewModel.setRotatorPort(text.toString())
}
}
}
private fun setupBTCard() {
binding.run {
settingsBtremote.BTremoteSwitch.apply {
isChecked = viewModel.getBTEnabled()
settingsBtremote.BTremoteAddress.isEnabled = isChecked
settingsBtremote.BTAddressEdit.setText(viewModel.getBTDeviceAddr())
settingsBtremote.BTremoteFormat.isEnabled = isChecked
settingsBtremote.BTFormatEdit.setText(viewModel.getBTFormat())
setOnCheckedChangeListener { _, isChecked ->
toggleBTstate(isChecked)
bluetoothRequest.launch(bluetooth)
}
}
settingsBtremote.BTAddressEdit.doOnTextChanged { text, _, _, _ ->
viewModel.setBTDeviceAddr(text.toString())
}
settingsBtremote.BTFormatEdit.doOnTextChanged { text, _, _, _ ->
viewModel.setBTFormat(text.toString())
}
}
}
private fun toggleBTstate(value: Boolean) {
binding.run {
viewModel.setBTEnabled(value)
settingsBtremote.BTremoteSwitch.isChecked = value
settingsBtremote.BTremoteAddress.isEnabled = value
settingsBtremote.BTremoteFormat.isEnabled = value
}
}
private fun setupOtherCard() {
binding.run {
settingsOther.otherSwitchUtc.apply {
isChecked = viewModel.getUseUTC()
setOnCheckedChangeListener { _, isChecked -> viewModel.setUseUTC(isChecked) }
}
settingsOther.otherSwitchUpdate.apply {
isChecked = viewModel.getAutoUpdateEnabled()
setOnCheckedChangeListener { _, isChecked -> viewModel.setAutoUpdateEnabled(isChecked) }
}
settingsOther.otherSwitchSweep.apply {
isChecked = viewModel.getShowSweep()
setOnCheckedChangeListener { _, isChecked -> viewModel.setShowSweep(isChecked) }
}
settingsOther.otherSwitchSensors.apply {
isChecked = viewModel.getUseCompass()
setOnCheckedChangeListener { _, isChecked -> viewModel.setUseCompass(isChecked) }
}
}
}
private fun setupOutroCard() {
binding.run {
settingsOutro.outroThanks.movementMethod = LinkMovementMethod.getInstance()
settingsOutro.outroLicense.movementMethod = LinkMovementMethod.getInstance()
}
}
private fun handleStationPosition(pos: DataState<GeoPos>) {
when (pos) {
is DataState.Success -> {
setPositionText(pos.data)
binding.settingsLocation.locationProgress.isIndeterminate = false
viewModel.setPositionHandled()
showToast(getString(R.string.location_success))
}
is DataState.Error -> {
binding.settingsLocation.locationProgress.isIndeterminate = false
viewModel.setPositionHandled()
showToast(pos.message.toString())
}
DataState.Loading -> {
binding.settingsLocation.locationProgress.isIndeterminate = true
}
DataState.Handled -> {}
}
}
private fun setPositionText(geoPos: GeoPos) {
binding.run {
val latFormat = getString(R.string.location_lat)
val lonFormat = getString(R.string.location_lon)
settingsLocation.locationLat.text = String.format(latFormat, geoPos.lat)
settingsLocation.locationLon.text = String.format(lonFormat, geoPos.lon)
}
}
private fun handleSatState(state: DataState<Long>) {
when (state) {
is DataState.Success -> {
binding.settingsData.dataProgress.isIndeterminate = false
setUpdateTime(state.data)
viewModel.setUpdateHandled()
if (state.data == 0L) {
showToast(getString(R.string.data_clear_success))
} else {
showToast(getString(R.string.data_success))
}
}
is DataState.Error -> {
binding.settingsData.dataProgress.isIndeterminate = false
viewModel.setUpdateHandled()
showToast(getString(R.string.data_error))
}
is DataState.Loading -> {
binding.settingsData.dataProgress.isIndeterminate = true
}
is DataState.Handled -> {}
}
}
private fun setUpdateTime(updateTime: Long) {
val updatePattern = getString(R.string.data_update)
val updateDate = if (updateTime == 0L) {
getString(R.string.pass_placeholder)
} else { } else {
showSnack(getString(R.string.pref_pos_qth_error)) SimpleDateFormat("d MMM yyyy - HH:mm:ss", Locale.getDefault()).format(Date(updateTime))
false
} }
binding.settingsData.dataUpdate.text = String.format(updatePattern, updateDate)
} }
private fun updatePositionFromGPS() { private fun showToast(message: String) {
val locPermString = Manifest.permission.ACCESS_FINE_LOCATION Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
val locPermResult = ContextCompat.checkSelfPermission(requireContext(), locPermString)
if (locPermResult == PackageManager.PERMISSION_GRANTED) {
if (preferences.updatePositionFromGPS()) {
showSnack(getString(R.string.pref_pos_success))
} else {
showSnack(getString(R.string.pref_pos_gps_null))
}
} else locPermReq.launch(arrayOf(locPermFine, locPermCoarse))
} }
private fun showSnack(message: String) { private fun gotoUrl(url: String) {
Snackbar.make(requireView(), message, Snackbar.LENGTH_SHORT).show() startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} }
} }
@@ -0,0 +1,110 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.settingsScreen
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ILocationManager
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.GeoPos
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharedFlow
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val locationManager: ILocationManager,
private val repository: IDataRepository,
private val settings: ISettingsManager
) : ViewModel() {
val entriesTotal = repository.getEntriesTotal().asLiveData()
val radiosTotal = repository.getRadiosTotal().asLiveData()
fun updateFromFile(uri: String) = repository.updateFromFile(uri)
fun updateFromWeb() = repository.updateFromWeb()
fun clearAllData() = repository.clearAllData()
fun getUseUTC(): Boolean = settings.getUseUTC()
fun setUseUTC(value: Boolean) = settings.setUseUTC(value)
fun getLastUpdateTime(): Long = settings.getLastUpdateTime()
fun getAutoUpdateEnabled(): Boolean = settings.getAutoUpdateEnabled()
fun setAutoUpdateEnabled(value: Boolean) = settings.setAutoUpdateEnabled(value)
fun getUseCompass(): Boolean = settings.getUseCompass()
fun setUseCompass(value: Boolean) = settings.setUseCompass(value)
fun getShowSweep(): Boolean = settings.getShowSweep()
fun setShowSweep(value: Boolean) = settings.setShowSweep(value)
fun getRotatorEnabled(): Boolean = settings.getRotatorEnabled()
fun setRotatorEnabled(value: Boolean) = settings.setRotatorEnabled(value)
fun getRotatorServer(): String = settings.getRotatorServer()
fun setRotatorServer(value: String) = settings.setRotatorServer(value)
fun getRotatorPort(): String = settings.getRotatorPort()
fun setRotatorPort(value: String) = settings.setRotatorPort(value)
fun getBTEnabled(): Boolean = settings.getBTEnabled()
fun setBTEnabled(value: Boolean) = settings.setBTEnabled(value)
fun getBTFormat(): String = settings.getBTFormat()
fun setBTFormat(value: String) = settings.setBTFormat(value)
// fun getBTDeviceName(): String = settings.getBTDeviceName()
// fun setBTDeviceName(value: String) = settings.setBTDeviceName(value)
fun getBTDeviceAddr(): String = settings.getBTDeviceAddr()
fun setBTDeviceAddr(value: String) = settings.setBTDeviceAddr(value)
fun getUpdateState() = repository.updateState
fun setUpdateHandled() = repository.setUpdateStateHandled()
val stationPosition: SharedFlow<DataState<GeoPos>> = locationManager.stationPosition
fun getStationPosition(): GeoPos = locationManager.getStationPosition()
fun setStationPosition(lat: Double, lon: Double) = locationManager.setStationPosition(lat, lon)
fun setPositionFromGps() = locationManager.setPositionFromGps()
fun setPositionFromNet() = locationManager.setPositionFromNet()
fun setPositionFromQth(qthString: String) = locationManager.setPositionFromQth(qthString)
fun setPositionHandled() = locationManager.setPositionHandled()
}
@@ -1,70 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.sourcesScreen
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.RecyclerView
import com.rtbishop.look4sat.databinding.ItemSourceBinding
import com.rtbishop.look4sat.framework.model.Source
class SourcesAdapter(private val sources: MutableList<Source> = mutableListOf()) :
RecyclerView.Adapter<SourcesAdapter.TleSourceHolder>() {
fun getSources(): List<Source> {
return sources.filter { it.sourceUrl.contains("https://") }
}
fun setSources(list: List<Source>) {
sources.clear()
sources.addAll(list)
}
fun addSource() {
sources.add(Source())
notifyItemInserted(itemCount - 1)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TleSourceHolder {
val binding = ItemSourceBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return TleSourceHolder(binding)
}
override fun onBindViewHolder(holder: TleSourceHolder, position: Int) {
holder.bind(sources[position])
}
override fun getItemCount(): Int {
return sources.size
}
inner class TleSourceHolder(private val binding: ItemSourceBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(source: Source) {
binding.sourceUrl.setText(source.sourceUrl)
binding.sourceUrl.doOnTextChanged { txt, _, _, _ -> source.sourceUrl = txt.toString() }
binding.sourceBtn.setOnClickListener {
sources.remove(source)
notifyItemRemoved(adapterPosition)
}
}
}
}
@@ -1,67 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2021 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.presentation.sourcesScreen
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.rtbishop.look4sat.R
import com.rtbishop.look4sat.databinding.DialogSourcesBinding
import com.rtbishop.look4sat.framework.model.Source
import com.rtbishop.look4sat.presentation.setNavResult
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class SourcesDialog : AppCompatDialogFragment() {
private val viewModel: SourcesViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, group: ViewGroup?, state: Bundle?): View? {
return inflater.inflate(R.layout.dialog_sources, group, false)
}
override fun onViewCreated(view: View, state: Bundle?) {
super.onViewCreated(view, state)
viewModel.sources.observe(viewLifecycleOwner, { sources ->
val adapter = SourcesAdapter().apply { setSources(sources.map { Source(it) }) }
DialogSourcesBinding.bind(view).apply {
dialog?.window?.setLayout(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT
)
sourcesRecycler.apply {
this.adapter = adapter
layoutManager = LinearLayoutManager(requireContext())
}
sourcesBtnAdd.setOnClickListener {
adapter.addSource()
}
sourcesBtnPos.setOnClickListener {
setNavResult("sources", adapter.getSources().map { it.sourceUrl })
dismiss()
}
sourcesBtnNeg.setOnClickListener { dismiss() }
}
})
}
}
@@ -1,15 +0,0 @@
package com.rtbishop.look4sat.presentation.sourcesScreen
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import com.rtbishop.look4sat.domain.DataRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class SourcesViewModel @Inject constructor(dataRepository: DataRepository) : ViewModel() {
val sources = liveData {
emit(dataRepository.getWebSources())
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

-9
View File
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/themeLight"
android:pathData="M8,5v14l11,-7z" />
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>
-11
View File
@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="16dp"
android:height="24dp"
android:viewportWidth="233.76132"
android:viewportHeight="338.40268">
<path
android:pathData="m113.404,0c-10.877,-0.015 -21.753,0.458 -32.594,1.417 -12.911,1.175 -26.51,2.595 -38.779,7.061 -4.484,1.634 -9.105,3.597 -12.514,7.061 -4.184,4.257 -5.55,10.84 -2.495,16.148 2.172,3.77 5.85,6.432 9.752,8.194 5.082,2.27 10.389,3.998 15.834,5.154 15.16,3.351 30.861,4.666 46.349,5.227 17.166,0.693 34.36,0.131 51.444,-1.68 4.225,-0.464 8.442,-1.021 12.651,-1.671 4.958,-0.76 8.14,-7.243 6.679,-11.759 -1.748,-5.399 -6.446,-7.493 -11.759,-6.678 -0.783,0.123 -1.561,0.236 -2.345,0.35l-0.564,0.082c-1.8,0.228 -3.6,0.44 -5.4,0.638 -3.718,0.401 -7.445,0.728 -11.181,0.983 -8.368,0.583 -16.758,0.851 -25.144,0.865 -8.24,0 -16.485,-0.232 -24.707,-0.774 -3.751,-0.246 -7.493,-0.559 -11.226,-0.938 -1.698,-0.178 -3.392,-0.364 -5.085,-0.574l-1.612,-0.205 -0.35,-0.05 -1.671,-0.241c-3.414,-0.515 -6.829,-1.106 -10.207,-1.821 -0.341,-0.076 -0.646,-0.266 -0.865,-0.538 -0.219,-0.272 -0.337,-0.611 -0.337,-0.96 0,-0.349 0.119,-0.688 0.337,-0.96 0.219,-0.272 0.524,-0.462 0.865,-0.537l0.064,0c2.927,-0.624 5.877,-1.156 8.836,-1.621 0.987,-0.155 1.976,-0.307 2.968,-0.455l0.027,0c1.853,-0.123 3.715,-0.455 5.559,-0.674 16.042,-1.669 32.178,-2.238 48.298,-1.703 7.826,0.228 15.647,0.687 23.436,1.479 1.675,0.173 3.342,0.355 5.008,0.56 0.637,0.077 1.279,0.169 1.921,0.246l1.293,0.187c3.77,0.561 7.519,1.243 11.249,2.044 5.527,1.202 12.625,1.594 15.083,7.649 0.783,1.921 1.138,4.056 1.57,6.072l0.551,2.572c0.015,0.046 0.025,0.093 0.032,0.141 1.302,6.07 2.605,12.14 3.91,18.21 0.096,0.448 0.098,0.912 0.007,1.361 -0.091,0.45 -0.274,0.875 -0.537,1.251 -0.263,0.375 -0.601,0.693 -0.992,0.932 -0.391,0.239 -0.827,0.395 -1.282,0.458l-0.037,0l-0.797,0.109 -0.787,0.105c-2.495,0.325 -4.993,0.628 -7.494,0.911 -4.926,0.561 -9.86,1.047 -14.801,1.457 -9.818,0.816 -19.656,1.352 -29.514,1.607 -5.023,0.133 -10.044,0.196 -15.064,0.187 -19.981,-0.016 -39.945,-1.177 -59.793,-3.478 -2.149,-0.255 -4.298,-0.528 -6.447,-0.806 1.666,0.214 -1.211,-0.164 -1.793,-0.246 -1.366,-0.191 -2.732,-0.39 -4.098,-0.596 -4.584,-0.688 -9.142,-1.534 -13.717,-2.276 -5.531,-0.91 -10.821,-0.455 -15.824,2.276 -4.107,2.247 -7.431,5.694 -9.529,9.879 -2.158,4.461 -2.799,9.319 -3.765,14.113 -0.965,4.794 -2.468,9.952 -1.899,14.873 1.225,10.621 8.65,19.252 19.33,21.183 10.047,1.821 20.149,3.296 30.279,4.553 39.791,4.873 79.99,5.456 119.905,1.739 3.25,-0.303 6.496,-0.634 9.738,-0.992 1.013,-0.111 2.037,0.005 2.998,0.341 0.962,0.336 1.836,0.883 2.558,1.6 0.723,0.718 1.276,1.588 1.619,2.547 0.343,0.959 0.467,1.983 0.363,2.996l-1.011,9.824c-2.037,19.852 -4.073,39.703 -6.11,59.552 -2.124,20.845 -4.263,41.687 -6.415,62.527 -0.607,5.871 -1.214,11.74 -1.821,17.606 -0.583,5.778 -0.665,11.737 -1.762,17.446 -1.73,8.977 -7.807,14.491 -16.675,16.508 -8.125,1.849 -16.425,2.82 -24.757,2.894 -9.237,0.05 -18.469,-0.36 -27.706,-0.31 -9.861,0.056 -21.939,-0.854 -29.551,-8.194 -6.688,-6.445 -7.612,-16.539 -8.522,-25.268 -1.214,-11.554 -2.417,-23.107 -3.61,-34.658L52.284,188.281 47.954,146.722c-0.073,-0.687 -0.146,-1.366 -0.214,-2.058 -0.519,-4.957 -4.029,-9.81 -9.561,-9.56 -4.734,0.209 -10.116,4.234 -9.56,9.56l3.209,30.812 6.638,63.735c1.891,18.104 3.777,36.21 5.659,54.321 0.364,3.469 0.705,6.948 1.088,10.416 2.081,18.957 16.558,29.173 34.486,32.049 10.471,1.685 21.196,2.032 31.822,2.204 13.621,0.22 27.379,0.744 40.777,-1.725 19.854,-3.643 34.749,-16.899 36.875,-37.462 0.607,-5.937 1.214,-11.875 1.821,-17.814 2.018,-19.643 4.034,-39.288 6.046,-58.933l6.583,-64.19 3.018,-29.418c0.15,-1.459 0.766,-2.831 1.756,-3.912 0.99,-1.081 2.303,-1.816 3.742,-2.093 5.677,-1.106 11.104,-2.995 15.142,-7.316 6.428,-6.879 7.707,-15.847 5.435,-24.888l-1.862,-9.392C229.186,72.629 225.394,64.666 216.744,61.621 213.971,60.646 210.825,60.228 208.699,58.211c-2.126,-2.017 -2.754,-5.149 -3.246,-8.053 -0.911,-5.331 -1.766,-10.667 -2.7,-15.989 -0.806,-4.575 -1.443,-9.715 -3.542,-13.912 -2.732,-5.636 -8.4,-8.932 -14.036,-11.113 -2.888,-1.078 -5.835,-1.99 -8.827,-2.731 -14.081,-3.715 -28.886,-5.081 -43.373,-5.86 -6.52,-0.36 -13.047,-0.544 -19.573,-0.552zM29.029,78.272c6.185,0.029 12.996,2.347 18.907,3.243 8.134,1.241 16.298,2.235 24.492,2.982 34.973,3.187 70.533,2.691 105.351,-1.971 6.346,-0.853 12.67,-1.844 18.97,-2.973 5.613,-1.006 11.837,-2.895 15.229,2.918 2.326,3.961 2.635,9.26 2.276,13.735 -0.111,1.95 -0.963,3.783 -2.381,5.126l0.004,0c-2.039,1.939 -5.112,2.841 -8.149,3.292 -34.053,5.053 -68.602,7.612 -103.028,6.483 -24.638,-0.842 -49.017,-3.578 -73.409,-7.024 -2.39,-0.337 -4.981,-0.774 -6.624,-2.536C17.57,98.223 19.091,91.531 19.896,87.515 20.634,83.837 22.046,78.934 26.421,78.411c0.854,-0.1 1.725,-0.143 2.608,-0.138zM19.164,96.794c-0.127,0.059 -0.155,0.523 -0.141,1.047 0.068,-0.346 0.228,-1.088 0.141,-1.047zM19.736,101.053c0.059,0.016 0.179,0.18 0.302,0.371 -0.214,-0.201 -0.351,-0.351 -0.328,-0.369l0.005,0c0.006,-0.004 0.013,-0.004 0.021,-0.002zM21.34,102.652l0.005,0 0.027,0c0,0.032 0.05,0.064 0.068,0.096 -0.03,-0.035 -0.064,-0.068 -0.1,-0.096zM156.916,147.68c-11.645,-0.005 -21.028,4.006 -31.868,8.645 -12.155,5.204 -25.9Line truncated
android:fillColor="@color/themeLight"
tools:ignore="VectorPath" />
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z" />
</vector>
-23
View File
@@ -1,23 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="@color/themeLight"
android:pathData="M8,20.0449C7.7923,20.0449 7.5902,20.0656 7.3945,20.1055C6.8076,20.225 6.2836,20.5152 5.877,20.9219C5.4703,21.3286 5.18,21.8506 5.0605,22.4375C5.0207,22.6331 5,22.8372 5,23.0449L5,30.0449L5,42.2754L5,43.0449C5,43.6682 5.1889,44.246 5.5117,44.7246C6.0498,45.5224 6.9612,46.0449 8,46.0449L40,46.0449C41.0387,46.0449 41.9502,45.5224 42.4883,44.7246C42.5959,44.5651 42.6899,44.3944 42.7656,44.2148C42.917,43.8557 43,43.4604 43,43.0449L43,42.2754L43,36.0449L43,23.8125L43,23.0449C43,22.6294 42.917,22.2342 42.7656,21.875C42.6899,21.6954 42.5959,21.5248 42.4883,21.3652C41.9502,20.5675 41.0387,20.0449 40,20.0449L8,20.0449zM24,22.5254A10.5182,10.5182 0,0 1,34.5176 33.0449A10.5182,10.5182 0,0 1,24 43.5625A10.5182,10.5182 0,0 1,13.4824 33.0449A10.5182,10.5182 0,0 1,24 22.5254zM24,24.4492A8.5958,8.5958 0,0 0,15.4043 33.0449A8.5958,8.5958 0,0 0,24 41.6406A8.5958,8.5958 0,0 0,32.5957 33.0449A8.5958,8.5958 0,0 0,24 24.4492zM24,26.75C27.434,26.75 30.25,29.566 30.25,33C30.25,36.434 27.434,39.25 24,39.25C21.0479,39.25 18.5583,37.1661 17.916,34.4004L21.0703,34.4004C21.5888,35.4973 22.6926,36.25 24,36.25C25.8127,36.25 27.25,34.8127 27.25,33C27.25,31.1873 25.8127,29.75 24,29.75C22.7748,29.75 21.734,30.415 21.1797,31.4004L17.9668,31.4004C18.6802,28.7347 21.1193,26.75 24,26.75z"
android:strokeWidth="2"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="miter"
tools:ignore="VectorPath" />
<path
android:fillColor="@color/themeLight"
android:pathData="M2.3379,1.5254C1.8997,1.5254 1.378,1.6672 1.5137,2.3242C1.6007,2.7458 4.4144,6.2239 5.4434,7.4902C5.2677,7.7794 5.1304,8.0942 5.0605,8.4375C5.0207,8.6331 5,8.8372 5,9.0449L5,9.582L5,12.0449L5,13.0449L5,15.5059L5,16.0449C5,16.6682 5.1889,17.246 5.5117,17.7246C6.0498,18.5224 6.9612,19.0449 8,19.0449L40,19.0449C41.0387,19.0449 41.9502,18.5224 42.4883,17.7246C42.5959,17.5651 42.6899,17.3944 42.7656,17.2148C42.8413,17.0353 42.8996,16.846 42.9395,16.6504C42.9793,16.4547 43,16.2527 43,16.0449L43,15.5059L43,13.0449L43,12.0449L43,9.582L43,9.0449C43,8.8372 42.9793,8.6331 42.9395,8.4375C42.8696,8.0942 42.7323,7.7794 42.5566,7.4902C43.5856,6.2239 46.3993,2.7458 46.4863,2.3242C46.622,1.6672 46.1003,1.5254 45.6621,1.5254C45.1945,1.5396 44.8462,1.9932 44.5859,2.2832L41.3984,6.4023C40.9796,6.1808 40.5086,6.0449 40,6.0449L8,6.0449C7.7923,6.0449 7.5902,6.0656 7.3945,6.1055C7.1989,6.1453 7.0097,6.2036 6.8301,6.2793C6.7496,6.3132 6.6782,6.3618 6.6016,6.4023L3.4141,2.2832C3.1538,1.9932 2.8055,1.5396 2.3379,1.5254zM14.375,9.75A3.375,3.375 0,0 1,17.75 13.125A3.375,3.375 0,0 1,14.375 16.5A3.375,3.375 0,0 1,11 13.125A3.375,3.375 0,0 1,14.375 9.75zM33.875,9.75A3.375,3.375 0,0 1,37.25 13.125A3.375,3.375 0,0 1,33.875 16.5A3.375,3.375 0,0 1,30.5 13.125A3.375,3.375 0,0 1,33.875 9.75z"
android:strokeWidth="2"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="miter"
tools:ignore="VectorPath" />
</vector>
-11
View File
@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/themeLight"
android:pathData="M12,0.297c-6.63,0 -12,5.373 -12,12 0,5.303 3.438,9.8 8.205,11.385 0.6,0.113 0.82,-0.258 0.82,-0.577 0,-0.285 -0.01,-1.04 -0.015,-2.04 -3.338,0.724 -4.042,-1.61 -4.042,-1.61C4.422,18.07 3.633,17.7 3.633,17.7c-1.087,-0.744 0.084,-0.729 0.084,-0.729 1.205,0.084 1.838,1.236 1.838,1.236 1.07,1.835 2.809,1.305 3.495,0.998 0.108,-0.776 0.417,-1.305 0.76,-1.605 -2.665,-0.3 -5.466,-1.332 -5.466,-5.93 0,-1.31 0.465,-2.38 1.235,-3.22 -0.135,-0.303 -0.54,-1.523 0.105,-3.176 0,0 1.005,-0.322 3.3,1.23 0.96,-0.267 1.98,-0.399 3,-0.405 1.02,0.006 2.04,0.138 3,0.405 2.28,-1.552 3.285,-1.23 3.285,-1.23 0.645,1.653 0.24,2.873 0.12,3.176 0.765,0.84 1.23,1.91 1.23,3.22 0,4.61 -2.805,5.625 -5.475,5.92 0.42,0.36 0.81,1.096 0.81,2.22 0,1.606 -0.015,2.896 -0.015,3.286 0,0.315 0.21,0.69 0.825,0.57C20.565,22.092 24,17.592 24,12.297c0,-6.627 -5.373,-12 -12,-12"
tools:ignore="VectorPath" />
</vector>
@@ -1,6 +1,4 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<gradient <solid android:color="@color/background" />
android:endColor="@color/greyDark"
android:startColor="@color/greyDark" />
</shape> </shape>
@@ -1,7 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:autoMirrored="true"
android:viewportWidth="271.2439" android:viewportWidth="271.2439"
android:viewportHeight="271.2439"> android:viewportHeight="271.2439">
<group <group
@@ -11,12 +11,11 @@
android:translateX="70.26573" android:translateX="70.26573"
android:translateY="70.26573"> android:translateY="70.26573">
<path <path
android:fillColor="@color/themeLight" android:fillColor="@color/accent"
android:pathData="m96.642,31.504c7.911,7.997 17.143,13.584 25.365,15.581l1.754,-3.997C123.78,43.048 123.786,43.005 123.802,42.965 125.834,38.995 127,34.495 127,29.715 127,13.881 114.379,1 98.867,1c-5.125,0 -9.92,1.429 -14.064,3.884l-3.398,1.598c0.08,0.306 0.133,0.599 0.221,0.91 2.32,8.104 7.652,16.667 15.016,24.112z" /> android:pathData="m96.642,31.504c7.911,7.997 17.143,13.584 25.365,15.581l1.754,-3.997C123.78,43.048 123.786,43.005 123.802,42.965 125.834,38.995 127,34.495 127,29.715 127,13.881 114.379,1 98.867,1c-5.125,0 -9.92,1.429 -14.064,3.884l-3.398,1.598c0.08,0.306 0.133,0.599 0.221,0.91 2.32,8.104 7.652,16.667 15.016,24.112z" />
<path <path
android:fillColor="@color/themeLight" android:fillColor="@color/accent"
android:pathData="M93.876,34.3C86.053,26.391 80.369,17.225 77.87,8.49 77.84,8.385 77.818,8.285 77.788,8.181L2.629,43.513c-1.404,0.66 -2.019,2.358 -1.372,3.791 0.472,1.046 1.485,1.663 2.545,1.663 0.392,0 0.79,-0.084 1.169,-0.263L74.045,16.235c-1.873,3.578 -2.997,7.611 -3.228,11.885L35.566,51.732c-1.294,0.866 -1.653,2.638 -0.805,3.957 0.538,0.836 1.431,1.29 2.344,1.29 0.526,0 1.06,-0.151 1.533,-0.469L71.189,34.707c1.4,8.11 6.135,15.044 12.715,19.285l-54.945,68.337c-0.981,1.22 -0.808,3.021 0.388,4.023 0.521,0.437 1.149,0.649 1.775,0.649 0.809,0 1.611,-0.357 2.166,-1.045L89.056,56.595c3.06,1.168 6.357,1.838 9.811,1.838 4.932,0 9.566,-1.312 13.601,-3.597L84.602,118.323c-0.633,1.44 0,3.131 1.41,3.777 0.372,0.17 0.76,0.25 1.144,0.25 1.07,0 2.092,-0.629 2.556,-1.691L120.402,50.739C111.642,48.416 102.042,42.555 93.876,34.3Z" android:pathData="M93.876,34.3C86.053,26.391 80.369,17.225 77.87,8.49 77.84,8.385 77.818,8.285 77.788,8.181L2.629,43.513c-1.404,0.66 -2.019,2.358 -1.372,3.791 0.472,1.046 1.485,1.663 2.545,1.663 0.392,0 0.79,-0.084 1.169,-0.263L74.045,16.235c-1.873,3.578 -2.997,7.611 -3.228,11.885L35.566,51.732c-1.294,0.866 -1.653,2.638 -0.805,3.957 0.538,0.836 1.431,1.29 2.344,1.29 0.526,0 1.06,-0.151 1.533,-0.469L71.189,34.707c1.4,8.11 6.135,15.044 12.715,19.285l-54.945,68.337c-0.981,1.22 -0.808,3.021 0.388,4.023 0.521,0.437 1.149,0.649 1.775,0.649 0.809,0 1.611,-0.357 2.166,-1.045L89.056,56.595c3.06,1.168 6.357,1.838 9.811,1.838 4.932,0 9.566,-1.312 13.601,-3.597L84.602,118.323c-0.633,1.44 0,3.131 1.41,3.777 0.372,0.17 0.76,0.25 1.144,0.25 1.07,0 2.092,-0.629 2.556,-1.691L120.402,50.739C111.642,48.416 102.042,42.555 93.876,34.3Z" />
tools:ignore="VectorPath" />
</group> </group>
</group> </group>
</vector> </vector>
@@ -1,9 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="@color/themeLight" android:fillColor="@android:color/white"
android:pathData="M2,21h19v-3H2v3zM20,8H3c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h17c0.55,0 1,-0.45 1,-1V9c0,-0.55 -0.45,-1 -1,-1zM2,3v3h19V3H2z" /> android:pathData="M2,21h19v-3H2v3zM20,8H3c-0.55,0 -1,0.45 -1,1v6c0,0.55 0.45,1 1,1h17c0.55,0 1,-0.45 1,-1V9c0,-0.55 -0.45,-1 -1,-1zM2,3v3h19V3H2z" />
</vector> </vector>
@@ -1,9 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="@color/themeLight" android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z" /> android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z" />
</vector> </vector>
-9
View File
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/themeLight"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
</vector>
+23
View File
@@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:fillColor="@android:color/white"
android:pathData="m105.138,114.121a2.864,2.864 90,0 0,1.701 -0.85l19.334,-19.331a2.864,2.864 90,0 0,-0 -4.027l-26.584,-26.625a2.864,2.864 90,0 0,-1.835 -0.805l-7.922,-0.626 5.773,-5.772a2.864,2.864 90,0 0,-0 -4.027l-19.602,-19.644a2.864,2.864 90,0 0,-4.073 -0l-5.773,5.772 -0.627,-7.92a2.864,2.864 90,0 0,-0.805 -1.835l-26.629,-26.58a2.864,2.864 90,0 0,-4.073 -0l-19.289,19.286a2.864,2.864 90,0 0,-0 4.072l26.584,26.625a2.864,2.864 90,0 0,1.835 0.805l7.922,0.626 -5.773,5.772a2.864,2.864 90,0 0,-0 4.072l19.603,19.599a2.864,2.864 90,0 0,4.073 -0l5.773,-5.772 0.626,7.92a2.864,2.864 90,0 0,0.806 1.835l26.629,26.58a2.864,2.864 90,0 0,2.059 0.85,2.864 2.864,90 0,0 0.269,-0zM104.825,107.23 L80.97,83.38 80.165,72.685 85.58,67.271 96.276,68.121 120.13,91.926zM55.326,47.85 L44.629,47 20.775,23.194 36.081,7.891 59.891,31.741 60.741,42.436z" />
<path
android:fillColor="@android:color/white"
android:pathData="m62.653,115.806a2.864,2.864 90,1 0,-0 -5.727c-11.457,-0 -22.876,-4.351 -31.634,-13.107 -8.741,-8.74 -13.095,-20.174 -13.095,-31.614a2.864,2.864 90,1 0,-5.728 -0c-0,12.897 4.935,25.829 14.773,35.664 9.85,9.848 22.772,14.785 35.684,14.785z" />
<path
android:fillColor="@android:color/white"
android:pathData="m62.653,127a2.864,2.864 90,1 0,-0 -5.727c-14.325,-0 -28.612,-5.441 -39.547,-16.375 -10.935,-10.933 -16.378,-25.233 -16.378,-39.541a2.864,2.864 90,1 0,-5.728 -0c-0,15.763 6.025,31.562 18.055,43.59 12.029,12.028 27.818,18.052 43.597,18.052z" />
<path
android:fillColor="@android:color/white"
android:pathData="m62.653,104.698a2.864,2.864 90,1 0,-0 -5.727c-8.62,-0 -17.201,-3.263 -23.778,-9.839 -6.577,-6.576 -9.855,-15.174 -9.855,-23.774a2.864,2.864 90,1 0,-5.728 -0c-0,10.061 3.861,20.153 11.533,27.824 7.672,7.671 17.756,11.517 27.829,11.517z" />
<path
android:fillColor="@android:color/white"
android:pathData="m62.653,93.475a2.864,2.864 90,1 0,-0 -5.727c-5.737,-0 -11.453,-2.174 -15.836,-6.557 -4.384,-4.383 -6.558,-10.098 -6.558,-15.834a2.864,2.864 90,1 0,-5.728 -0c-0,7.192 2.757,14.406 8.236,19.883 5.479,5.478 12.693,8.234 19.887,8.234z" />
</vector>
@@ -1,14 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:fillColor="@color/themeLight"
android:pathData="m96.642,31.504c7.911,7.997 17.143,13.584 25.365,15.581l1.754,-3.997C123.78,43.048 123.786,43.005 123.802,42.965 125.834,38.995 127,34.495 127,29.715 127,13.881 114.379,1 98.867,1c-5.125,0 -9.92,1.429 -14.064,3.884l-3.398,1.598c0.08,0.306 0.133,0.599 0.221,0.91 2.32,8.104 7.652,16.667 15.016,24.112z" />
<path
android:fillColor="@color/themeLight"
android:pathData="M93.876,34.3C86.053,26.391 80.369,17.225 77.87,8.49 77.84,8.385 77.818,8.285 77.788,8.181L2.629,43.513c-1.404,0.66 -2.019,2.358 -1.372,3.791 0.472,1.046 1.485,1.663 2.545,1.663 0.392,0 0.79,-0.084 1.169,-0.263L74.045,16.235c-1.873,3.578 -2.997,7.611 -3.228,11.885L35.566,51.732c-1.294,0.866 -1.653,2.638 -0.805,3.957 0.538,0.836 1.431,1.29 2.344,1.29 0.526,0 1.06,-0.151 1.533,-0.469L71.189,34.707c1.4,8.11 6.135,15.044 12.715,19.285l-54.945,68.337c-0.981,1.22 -0.808,3.021 0.388,4.023 0.521,0.437 1.149,0.649 1.775,0.649 0.809,0 1.611,-0.357 2.166,-1.045L89.056,56.595c3.06,1.168 6.357,1.838 9.811,1.838 4.932,0 9.566,-1.312 13.601,-3.597L84.602,118.323c-0.633,1.44 0,3.131 1.41,3.777 0.372,0.17 0.76,0.25 1.144,0.25 1.07,0 2.092,-0.629 2.556,-1.691L120.402,50.739C111.642,48.416 102.042,42.555 93.876,34.3Z"
tools:ignore="VectorPath" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/themeLight"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/themeLight"
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z" />
</vector>
+4 -2
View File
@@ -1,9 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="@color/themeLight" android:fillColor="@android:color/white"
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z" /> android:pathData="M8,6v14l11,-7z" />
</vector> </vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/themeLight"
android:pathData="M21,19V20H3V19L5,17V11C5,7.9 7.03,5.17 10,4.29C10,4.19 10,4.1 10,4A2,2 0 0,1 12,2A2,2 0 0,1 14,4C14,4.1 14,4.19 14,4.29C16.97,5.17 19,7.9 19,11V17L21,19M14,21A2,2 0 0,1 12,23A2,2 0 0,1 10,21" />
</vector>
@@ -1,9 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="@color/themeLight" android:fillColor="@color/accent"
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z" /> android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z" />
</vector> </vector>
@@ -1,9 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="@color/themeLight" android:fillColor="@android:color/white"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /> android:pathData="M16,6v14l-11,-7z" />
</vector> </vector>
+3 -1
View File
@@ -1,9 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="@color/themeLight" android:fillColor="@android:color/white"
android:pathData="M19,8l-4,4h3c0,3.31 -2.69,6 -6,6 -1.01,0 -1.97,-0.25 -2.8,-0.7l-1.46,1.46C8.97,19.54 10.43,20 12,20c4.42,0 8,-3.58 8,-8h3l-4,-4zM6,12c0,-3.31 2.69,-6 6,-6 1.01,0 1.97,0.25 2.8,0.7l1.46,-1.46C15.03,4.46 13.57,4 12,4c-4.42,0 -8,3.58 -8,8H1l4,4 4,-4H6z" /> android:pathData="M19,8l-4,4h3c0,3.31 -2.69,6 -6,6 -1.01,0 -1.97,-0.25 -2.8,-0.7l-1.46,1.46C8.97,19.54 10.43,20 12,20c4.42,0 8,-3.58 8,-8h3l-4,-4zM6,12c0,-3.31 2.69,-6 6,-6 1.01,0 1.97,0.25 2.8,0.7l1.46,-1.46C15.03,4.46 13.57,4 12,4c-4.42,0 -8,3.58 -8,8H1l4,4 4,-4H6z" />
</vector> </vector>
+6 -15
View File
@@ -1,23 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="128" android:viewportWidth="128"
android:viewportHeight="128"> android:viewportHeight="128">
<path <path
android:fillColor="@color/themeLight" android:fillColor="@android:color/white"
android:pathData="m105.138,114.121a2.864,2.864 90,0 0,1.701 -0.85l19.334,-19.331a2.864,2.864 90,0 0,-0 -4.027l-26.584,-26.625a2.864,2.864 90,0 0,-1.835 -0.805l-7.922,-0.626 5.773,-5.772a2.864,2.864 90,0 0,-0 -4.027l-19.602,-19.644a2.864,2.864 90,0 0,-4.073 -0l-5.773,5.772 -0.627,-7.92a2.864,2.864 90,0 0,-0.805 -1.835l-26.629,-26.58a2.864,2.864 90,0 0,-4.073 -0l-19.289,19.286a2.864,2.864 90,0 0,-0 4.072l26.584,26.625a2.864,2.864 90,0 0,1.835 0.805l7.922,0.626 -5.773,5.772a2.864,2.864 90,0 0,-0 4.072l19.603,19.599a2.864,2.864 90,0 0,4.073 -0l5.773,-5.772 0.626,7.92a2.864,2.864 90,0 0,0.806 1.835l26.629,26.58a2.864,2.864 90,0 0,2.059 0.85,2.864 2.864,90 0,0 0.269,-0zM104.825,107.23 L80.97,83.38 80.165,72.685 85.58,67.271 96.276,68.121 120.13,91.926zM55.326,47.85 L44.629,47 20.775,23.194 36.081,7.891 59.891,31.741 60.741,42.436z" android:pathData="m96.642,31.504c7.911,7.997 17.143,13.584 25.365,15.581l1.754,-3.997C123.78,43.048 123.786,43.005 123.802,42.965 125.834,38.995 127,34.495 127,29.715 127,13.881 114.379,1 98.867,1c-5.125,0 -9.92,1.429 -14.064,3.884l-3.398,1.598c0.08,0.306 0.133,0.599 0.221,0.91 2.32,8.104 7.652,16.667 15.016,24.112z" />
tools:ignore="VectorPath" />
<path <path
android:fillColor="@color/themeLight" android:fillColor="@android:color/white"
android:pathData="m62.653,115.806a2.864,2.864 90,1 0,-0 -5.727c-11.457,-0 -22.876,-4.351 -31.634,-13.107 -8.741,-8.74 -13.095,-20.174 -13.095,-31.614a2.864,2.864 90,1 0,-5.728 -0c-0,12.897 4.935,25.829 14.773,35.664 9.85,9.848 22.772,14.785 35.684,14.785z" /> android:pathData="M93.876,34.3C86.053,26.391 80.369,17.225 77.87,8.49 77.84,8.385 77.818,8.285 77.788,8.181L2.629,43.513c-1.404,0.66 -2.019,2.358 -1.372,3.791 0.472,1.046 1.485,1.663 2.545,1.663 0.392,0 0.79,-0.084 1.169,-0.263L74.045,16.235c-1.873,3.578 -2.997,7.611 -3.228,11.885L35.566,51.732c-1.294,0.866 -1.653,2.638 -0.805,3.957 0.538,0.836 1.431,1.29 2.344,1.29 0.526,0 1.06,-0.151 1.533,-0.469L71.189,34.707c1.4,8.11 6.135,15.044 12.715,19.285l-54.945,68.337c-0.981,1.22 -0.808,3.021 0.388,4.023 0.521,0.437 1.149,0.649 1.775,0.649 0.809,0 1.611,-0.357 2.166,-1.045L89.056,56.595c3.06,1.168 6.357,1.838 9.811,1.838 4.932,0 9.566,-1.312 13.601,-3.597L84.602,118.323c-0.633,1.44 0,3.131 1.41,3.777 0.372,0.17 0.76,0.25 1.144,0.25 1.07,0 2.092,-0.629 2.556,-1.691L120.402,50.739C111.642,48.416 102.042,42.555 93.876,34.3Z" />
<path
android:fillColor="@color/themeLight"
android:pathData="m62.653,127a2.864,2.864 90,1 0,-0 -5.727c-14.325,-0 -28.612,-5.441 -39.547,-16.375 -10.935,-10.933 -16.378,-25.233 -16.378,-39.541a2.864,2.864 90,1 0,-5.728 -0c-0,15.763 6.025,31.562 18.055,43.59 12.029,12.028 27.818,18.052 43.597,18.052z" />
<path
android:fillColor="@color/themeLight"
android:pathData="m62.653,104.698a2.864,2.864 90,1 0,-0 -5.727c-8.62,-0 -17.201,-3.263 -23.778,-9.839 -6.577,-6.576 -9.855,-15.174 -9.855,-23.774a2.864,2.864 90,1 0,-5.728 -0c-0,10.061 3.861,20.153 11.533,27.824 7.672,7.671 17.756,11.517 27.829,11.517z" />
<path
android:fillColor="@color/themeLight"
android:pathData="m62.653,93.475a2.864,2.864 90,1 0,-0 -5.727c-5.737,-0 -11.453,-2.174 -15.836,-6.557 -4.384,-4.383 -6.558,-10.098 -6.558,-15.834a2.864,2.864 90,1 0,-5.728 -0c-0,7.192 2.757,14.406 8.236,19.883 5.479,5.478 12.693,8.234 19.887,8.234z" />
</vector> </vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/themeLight"
android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
</vector>
+13
View File
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="200dp"
android:viewportWidth="210"
android:viewportHeight="220">
<group
android:translateX="52"
android:translateY="44">
<path
android:fillColor="@color/accent"
android:pathData="m104.005,111.936q0.504,0 0.958,-0.063 0.467,-0.076 0.87,-0.176l0,1.674q-0.202,0.101 -0.555,0.214 -0.34,0.113 -0.807,0.201 -0.454,0.101 -1.034,0.164 -0.58,0.063 -1.261,0.063 -0.87,0 -1.576,-0.214 -0.706,-0.201 -1.198,-0.68 -0.492,-0.491 -0.757,-1.259 -0.265,-0.78 -0.265,-1.913l0,-8.244l-1.942,0l0,-1.322q0.832,0 1.488,-0.327 0.656,-0.327 1.047,-0.755 0.794,-0.831 1.185,-2.555l2.144,0l0,3.197l3.304,0l0,1.762l-3.304,0l0,7.98q0,1.171 0.391,1.712 0.404,0.541 1.311,0.541zM85.595,109.922q0,1.108 0.353,1.661 0.353,0.554 1.11,0.554 0.555,0 0.996,-0.227 0.454,-0.239 0.769,-0.667 0.328,-0.428 0.492,-1.032 0.177,-0.617 0.177,-1.372l0,-1.938l-1.16,0.076q-0.769,0.038 -1.299,0.252 -0.53,0.201 -0.845,0.566 -0.315,0.365 -0.454,0.894 -0.139,0.529 -0.139,1.233zM87.89,101.188q-0.492,0 -0.832,0.214 -0.328,0.201 -0.53,0.579 -0.202,0.365 -0.29,0.881 -0.076,0.503 -0.076,1.108 -1.74,0 -2.623,-0.39 -0.87,-0.39 -0.87,-1.334 0,-0.705 0.416,-1.208 0.429,-0.503 1.147,-0.818 0.731,-0.315 1.69,-0.453 0.971,-0.151 2.068,-0.151 1.362,0 2.371,0.239 1.021,0.227 1.69,0.743 0.681,0.516 1.021,1.359 0.34,0.831 0.34,2.039l0,6.57q0,0.529 0.076,0.881 0.088,0.352 0.265,0.566 0.177,0.201 0.479,0.289 0.303,0.088 0.731,0.088l0.101,0l0,1.372l-4.804,0l-0.542,-1.775l-0.227,0q-0.429,0.516 -0.807,0.906 -0.378,0.378 -0.807,0.629 -0.429,0.252 -0.971,0.365 -0.53,0.126 -1.261,0.126 -0.857,0 -1.601,-0.252 -0.731,-0.252 -1.286,-0.768 -0.542,-0.529 -0.857,-1.309 -0.315,-0.793 -0.315,-1.863 0,-2.102 1.438,-3.109 1.45,-1.007 4.35,-1.095l2.118,-0.076l0,-1.422q0,-0.642 -0.05,-1.183 -0.05,-0.541 -0.214,-0.931 -0.164,-0.39 -0.492,-0.604 -0.315,-0.214 -0.845,-0.214zM72.115,114.013q-1.74,0 -2.9,-0.327 -1.16,-0.327 -1.866,-0.843 -0.694,-0.516 -0.996,-1.158 -0.29,-0.642 -0.29,-1.271 0,-0.667 0.24,-1.12 0.252,-0.453 0.656,-0.743 0.416,-0.289 0.933,-0.415 0.53,-0.126 1.097,-0.126 0,1.108 0.265,1.951 0.265,0.831 0.731,1.397 0.467,0.554 1.11,0.843 0.656,0.277 1.425,0.277 0.757,0 1.349,-0.214 0.593,-0.214 1.009,-0.592 0.416,-0.378 0.63,-0.881 0.214,-0.516 0.214,-1.108 0,-0.654 -0.265,-1.158 -0.265,-0.516 -0.807,-0.944 -0.53,-0.441 -1.349,-0.831 -0.807,-0.403 -1.891,-0.856 -1.349,-0.554 -2.282,-1.133 -0.921,-0.592 -1.488,-1.271 -0.567,-0.68 -0.82,-1.473 -0.24,-0.793 -0.24,-1.737 0,-1.183 0.479,-2.14 0.479,-0.957 1.324,-1.636 0.845,-0.68 2.005,-1.045 1.16,-0.365 2.522,-0.365 1.425,0 2.459,0.239 1.034,0.227 1.702,0.629 0.681,0.403 1.009,0.957 0.328,0.541 0.328,1.171 0,0.453 -0.202,0.843 -0.189,0.39 -0.593,0.68 -0.391,0.289 -0.996,0.453 -0.593,0.164 -1.387,0.164 0,-0.541 -0.151,-1.158 -0.139,-0.629 -0.467,-1.158 -0.328,-0.541 -0.857,-0.894 -0.517,-0.352 -1.261,-0.352 -0.517,0 -0.984,0.151 -0.467,0.151 -0.82,0.453 -0.34,0.302 -0.542,0.755 -0.202,0.441 -0.202,1.019 0,0.541 0.177,1.019 0.177,0.478 0.643,0.944 0.479,0.453 1.299,0.919 0.832,0.453 2.144,0.982 1.324,0.541 2.257,1.108 0.933,0.566 1.526,1.221 0.605,0.642 0.87,1.422 0.277,0.768 0.277,1.712 0,1.246 -0.492,2.291 -0.492,1.032 -1.412,1.775 -0.921,0.743 -2.219,1.158 -1.286,0.415 -2.9,0.415zM62.115,108.966l0,1.573q0,0.604 0.164,0.969 0.177,0.365 0.454,0.566 0.29,0.189 0.681,0.252 0.391,0.063 0.82,0.063l0.328,0l0,1.372l-9.054,0l0,-1.372l0.719,0q0.441,0 0.82,-0.063 0.391,-0.063 0.668,-0.252 0.29,-0.201 0.454,-0.566 0.177,-0.365 0.177,-0.969l0,-1.573l-7.15,0l0,-1.573l7.326,-12.032l3.594,0l0,11.906l2.686,0l0,1.699zM58.345,102.761q0,-1.133 0.038,-2.442 0.038,-1.309 0.139,-2.517 -0.126,0.302 -0.404,0.831 -0.277,0.529 -0.593,1.095 -0.315,0.554 -0.605,1.057 -0.29,0.491 -0.441,0.73l-3.48,5.752l5.347,0zM50.855,113.761l-0.366,0q-1.412,0 -2.371,-0.063 -0.958,-0.063 -1.652,-0.289 -0.694,-0.227 -1.198,-0.68 -0.504,-0.466 -1.009,-1.259l-2.295,-3.612 -1.034,0.768l0,2.089q0,0.554 0.177,0.881 0.189,0.327 0.454,0.503 0.277,0.176 0.605,0.239 0.328,0.05 0.605,0.05l0.303,0l0,1.372L34.866,113.761l0,-1.372l0.303,0q0.29,0 0.605,-0.05 0.328,-0.063 0.593,-0.239 0.277,-0.176 0.454,-0.503 0.189,-0.327 0.189,-0.881l0,-13.606q0,-0.516 -0.189,-0.818 -0.177,-0.315 -0.454,-0.478 -0.277,-0.176 -0.605,-0.227 -0.315,-0.05 -0.593,-0.05l-0.303,0l0,-1.359l6.065,0l0,9.943q0,0.352 -0.025,0.818 -0.013,0.453 -0.038,0.868 -0.038,0.491 -0.063,0.994l3.253,-3.637q0.429,-0.478 0.555,-0.755 0.139,-0.289 0.139,-0.478 0,-0.327 -0.353,-0.503 -0.34,-0.189 -0.984,-0.189l0,-1.296l6.507,0l0,1.296q-0.895,0 -1.765,0.529 -0.857,0.516 -1.879,1.661l-1.627,1.85 3.43,5.11q0.681,1.019 1.374,1.51 0.694,0.491 1.311,0.491l0.088,0zM33.517,106.31q0,1.007 -0.29,1.901 -0.29,0.894 -0.82,1.573 -0.517,0.667 -1.261,1.057 -0.731,0.39 -1.627,0.39 -1.198,0 -2.333,-0.73 -1.122,-0.743 -2.055,-2.417 -0.794,1.523 -1.828,2.303 -1.034,0.768 -2.181,0.768 -0.883,0 -1.627,-0.34 -0.744,-0.352 -1.286,-0.982 -0.542,-0.642 -0.845,-1.548 -0.29,-0.919 -0.29,-2.052 0,-1.032 0.29,-1.926 0.29,-0.906 0.82,-1.561 0.53,-0.667 1.274,-1.045 0.744,-0.39 1.664,-0.39 0.593,0 1.173,0.176 0.593,0.164 1.147,0.541 0.567,0.365 1.072,0.944 0.517,0.579 0.946,1.41 0.794,-1.498 1.791,-2.24 0.996,-0.755 2.295,-0.755Line truncated
</group>
</vector>
@@ -1,11 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="192dp"
android:height="200dp"
android:viewportWidth="105.83323"
android:viewportHeight="114.01299">
<path
android:fillColor="@color/themeLight"
android:pathData="m104.005,111.936q0.504,0 0.958,-0.063 0.467,-0.076 0.87,-0.176l0,1.674q-0.202,0.101 -0.555,0.214 -0.34,0.113 -0.807,0.201 -0.454,0.101 -1.034,0.164 -0.58,0.063 -1.261,0.063 -0.87,0 -1.576,-0.214 -0.706,-0.201 -1.198,-0.68 -0.492,-0.491 -0.757,-1.259 -0.265,-0.78 -0.265,-1.913l0,-8.244l-1.942,0l0,-1.322q0.832,0 1.488,-0.327 0.656,-0.327 1.047,-0.755 0.794,-0.831 1.185,-2.555l2.144,0l0,3.197l3.304,0l0,1.762l-3.304,0l0,7.98q0,1.171 0.391,1.712 0.404,0.541 1.311,0.541zM85.595,109.922q0,1.108 0.353,1.661 0.353,0.554 1.11,0.554 0.555,0 0.996,-0.227 0.454,-0.239 0.769,-0.667 0.328,-0.428 0.492,-1.032 0.177,-0.617 0.177,-1.372l0,-1.938l-1.16,0.076q-0.769,0.038 -1.299,0.252 -0.53,0.201 -0.845,0.566 -0.315,0.365 -0.454,0.894 -0.139,0.529 -0.139,1.233zM87.89,101.188q-0.492,0 -0.832,0.214 -0.328,0.201 -0.53,0.579 -0.202,0.365 -0.29,0.881 -0.076,0.503 -0.076,1.108 -1.74,0 -2.623,-0.39 -0.87,-0.39 -0.87,-1.334 0,-0.705 0.416,-1.208 0.429,-0.503 1.147,-0.818 0.731,-0.315 1.69,-0.453 0.971,-0.151 2.068,-0.151 1.362,0 2.371,0.239 1.021,0.227 1.69,0.743 0.681,0.516 1.021,1.359 0.34,0.831 0.34,2.039l0,6.57q0,0.529 0.076,0.881 0.088,0.352 0.265,0.566 0.177,0.201 0.479,0.289 0.303,0.088 0.731,0.088l0.101,0l0,1.372l-4.804,0l-0.542,-1.775l-0.227,0q-0.429,0.516 -0.807,0.906 -0.378,0.378 -0.807,0.629 -0.429,0.252 -0.971,0.365 -0.53,0.126 -1.261,0.126 -0.857,0 -1.601,-0.252 -0.731,-0.252 -1.286,-0.768 -0.542,-0.529 -0.857,-1.309 -0.315,-0.793 -0.315,-1.863 0,-2.102 1.438,-3.109 1.45,-1.007 4.35,-1.095l2.118,-0.076l0,-1.422q0,-0.642 -0.05,-1.183 -0.05,-0.541 -0.214,-0.931 -0.164,-0.39 -0.492,-0.604 -0.315,-0.214 -0.845,-0.214zM72.115,114.013q-1.74,0 -2.9,-0.327 -1.16,-0.327 -1.866,-0.843 -0.694,-0.516 -0.996,-1.158 -0.29,-0.642 -0.29,-1.271 0,-0.667 0.24,-1.12 0.252,-0.453 0.656,-0.743 0.416,-0.289 0.933,-0.415 0.53,-0.126 1.097,-0.126 0,1.108 0.265,1.951 0.265,0.831 0.731,1.397 0.467,0.554 1.11,0.843 0.656,0.277 1.425,0.277 0.757,0 1.349,-0.214 0.593,-0.214 1.009,-0.592 0.416,-0.378 0.63,-0.881 0.214,-0.516 0.214,-1.108 0,-0.654 -0.265,-1.158 -0.265,-0.516 -0.807,-0.944 -0.53,-0.441 -1.349,-0.831 -0.807,-0.403 -1.891,-0.856 -1.349,-0.554 -2.282,-1.133 -0.921,-0.592 -1.488,-1.271 -0.567,-0.68 -0.82,-1.473 -0.24,-0.793 -0.24,-1.737 0,-1.183 0.479,-2.14 0.479,-0.957 1.324,-1.636 0.845,-0.68 2.005,-1.045 1.16,-0.365 2.522,-0.365 1.425,0 2.459,0.239 1.034,0.227 1.702,0.629 0.681,0.403 1.009,0.957 0.328,0.541 0.328,1.171 0,0.453 -0.202,0.843 -0.189,0.39 -0.593,0.68 -0.391,0.289 -0.996,0.453 -0.593,0.164 -1.387,0.164 0,-0.541 -0.151,-1.158 -0.139,-0.629 -0.467,-1.158 -0.328,-0.541 -0.857,-0.894 -0.517,-0.352 -1.261,-0.352 -0.517,0 -0.984,0.151 -0.467,0.151 -0.82,0.453 -0.34,0.302 -0.542,0.755 -0.202,0.441 -0.202,1.019 0,0.541 0.177,1.019 0.177,0.478 0.643,0.944 0.479,0.453 1.299,0.919 0.832,0.453 2.144,0.982 1.324,0.541 2.257,1.108 0.933,0.566 1.526,1.221 0.605,0.642 0.87,1.422 0.277,0.768 0.277,1.712 0,1.246 -0.492,2.291 -0.492,1.032 -1.412,1.775 -0.921,0.743 -2.219,1.158 -1.286,0.415 -2.9,0.415zM62.115,108.966l0,1.573q0,0.604 0.164,0.969 0.177,0.365 0.454,0.566 0.29,0.189 0.681,0.252 0.391,0.063 0.82,0.063l0.328,0l0,1.372l-9.054,0l0,-1.372l0.719,0q0.441,0 0.82,-0.063 0.391,-0.063 0.668,-0.252 0.29,-0.201 0.454,-0.566 0.177,-0.365 0.177,-0.969l0,-1.573l-7.15,0l0,-1.573l7.326,-12.032l3.594,0l0,11.906l2.686,0l0,1.699zM58.345,102.761q0,-1.133 0.038,-2.442 0.038,-1.309 0.139,-2.517 -0.126,0.302 -0.404,0.831 -0.277,0.529 -0.593,1.095 -0.315,0.554 -0.605,1.057 -0.29,0.491 -0.441,0.73l-3.48,5.752l5.347,0zM50.855,113.761l-0.366,0q-1.412,0 -2.371,-0.063 -0.958,-0.063 -1.652,-0.289 -0.694,-0.227 -1.198,-0.68 -0.504,-0.466 -1.009,-1.259l-2.295,-3.612 -1.034,0.768l0,2.089q0,0.554 0.177,0.881 0.189,0.327 0.454,0.503 0.277,0.176 0.605,0.239 0.328,0.05 0.605,0.05l0.303,0l0,1.372L34.866,113.761l0,-1.372l0.303,0q0.29,0 0.605,-0.05 0.328,-0.063 0.593,-0.239 0.277,-0.176 0.454,-0.503 0.189,-0.327 0.189,-0.881l0,-13.606q0,-0.516 -0.189,-0.818 -0.177,-0.315 -0.454,-0.478 -0.277,-0.176 -0.605,-0.227 -0.315,-0.05 -0.593,-0.05l-0.303,0l0,-1.359l6.065,0l0,9.943q0,0.352 -0.025,0.818 -0.013,0.453 -0.038,0.868 -0.038,0.491 -0.063,0.994l3.253,-3.637q0.429,-0.478 0.555,-0.755 0.139,-0.289 0.139,-0.478 0,-0.327 -0.353,-0.503 -0.34,-0.189 -0.984,-0.189l0,-1.296l6.507,0l0,1.296q-0.895,0 -1.765,0.529 -0.857,0.516 -1.879,1.661l-1.627,1.85 3.43,5.11q0.681,1.019 1.374,1.51 0.694,0.491 1.311,0.491l0.088,0zM33.517,106.31q0,1.007 -0.29,1.901 -0.29,0.894 -0.82,1.573 -0.517,0.667 -1.261,1.057 -0.731,0.39 -1.627,0.39 -1.198,0 -2.333,-0.73 -1.122,-0.743 -2.055,-2.417 -0.794,1.523 -1.828,2.303 -1.034,0.768 -2.181,0.768 -0.883,0 -1.627,-0.34 -0.744,-0.352 -1.286,-0.982 -0.542,-0.642 -0.845,-1.548 -0.29,-0.919 -0.29,-2.052 0,-1.032 0.29,-1.926 0.29,-0.906 0.82,-1.561 0.53,-0.667 1.274,-1.045 0.744,-0.39 1.664,-0.39 0.593,0 1.173,0.176 0.593,0.164 1.147,0.541 0.567,0.365 1.072,0.944 0.517,0.579 0.946,1.41 0.794,-1.498 1.791,-2.24 0.996,-0.755 2.295,-0.755 0.8Line truncated
tools:ignore="VectorPath" />
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M14,21c1.93,0 3.62,-1.17 4,-3l-1.75,-0.88C16,18.21 15.33,19 14,19l-4.9,0c0.83,-1 1.5,-2.34 1.5,-4c0,-0.35 -0.03,-0.69 -0.08,-1L14,14v-2l-4.18,0C9,10.42 8,9.6 8,8c0,-1.93 1.57,-3.5 3.5,-3.5c1.5,0 2.79,0.95 3.28,2.28L16.63,6c-0.8,-2.05 -2.79,-3.5 -5.13,-3.5C8.46,2.5 6,4.96 6,8c0,1.78 0.79,2.9 1.49,4L6,12v2l2.47,0c0.08,0.31 0.13,0.64 0.13,1c0,2.7 -2.6,4 -2.6,4v2H14z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/themeLight"
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/themeLight"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM17,13l-5,5 -5,-5h3V9h4v4h3z" />
</vector>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size
android:width="1dp"
android:height="1dp" />
<solid android:color="@color/background" />
</shape>
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke
android:width="0dp"
android:color="@color/mapInfoLayout" />
<solid android:color="@color/mapInfoLayout" />
<corners android:radius="8dp" />
</shape>
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<size
android:width="1dp"
android:height="2dp" />
<solid android:color="@color/greyDark" />
</shape>
</item>
</layer-list>
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<size
android:width="1dp"
android:height="0.5dp" />
<solid android:color="@color/greyDivider" />
</shape>
</item>
</layer-list>
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff. Show more