From dd3e954861f0cf9a7c98c58e08de44a6573de7f7 Mon Sep 17 00:00:00 2001 From: Alexander Heinrich Date: Tue, 11 Jun 2024 12:27:05 +0200 Subject: [PATCH 1/2] AirGuard Version 2.2 (#175) * improve LocationProvider, can handle COARSE_LOCATION, works with any Location Provider, improve readability and efficiency * small improvements to map rendering * make ObserveTracker functionality visible again * improve Observation Screen Design * ObserveTrackerWorker now calls ScanBluetoothWorker manually before checking Observation criteria. Clarify explanation how Observe Tracker works. * update gradle version * improve Background Worker Scheduling, add Error Case for ObserveTracker * add handling for inconsistent database entries in ObserveTracker, make changing ID Text only visible when the device can change the id * Tracker Observation can now be stopped, added visual indicator whether device is already being observed * add Article Fragment * fix crash in Play Sound Dialog * add navigation to article * Adding new API endpoint that deletes study data for a specific user * Setting request to DELETE method * implement Data Deletion in UI * fix api initialization bug, better initialization of api token, add explanation text to data deletion, data deletion leads to automatic study opt out * add first implementation of Online Article Download * add additional information in JSON * Article Download now Coroutine, loading animation while downloading JSON, remove old code * Design improvement * Article offline handling * support Online Articles in multiple languages * upgrade to gradle 8.1.3 * fix security issues in the AndroidManifest * update backup rules to confirm to android 12 standards * optimize Article Offline Handling and add old cards again * upgrade gradle and some libraries * speed up map rendering and add permission necessary for Android 14 * Rename SmartTags to be consistent with Samsungs current product lines * fix for common IllegalStateException in PlaySoundDialogFragment * add previewImage to the Article Function * Fix for Delete Study Data * Relevant Tracking Days can be set inividually for each Type of tracker, set for 2 Days for Chipolo and Tile * upgrade gradle to 8.2.0 * upgrade to Android 14 / SDK 34 * fix Crash when clicking on Scan Element * optimize BluetoothReceiver for Android 13 or higher * upgrade some plugins and dependencies * UI improvements: Manual Scan now more user friendly, more explanation, less technical terms * adjust articles for in production Server files * number of Locations can be set individually for each tracker, in default number of Locations is now depending on detection sensitivity * remove own location on map * NumberofLocations override can now be overwritten for every RiskLevel * separate the connected / low Risk devices from the offline / high risk devices in manual scan. Remove settings to toggle between scan type * separated ScanView is now a NestedScrollView * Manual Scan Behaviour now resembles the one from the iOS App * replace some of the deprecated Java Functions with backwards compatible replacements * upgrade gradle to 8.2.1 * Scanning behaviour of Tile Tracker now closer to iOS Version * Device Detail Page now vertical, similar to iOS Design * add Website Link to Tracker Detail Page * Update deprecated Code * change relevant days to relevant hours * Fixed an issue where the versionProvider was removed in the LocationProvidern * Update 2.1 for F-Droid * 2.0 F-Droid Release version * Fix Google Play badge in README.md The old endpoint is longer maintained and project archived: - https://github.com/cvzi/playshields The alternative made by the same author can simply replace the old one: - https://github.com/cvzi/play * Working on fixing SendStatisticsWorker * Changing proguard rules This fixes a bug caused by proguard, which would let any network connections fail. * Reducing duplicate devices uploads * F-Droid 2.1.1 Release * Updating F-Droid Metadata * Rebasing Main onto current branch * ObserveTrackerWorker now calls ScanBluetoothWorker manually before checking Observation criteria. Clarify explanation how Observe Tracker works. * Updating version in Manifest Updating Tests to run with new LocationProvider changes * some optimizations to the processing of manual scan items * some more optimizations to the processing of manual scan items, Device Detection with more error checks, temporarily remove Sorting Options for Manual Scan * A New notification for a device will only be sent if the device meets all the criteria for a notification again only considering events after the last notification. * upgrade Gradle and some dependecies * upgrade Kotlin Version to 1.9.22, more dependencies, migrate some functions to be compatible with new kotlin version * reintroduce Scan Sorting again * Detail Scan now shows correct Time since Tracker is in offline Mode * add Legend to map * upgrade some dependencies * upgrade some dependencies, modify dependency structure * hide Website URL Button if Website URL could not be determined * fix bug in sorting of manual scan, fix bug in article rendering in dashboard * adjust RiskLevel Numbers so they are consistent with the iOS Version * use separate Algorithm for Notifications and RiskLevel evaluation * optimize and Bugfix RiskLevelEvaluator * add feedback options other and not found. Add message success when submitting feedback. * save sensitivity level for every notification, save connectionState for every new beacon * devices that have not been seen in the last 30 days and beacons older than 30 days, both without a notification will get deleted * improve connectionState for chipolo devices * safe all devices no matter in what connectionState. Hide safe trackers in ui, so that visible behaviour stays the same. * log when and how many devices are getting deleted, small optimizations in deleting process * upgrade gradle to 8.3.0 and upgrade some dependencies * remove Battery from Distance Scan remove Sorting Options from Manual Scan remove Potential Trackers Label from Manual Scan * Show No trackers found since installation date, instead of last 14 days * fix string showing wrong device type in feedback View * Redesign Feedback, Fix Crashes in Feedback * make Feedback Button more visible, also improve some padding * add explanation text to device view that explains that some devices change their key * add explanation for safe trackers in manual scan * ignored trackers will be treated as safe trackers in manual scan * add Advanced Mode, that hides complicated to understand options and menus * when deleting old devices and beacons, locations that are not used anymore also get deleted * add Clustering to the map * highly optimize speed in loading map markers, update some dependencies * fix bug where numbers of locations being displayed were inconsistent * fix bug where map markers where not displayed correctly in TrackingFragment * automatic adjustments to the code * fix a bug where markers were not displayed correctly when accessed via a notification * optimize retrieval of markers from the database * Moving background scans to be scheduled by AlarmManager instead of background workers. - All Background Scans are now perform by the BackgroundBluetoothScanner - The ScanBluetoothWorker stays functional - Tracking Detection is still performed by a worker * Fixing map marker display issue: - On the TrackingFragment the map now shows the markers as expected. * fix merge conflict * add indices to Beacon and BaseDevice * Checking permission for setExact Alarm Using a wake lock on the BackgroundBluetoothScanner Replacing Beacon count with times seen * Fixing merge issues with git Testing Background scans with Alarm * upgrade gradle to 8.3.1, upgrade dependencies * replace some Strings with Plurals * fix crash when double tapping scan result * add caching to deviceType retrieval and fix bug in old Beacon deletion * manual scan keeps pause state while viewing scan items. Limit notifyDataSetChanged to at most once per second. * device name can be edited from device detail view by clicking on the device name * limit device name to 255 characters and one line, device name cannot be empty * revert savedConnections only to for Tracking Detection relevant * make corners of minimap rounded * add (currently not visible) simpler design for device view * altitude of each location now gets saved * update existing locations if more recent and more accurate data is available * upgrade gradle and some dependecies * upgrade Kotlin Version * do not save devices above an altitude of 9000 meters. This should avoid unnecessary detections on planes. * don't create location object above 3000 meters, this is different to not creating a device object above 9000 meters from the previous commit * upgrade gradle to 8.4.0 * fix bug where devices where not correctly put into safe and unsafe trackers * make some string more understandable * make padding of map more consistent * massive speed up of manual scan, cleanup code * replace ScanResult in manual scan with Wrapper Object. Massive Speed Improvements * Setting minimum detection events to 3 on medium security mode. Due to internal testing we found that this would lead to more reliable results * Updating gradle and manifest * Updating gradle and manifest * Testing app without FOREGROUND_SERVICE - Optimizing setAlarm for Android 14 * Rssi indicator image in manual scan updates if rssi value changes, update some dependencies * optimize Repository Loading in manual scan * further use of ScanResultWrapper in Backend. Put back Exact Alarm Permissions * fix TypeToken Error on some devices by updating proguard rules * add message in manual scan that Location Permission has to turned on * handle case if wakeLock cannot be held * upgrade gradle to 8.4.1. * avoid unnecessary duplicate calls of getDeviceType * low risk "secure tracker" explanation in manual scan is hidden if there are now low risk trackers * Show Website URL also for Low Risk Devices * Fix DeviceType Detection and Display of Explanation for it for unknown devices * fix bug in ScanDistance * fix bug in fetching deviceType from safe trackers * fix compilation bug * Adopting changes to set alarms more reliably * Removing second launch icon * Adding Scheduling alarms with new permission for Android 13 and Android 14 * F-Droid 2.2 --------- Co-authored-by: Dennis Arndt Co-authored-by: Peter Dave --- api.properties | 2 +- app/build.gradle | 72 +-- app/proguard-rules.pro | 13 +- .../13.json | 411 +++++++++++++++ .../14.json | 418 +++++++++++++++ .../15.json | 483 +++++++++++++++++ .../16.json | 489 ++++++++++++++++++ .../ExampleInstrumentedTest.kt | 36 +- .../LocationProviderTest.kt | 4 +- .../ScanBluetoothWorkerTest.kt | 53 +- .../at_tracking_detection/ScanDBTests.kt | 6 +- app/src/main/AndroidManifest.xml | 51 +- .../ATTrackingDetectionApplication.kt | 25 +- .../database/AppDatabase.kt | 10 +- .../database/Converters.kt | 1 - .../database/daos/BeaconDao.kt | 10 +- .../database/daos/DeviceDao.kt | 20 +- .../database/daos/LocationDao.kt | 16 +- .../database/daos/NotificationDao.kt | 9 +- .../database/daos/ScanDao.kt | 11 +- .../database/models/Beacon.kt | 26 +- .../database/models/Location.kt | 17 +- .../database/models/Notification.kt | 8 +- .../database/models/device/BaseDevice.kt | 69 ++- .../database/models/device/DeviceContext.kt | 16 + .../database/models/device/DeviceManager.kt | 99 ++-- .../database/models/device/DeviceType.kt | 60 +++ .../database/models/device/types/AirPods.kt | 3 + .../database/models/device/types/AirTag.kt | 4 + .../models/device/types/AppleDevice.kt | 3 + .../database/models/device/types/Chipolo.kt | 5 + .../database/models/device/types/FindMy.kt | 3 + .../models/device/types/SamsungDevice.kt | 24 +- .../database/models/device/types/SmartTag.kt | 7 +- .../models/device/types/SmartTagPlus.kt | 7 +- .../database/models/device/types/Tile.kt | 13 +- .../database/repository/BeaconRepository.kt | 8 +- .../database/repository/DeviceRepository.kt | 41 +- .../database/repository/LocationRepository.kt | 17 + .../database/repository/ScanRepository.kt | 18 +- .../viewmodel/NotificationViewModel.kt | 4 +- .../detection/BackgroundBluetoothScanner.kt | 473 +++++++++++++++++ .../detection/BluetoothReceiver.kt | 9 +- .../detection/LocationProvider.kt | 231 +++++---- .../detection/ScanBluetoothWorker.kt | 326 +----------- .../detection/TrackingDetectorConstants.kt | 2 + .../detection/TrackingDetectorWorker.kt | 88 +++- .../at_tracking_detection/hilt/ApiModule.kt | 2 +- .../hilt/DatabaseModule.kt | 44 +- .../notifications/NotificationBuilder.kt | 45 +- .../notifications/NotificationService.kt | 58 ++- .../ScheduledNotificationReceiver.kt | 3 +- .../statistics/SendStatisticsWorker.kt | 3 +- .../at_tracking_detection/ui/MainActivity.kt | 30 +- .../ui/MarkdownViewerActivity.kt | 45 -- .../ui/OnboardingActivity.kt | 6 +- .../ui/TrackingNotificationActivity.kt | 22 +- .../ui/dashboard/Article.kt | 96 ++++ .../ui/dashboard/ArticleFragment.kt | 88 ++++ .../ui/dashboard/DashboardRiskFragment.kt | 93 +++- .../ui/dashboard/DashboardViewModel.kt | 4 +- .../ui/dashboard/DeviceMapFragment.kt | 52 +- .../ui/dashboard/DeviceMapViewModel.kt | 2 +- .../ui/dashboard/RallyLineGraphChart.kt | 8 +- .../ui/dashboard/RiskCardViewModel.kt | 44 +- .../ui/dashboard/RiskDetailFragment.kt | 7 + .../ui/dashboard/RiskDetailViewModel.kt | 15 +- .../ui/dashboard/RiskRowViewModel.kt | 7 +- .../ui/debug/DebugLogViewModel.kt | 5 +- .../ui/debug/DebugScanViewModel.kt | 3 +- .../ui/debug/DebugScansFragment.kt | 18 +- .../ui/debug/DebugViewModel.kt | 9 +- .../ui/devices/AllDevicesFragment.kt | 4 - .../ui/devices/AllDevicesViewModel.kt | 2 +- .../ui/devices/DeviceAdapter.kt | 2 +- .../ui/devices/DevicesFragment.kt | 22 +- .../devices/filter/models/DeviceTypeFilter.kt | 4 +- .../ui/feedback/FeedbackFragment.kt | 91 +++- .../ui/feedback/LocationItem.kt | 7 + .../IgnoreBatteryOptimizationFragment.kt | 7 +- .../ui/onboarding/LocationFragment.kt | 20 +- .../ui/onboarding/ShareDataFragment.kt | 4 + .../ui/scan/BluetoothDeviceAdapter.kt | 69 +-- .../ui/scan/ScanDistanceFragment.kt | 105 ++-- .../ui/scan/ScanFragment.kt | 98 ++-- .../ui/scan/ScanResultWrapper.kt | 28 + .../ui/scan/ScanViewModel.kt | 163 +++--- .../ui/scan/dialog/DialogViewModel.kt | 7 +- .../ui/scan/dialog/PlaySoundDialogFragment.kt | 68 +-- .../ui/settings/AttributionAdapter.kt | 3 +- .../ui/settings/DataDeletionFragment.kt | 56 +- .../ui/settings/InformationFragment.kt | 1 - .../ui/settings/SettingsFragment.kt | 36 ++ .../ui/tracking/ObserveTrackerFragment.kt | 147 ++++-- .../ui/tracking/ObserveTrackerViewModel.kt | 3 +- .../ui/tracking/TrackingFragment.kt | 235 ++++++--- .../ui/tracking/TrackingViewModel.kt | 61 ++- .../util/BindingAdapter.kt | 91 ++-- .../at_tracking_detection/util/SharedPrefs.kt | 58 ++- .../at_tracking_detection/util/Utility.kt | 122 +++-- .../util/ble/BLEScanCallback.kt | 9 +- .../util/ble/BLEScanner.kt | 25 +- .../util/ble/OpportunisticBLEScanner.kt | 48 +- .../util/risk/RiskLevelEvaluator.kt | 57 +- .../worker/BackgroundWorkBuilder.kt | 5 + .../worker/BackgroundWorkScheduler.kt | 166 +++++- ...rkersWorker.kt => ObserveTrackerWorker.kt} | 18 +- .../worker/ScheduleWorkersReceiver.kt | 75 ++- .../SetExactAlarmPermissionChangedReceiver.kt | 17 + .../worker/WorkerConstants.kt | 1 + .../ic_baseline_admin_panel_settings_24.xml | 7 + .../res/drawable/ic_baseline_backpack_24.xml | 5 + .../drawable/ic_baseline_bike_scooter_24.xml | 9 + .../res/drawable/ic_baseline_cancel_24.xml | 5 + .../main/res/drawable/ic_baseline_car_24.xml | 5 + .../drawable/ic_baseline_more_horiz_24.xml | 5 + .../res/drawable/ic_baseline_person_24.xml | 7 + .../res/layout/activity_markdown_viewer.xml | 13 - app/src/main/res/layout/fragment_article.xml | 61 +++ .../res/layout/fragment_dashboard_risk.xml | 43 +- .../res/layout/fragment_data_deletion.xml | 5 + app/src/main/res/layout/fragment_debug.xml | 14 + .../main/res/layout/fragment_device_map.xml | 47 +- app/src/main/res/layout/fragment_feedback.xml | 12 +- .../res/layout/fragment_observe_tracker.xml | 31 +- .../main/res/layout/fragment_risk_detail.xml | 1 + app/src/main/res/layout/fragment_scan.xml | 192 +++---- .../res/layout/fragment_scan_distance.xml | 118 +++-- app/src/main/res/layout/fragment_tracking.xml | 330 +++++++----- .../main/res/layout/include_article_card.xml | 48 ++ .../res/layout/include_location_disabled.xml | 58 +++ app/src/main/res/layout/include_map.xml | 14 +- .../main/res/layout/include_tracking_tile.xml | 47 +- app/src/main/res/layout/item_device.xml | 8 +- .../res/layout/item_feedback_selection.xml | 33 ++ app/src/main/res/layout/item_scan_result.xml | 95 +--- .../main/res/navigation/main_navigation.xml | 34 ++ .../res/navigation/tracking_navigation.xml | 4 + app/src/main/res/values-de/strings.xml | 81 ++- app/src/main/res/values-ja/strings.xml | 4 +- app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 87 +++- .../main/res/xml/data_extraction_rules.xml | 7 + app/src/main/res/xml/fragment_settings.xml | 23 +- .../at_tracking_detection/ExampleUnitTest.kt | 34 +- build.gradle | 17 +- gradle.properties | 1 - gradle/wrapper/gradle-wrapper.properties | 2 +- 148 files changed, 5770 insertions(+), 2103 deletions(-) create mode 100644 app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/13.json create mode 100644 app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/14.json create mode 100644 app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/15.json create mode 100644 app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/16.json create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt delete mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/ui/MarkdownViewerActivity.kt create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/Article.kt create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ArticleFragment.kt create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/ui/feedback/LocationItem.kt create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanResultWrapper.kt rename app/src/main/java/de/seemoo/at_tracking_detection/worker/{ScheduleWorkersWorker.kt => ObserveTrackerWorker.kt} (76%) create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/worker/SetExactAlarmPermissionChangedReceiver.kt create mode 100644 app/src/main/res/drawable/ic_baseline_admin_panel_settings_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_backpack_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_bike_scooter_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_cancel_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_car_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_more_horiz_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_person_24.xml delete mode 100644 app/src/main/res/layout/activity_markdown_viewer.xml create mode 100644 app/src/main/res/layout/fragment_article.xml create mode 100644 app/src/main/res/layout/include_article_card.xml create mode 100644 app/src/main/res/layout/include_location_disabled.xml create mode 100644 app/src/main/res/layout/item_feedback_selection.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml diff --git a/api.properties b/api.properties index 5ce2e9b8..157eb729 100644 --- a/api.properties +++ b/api.properties @@ -1,2 +1,2 @@ -API_KEY="UCKgXwP.Dvmxyn1UygvnJfy2DV16OjbmHia4xXpd" +API_KEY="AGdk9qPF.Bl9yatnjjoy9WEEjXYGVewE7ZyJET9Yy" API_BASE_ADDRESS="https://tpe.seemoo.tu-darmstadt.de/api/" \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 55247519..103898b2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,16 +17,18 @@ android { viewBinding true dataBinding true compose true + buildConfig true } + compileSdk = 34 + defaultConfig { applicationId "de.seemoo.at_tracking_detection" minSdkVersion 28 - targetSdkVersion 33 - compileSdk 33 - versionCode 39 - versionName "2.1.1" - + targetSdk = 34 + versionCode 44 + versionName "2.2" + buildConfigField "String", "API_KEY", apiProperties["API_KEY"] buildConfigField "String", "API_BASE_ADDRESS", apiProperties["API_BASE_ADDRESS"] @@ -64,7 +66,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion "1.4.8" + kotlinCompilerExtensionVersion "1.5.11" } @@ -91,46 +93,50 @@ dependencies { implementation 'com.jakewharton.timber:timber:5.0.1' implementation 'com.github.bastienpaulfr:Treessence:1.0.0' - implementation 'androidx.work:work-runtime-ktx:2.8.1' - implementation 'androidx.core:core-ktx:1.10.1' + implementation "androidx.work:work-runtime-ktx:$work_version" + implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.9.0' + implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.vectordrawable:vectordrawable:1.1.0' - implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0' - implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' + implementation 'androidx.vectordrawable:vectordrawable:1.2.0' + implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7' + implementation 'androidx.navigation:navigation-ui-ktx:2.7.7' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.3.1' + implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' - implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.5' + implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' implementation 'com.google.code.gson:gson:2.10.1' implementation 'androidx.legacy:legacy-support-v4:1.0.0' - implementation 'androidx.work:work-testing:2.8.1' - implementation 'androidx.core:core-ktx:1.10.1' - debugImplementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.5' + implementation "androidx.work:work-testing:$work_version" + implementation 'androidx.core:core-ktx:1.13.1' + debugImplementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12' implementation "com.google.dagger:hilt-android:$hilt_version" - implementation 'androidx.hilt:hilt-work:1.0.0' - implementation 'androidx.hilt:hilt-navigation-fragment:1.0.0' + implementation 'androidx.hilt:hilt-work:1.2.0' + implementation 'androidx.hilt:hilt-navigation-fragment:1.2.0' implementation 'com.github.AppIntro:AppIntro:6.1.0' - implementation 'org.osmdroid:osmdroid-android:6.1.16' + implementation 'org.osmdroid:osmdroid-android:6.1.18' + implementation 'com.github.MKergall:osmbonuspack:6.9.0' implementation 'com.github.ybq:Android-SpinKit:1.4.0' implementation "com.mikepenz:aboutlibraries:$about_libraries_version" - implementation 'io.noties.markwon:core:4.6.2' + implementation 'com.github.mukeshsolanki:MarkdownView-Android:2.0.0' + + implementation 'com.github.bumptech.glide:glide:4.16.0' kapt "com.google.dagger:hilt-compiler:$hilt_version" - kapt 'androidx.hilt:hilt-compiler:1.0.0' + kapt 'androidx.hilt:hilt-compiler:1.2.0' implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" @@ -138,32 +144,32 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation 'androidx.room:room-testing:2.5.2' - androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' + androidTestImplementation "androidx.room:room-testing:$room_version" + androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0' androidTestImplementation 'androidx.test:core:1.5.0' androidTestImplementation 'androidx.test:core-ktx:1.5.0' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1' - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' //Finds memory leaks while running the app in Debug mode // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1' //Compose // Integration with activities - implementation 'androidx.activity:activity-compose:1.7.2' + implementation 'androidx.activity:activity-compose:1.9.0' // Compose Material Design - implementation 'androidx.compose.material:material:1.4.3' + implementation 'androidx.compose.material:material:1.6.7' // Animations - implementation 'androidx.compose.animation:animation:1.4.3' + implementation 'androidx.compose.animation:animation:1.6.7' // Tooling support (Previews, etc.) - implementation 'androidx.compose.ui:ui-tooling:1.4.3' + implementation 'androidx.compose.ui:ui-tooling:1.6.7' // Integration with ViewModels - implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0' // UI Tests - androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.4.3' + androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.6.7' // When using a MDC theme implementation "com.google.android.material:compose-theme-adapter:1.2.1" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index c1071cd2..ac1f9a7d 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -72,4 +72,15 @@ # R8 full mode strips generic signatures from return types if not kept. -keep,allowobfuscation,allowshrinking class retrofit2.Response --keep class de.seemoo.at_tracking_detection.** { *; } \ No newline at end of file +# Keep Gson classes +-keep class com.google.gson.reflect.TypeToken { *; } +-keep class com.google.gson.Gson { *; } +-keep class com.google.gson.TypeAdapter { *; } +-keep class com.google.gson.stream.JsonReader { *; } +-keep class com.google.gson.stream.JsonWriter { *; } + +# Ensure that the classes related to Article are not stripped +-keep class de.seemoo.at_tracking_detection.ui.dashboard.Article { *; } + +# Keep ProGuard/R8 from stripping out important methods or classes +-keep class * implements com.google.gson.reflect.TypeToken { *; } diff --git a/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/13.json b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/13.json new file mode 100644 index 00000000..0fb164e7 --- /dev/null +++ b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/13.json @@ -0,0 +1,411 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "d829378e4cbf1035e49924090f632174", + "entities": [ + { + "tableName": "device", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uniqueId` TEXT, `address` TEXT NOT NULL, `name` TEXT, `ignore` INTEGER NOT NULL, `connectable` INTEGER DEFAULT 0, `payloadData` INTEGER, `firstDiscovery` TEXT NOT NULL, `lastSeen` TEXT NOT NULL, `notificationSent` INTEGER NOT NULL, `lastNotificationSent` TEXT, `deviceType` TEXT, `riskLevel` INTEGER NOT NULL DEFAULT 0, `lastCalculatedRiskDate` TEXT, `nextObservationNotification` TEXT, `currentObservationDuration` INTEGER)", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uniqueId", + "columnName": "uniqueId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ignore", + "columnName": "ignore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "connectable", + "columnName": "connectable", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "payloadData", + "columnName": "payloadData", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "firstDiscovery", + "columnName": "firstDiscovery", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSeen", + "columnName": "lastSeen", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationSent", + "columnName": "notificationSent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationSent", + "columnName": "lastNotificationSent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deviceType", + "columnName": "deviceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "riskLevel", + "columnName": "riskLevel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCalculatedRiskDate", + "columnName": "lastCalculatedRiskDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "nextObservationNotification", + "columnName": "nextObservationNotification", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentObservationDuration", + "columnName": "currentObservationDuration", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "deviceId" + ] + }, + "indices": [ + { + "name": "index_device_address", + "unique": true, + "columnNames": [ + "address" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_device_address` ON `${TABLE_NAME}` (`address`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceAddress` TEXT NOT NULL, `falseAlarm` INTEGER NOT NULL, `dismissed` INTEGER, `clicked` INTEGER, `createdAt` TEXT NOT NULL, `sensitivity` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceAddress", + "columnName": "deviceAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "falseAlarm", + "columnName": "falseAlarm", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "clicked", + "columnName": "clicked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sensitivity", + "columnName": "sensitivity", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "notificationId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "beacon", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`beaconId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `receivedAt` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `deviceAddress` TEXT NOT NULL, `locationId` INTEGER, `mfg` BLOB, `serviceUUIDs` TEXT, `connectionState` TEXT NOT NULL DEFAULT 'UNKNOWN')", + "fields": [ + { + "fieldPath": "beaconId", + "columnName": "beaconId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "receivedAt", + "columnName": "receivedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceAddress", + "columnName": "deviceAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "manufacturerData", + "columnName": "mfg", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "serviceUUIDs", + "columnName": "serviceUUIDs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connectionState", + "columnName": "connectionState", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'UNKNOWN'" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "beaconId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "feedback", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`feedbackId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `notificationId` INTEGER NOT NULL, `location` TEXT)", + "fields": [ + { + "fieldPath": "feedbackId", + "columnName": "feedbackId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "feedbackId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "scan", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scanId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `endDate` TEXT, `noDevicesFound` INTEGER, `duration` INTEGER, `isManual` INTEGER NOT NULL, `scanMode` INTEGER NOT NULL, `startDate` TEXT)", + "fields": [ + { + "fieldPath": "scanId", + "columnName": "scanId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "noDevicesFound", + "columnName": "noDevicesFound", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isManual", + "columnName": "isManual", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scanMode", + "columnName": "scanMode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "scanId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "location", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `firstDiscovery` TEXT NOT NULL, `lastSeen` TEXT NOT NULL, `longitude` REAL NOT NULL, `latitude` REAL NOT NULL, `accuracy` REAL)", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstDiscovery", + "columnName": "firstDiscovery", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSeen", + "columnName": "lastSeen", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "accuracy", + "columnName": "accuracy", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_location_latitude_longitude", + "unique": true, + "columnNames": [ + "latitude", + "longitude" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_location_latitude_longitude` ON `${TABLE_NAME}` (`latitude`, `longitude`)" + } + ], + "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, 'd829378e4cbf1035e49924090f632174')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/14.json b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/14.json new file mode 100644 index 00000000..b3229a1e --- /dev/null +++ b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/14.json @@ -0,0 +1,418 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "7304689c7fc27bdc81b636a3c17ca82d", + "entities": [ + { + "tableName": "device", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uniqueId` TEXT, `address` TEXT NOT NULL, `name` TEXT, `ignore` INTEGER NOT NULL, `connectable` INTEGER DEFAULT 0, `payloadData` INTEGER, `firstDiscovery` TEXT NOT NULL, `lastSeen` TEXT NOT NULL, `notificationSent` INTEGER NOT NULL, `lastNotificationSent` TEXT, `deviceType` TEXT, `riskLevel` INTEGER NOT NULL DEFAULT 0, `lastCalculatedRiskDate` TEXT, `nextObservationNotification` TEXT, `currentObservationDuration` INTEGER, `safeTracker` INTEGER NOT NULL DEFAULT false)", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uniqueId", + "columnName": "uniqueId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ignore", + "columnName": "ignore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "connectable", + "columnName": "connectable", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "payloadData", + "columnName": "payloadData", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "firstDiscovery", + "columnName": "firstDiscovery", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSeen", + "columnName": "lastSeen", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationSent", + "columnName": "notificationSent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationSent", + "columnName": "lastNotificationSent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deviceType", + "columnName": "deviceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "riskLevel", + "columnName": "riskLevel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCalculatedRiskDate", + "columnName": "lastCalculatedRiskDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "nextObservationNotification", + "columnName": "nextObservationNotification", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentObservationDuration", + "columnName": "currentObservationDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "safeTracker", + "columnName": "safeTracker", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "deviceId" + ] + }, + "indices": [ + { + "name": "index_device_address", + "unique": true, + "columnNames": [ + "address" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_device_address` ON `${TABLE_NAME}` (`address`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceAddress` TEXT NOT NULL, `falseAlarm` INTEGER NOT NULL, `dismissed` INTEGER, `clicked` INTEGER, `createdAt` TEXT NOT NULL, `sensitivity` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceAddress", + "columnName": "deviceAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "falseAlarm", + "columnName": "falseAlarm", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "clicked", + "columnName": "clicked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sensitivity", + "columnName": "sensitivity", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "notificationId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "beacon", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`beaconId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `receivedAt` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `deviceAddress` TEXT NOT NULL, `locationId` INTEGER, `mfg` BLOB, `serviceUUIDs` TEXT, `connectionState` TEXT NOT NULL DEFAULT 'UNKNOWN')", + "fields": [ + { + "fieldPath": "beaconId", + "columnName": "beaconId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "receivedAt", + "columnName": "receivedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceAddress", + "columnName": "deviceAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "manufacturerData", + "columnName": "mfg", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "serviceUUIDs", + "columnName": "serviceUUIDs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connectionState", + "columnName": "connectionState", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'UNKNOWN'" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "beaconId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "feedback", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`feedbackId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `notificationId` INTEGER NOT NULL, `location` TEXT)", + "fields": [ + { + "fieldPath": "feedbackId", + "columnName": "feedbackId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "feedbackId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "scan", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scanId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `endDate` TEXT, `noDevicesFound` INTEGER, `duration` INTEGER, `isManual` INTEGER NOT NULL, `scanMode` INTEGER NOT NULL, `startDate` TEXT)", + "fields": [ + { + "fieldPath": "scanId", + "columnName": "scanId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "noDevicesFound", + "columnName": "noDevicesFound", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isManual", + "columnName": "isManual", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scanMode", + "columnName": "scanMode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "scanId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "location", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `firstDiscovery` TEXT NOT NULL, `lastSeen` TEXT NOT NULL, `longitude` REAL NOT NULL, `latitude` REAL NOT NULL, `accuracy` REAL)", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstDiscovery", + "columnName": "firstDiscovery", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSeen", + "columnName": "lastSeen", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "accuracy", + "columnName": "accuracy", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_location_latitude_longitude", + "unique": true, + "columnNames": [ + "latitude", + "longitude" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_location_latitude_longitude` ON `${TABLE_NAME}` (`latitude`, `longitude`)" + } + ], + "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, '7304689c7fc27bdc81b636a3c17ca82d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/15.json b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/15.json new file mode 100644 index 00000000..8cba519b --- /dev/null +++ b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/15.json @@ -0,0 +1,483 @@ +{ + "formatVersion": 1, + "database": { + "version": 15, + "identityHash": "8175e59910289659de2d3d985db686d4", + "entities": [ + { + "tableName": "device", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uniqueId` TEXT, `address` TEXT NOT NULL, `name` TEXT, `ignore` INTEGER NOT NULL, `connectable` INTEGER DEFAULT 0, `payloadData` INTEGER, `firstDiscovery` TEXT NOT NULL, `lastSeen` TEXT NOT NULL, `notificationSent` INTEGER NOT NULL, `lastNotificationSent` TEXT, `deviceType` TEXT, `riskLevel` INTEGER NOT NULL DEFAULT 0, `lastCalculatedRiskDate` TEXT, `nextObservationNotification` TEXT, `currentObservationDuration` INTEGER, `safeTracker` INTEGER NOT NULL DEFAULT false)", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uniqueId", + "columnName": "uniqueId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ignore", + "columnName": "ignore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "connectable", + "columnName": "connectable", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "payloadData", + "columnName": "payloadData", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "firstDiscovery", + "columnName": "firstDiscovery", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSeen", + "columnName": "lastSeen", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationSent", + "columnName": "notificationSent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationSent", + "columnName": "lastNotificationSent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deviceType", + "columnName": "deviceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "riskLevel", + "columnName": "riskLevel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCalculatedRiskDate", + "columnName": "lastCalculatedRiskDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "nextObservationNotification", + "columnName": "nextObservationNotification", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentObservationDuration", + "columnName": "currentObservationDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "safeTracker", + "columnName": "safeTracker", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "deviceId" + ] + }, + "indices": [ + { + "name": "index_device_lastSeen", + "unique": false, + "columnNames": [ + "lastSeen" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_device_lastSeen` ON `${TABLE_NAME}` (`lastSeen`)" + }, + { + "name": "index_device_address", + "unique": true, + "columnNames": [ + "address" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_device_address` ON `${TABLE_NAME}` (`address`)" + }, + { + "name": "index_device_notificationSent", + "unique": false, + "columnNames": [ + "notificationSent" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_device_notificationSent` ON `${TABLE_NAME}` (`notificationSent`)" + }, + { + "name": "index_device_deviceType", + "unique": false, + "columnNames": [ + "deviceType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_device_deviceType` ON `${TABLE_NAME}` (`deviceType`)" + }, + { + "name": "index_device_lastSeen_deviceType", + "unique": false, + "columnNames": [ + "lastSeen", + "deviceType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_device_lastSeen_deviceType` ON `${TABLE_NAME}` (`lastSeen`, `deviceType`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceAddress` TEXT NOT NULL, `falseAlarm` INTEGER NOT NULL, `dismissed` INTEGER, `clicked` INTEGER, `createdAt` TEXT NOT NULL, `sensitivity` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceAddress", + "columnName": "deviceAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "falseAlarm", + "columnName": "falseAlarm", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "clicked", + "columnName": "clicked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sensitivity", + "columnName": "sensitivity", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "notificationId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "beacon", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`beaconId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `receivedAt` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `deviceAddress` TEXT NOT NULL, `locationId` INTEGER, `mfg` BLOB, `serviceUUIDs` TEXT, `connectionState` TEXT NOT NULL DEFAULT 'UNKNOWN')", + "fields": [ + { + "fieldPath": "beaconId", + "columnName": "beaconId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "receivedAt", + "columnName": "receivedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceAddress", + "columnName": "deviceAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "manufacturerData", + "columnName": "mfg", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "serviceUUIDs", + "columnName": "serviceUUIDs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connectionState", + "columnName": "connectionState", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'UNKNOWN'" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "beaconId" + ] + }, + "indices": [ + { + "name": "index_beacon_receivedAt", + "unique": false, + "columnNames": [ + "receivedAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_receivedAt` ON `${TABLE_NAME}` (`receivedAt`)" + }, + { + "name": "index_beacon_deviceAddress", + "unique": false, + "columnNames": [ + "deviceAddress" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_deviceAddress` ON `${TABLE_NAME}` (`deviceAddress`)" + }, + { + "name": "index_beacon_connectionState", + "unique": false, + "columnNames": [ + "connectionState" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_connectionState` ON `${TABLE_NAME}` (`connectionState`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "feedback", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`feedbackId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `notificationId` INTEGER NOT NULL, `location` TEXT)", + "fields": [ + { + "fieldPath": "feedbackId", + "columnName": "feedbackId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "feedbackId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "scan", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scanId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `endDate` TEXT, `noDevicesFound` INTEGER, `duration` INTEGER, `isManual` INTEGER NOT NULL, `scanMode` INTEGER NOT NULL, `startDate` TEXT)", + "fields": [ + { + "fieldPath": "scanId", + "columnName": "scanId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "noDevicesFound", + "columnName": "noDevicesFound", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isManual", + "columnName": "isManual", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scanMode", + "columnName": "scanMode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "scanId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "location", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `firstDiscovery` TEXT NOT NULL, `lastSeen` TEXT NOT NULL, `longitude` REAL NOT NULL, `latitude` REAL NOT NULL, `accuracy` REAL)", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstDiscovery", + "columnName": "firstDiscovery", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSeen", + "columnName": "lastSeen", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "accuracy", + "columnName": "accuracy", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_location_latitude_longitude", + "unique": true, + "columnNames": [ + "latitude", + "longitude" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_location_latitude_longitude` ON `${TABLE_NAME}` (`latitude`, `longitude`)" + } + ], + "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, '8175e59910289659de2d3d985db686d4')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/16.json b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/16.json new file mode 100644 index 00000000..dadd4cb9 --- /dev/null +++ b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/16.json @@ -0,0 +1,489 @@ +{ + "formatVersion": 1, + "database": { + "version": 16, + "identityHash": "ae2d890772bfe46cb4203a3676128dc2", + "entities": [ + { + "tableName": "device", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uniqueId` TEXT, `address` TEXT NOT NULL, `name` TEXT, `ignore` INTEGER NOT NULL, `connectable` INTEGER DEFAULT 0, `payloadData` INTEGER, `firstDiscovery` TEXT NOT NULL, `lastSeen` TEXT NOT NULL, `notificationSent` INTEGER NOT NULL, `lastNotificationSent` TEXT, `deviceType` TEXT, `riskLevel` INTEGER NOT NULL DEFAULT 0, `lastCalculatedRiskDate` TEXT, `nextObservationNotification` TEXT, `currentObservationDuration` INTEGER, `safeTracker` INTEGER NOT NULL DEFAULT false)", + "fields": [ + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uniqueId", + "columnName": "uniqueId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ignore", + "columnName": "ignore", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "connectable", + "columnName": "connectable", + "affinity": "INTEGER", + "notNull": false, + "defaultValue": "0" + }, + { + "fieldPath": "payloadData", + "columnName": "payloadData", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "firstDiscovery", + "columnName": "firstDiscovery", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSeen", + "columnName": "lastSeen", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationSent", + "columnName": "notificationSent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationSent", + "columnName": "lastNotificationSent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deviceType", + "columnName": "deviceType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "riskLevel", + "columnName": "riskLevel", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCalculatedRiskDate", + "columnName": "lastCalculatedRiskDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "nextObservationNotification", + "columnName": "nextObservationNotification", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "currentObservationDuration", + "columnName": "currentObservationDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "safeTracker", + "columnName": "safeTracker", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "false" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "deviceId" + ] + }, + "indices": [ + { + "name": "index_device_lastSeen", + "unique": false, + "columnNames": [ + "lastSeen" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_device_lastSeen` ON `${TABLE_NAME}` (`lastSeen`)" + }, + { + "name": "index_device_address", + "unique": true, + "columnNames": [ + "address" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_device_address` ON `${TABLE_NAME}` (`address`)" + }, + { + "name": "index_device_notificationSent", + "unique": false, + "columnNames": [ + "notificationSent" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_device_notificationSent` ON `${TABLE_NAME}` (`notificationSent`)" + }, + { + "name": "index_device_deviceType", + "unique": false, + "columnNames": [ + "deviceType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_device_deviceType` ON `${TABLE_NAME}` (`deviceType`)" + }, + { + "name": "index_device_lastSeen_deviceType", + "unique": false, + "columnNames": [ + "lastSeen", + "deviceType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_device_lastSeen_deviceType` ON `${TABLE_NAME}` (`lastSeen`, `deviceType`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceAddress` TEXT NOT NULL, `falseAlarm` INTEGER NOT NULL, `dismissed` INTEGER, `clicked` INTEGER, `createdAt` TEXT NOT NULL, `sensitivity` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceAddress", + "columnName": "deviceAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "falseAlarm", + "columnName": "falseAlarm", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dismissed", + "columnName": "dismissed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "clicked", + "columnName": "clicked", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sensitivity", + "columnName": "sensitivity", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "notificationId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "beacon", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`beaconId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `receivedAt` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `deviceAddress` TEXT NOT NULL, `locationId` INTEGER, `mfg` BLOB, `serviceUUIDs` TEXT, `connectionState` TEXT NOT NULL DEFAULT 'UNKNOWN')", + "fields": [ + { + "fieldPath": "beaconId", + "columnName": "beaconId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "receivedAt", + "columnName": "receivedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rssi", + "columnName": "rssi", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deviceAddress", + "columnName": "deviceAddress", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "manufacturerData", + "columnName": "mfg", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "serviceUUIDs", + "columnName": "serviceUUIDs", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "connectionState", + "columnName": "connectionState", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'UNKNOWN'" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "beaconId" + ] + }, + "indices": [ + { + "name": "index_beacon_receivedAt", + "unique": false, + "columnNames": [ + "receivedAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_receivedAt` ON `${TABLE_NAME}` (`receivedAt`)" + }, + { + "name": "index_beacon_deviceAddress", + "unique": false, + "columnNames": [ + "deviceAddress" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_deviceAddress` ON `${TABLE_NAME}` (`deviceAddress`)" + }, + { + "name": "index_beacon_connectionState", + "unique": false, + "columnNames": [ + "connectionState" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_connectionState` ON `${TABLE_NAME}` (`connectionState`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "feedback", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`feedbackId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `notificationId` INTEGER NOT NULL, `location` TEXT)", + "fields": [ + { + "fieldPath": "feedbackId", + "columnName": "feedbackId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "feedbackId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "scan", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scanId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `endDate` TEXT, `noDevicesFound` INTEGER, `duration` INTEGER, `isManual` INTEGER NOT NULL, `scanMode` INTEGER NOT NULL, `startDate` TEXT)", + "fields": [ + { + "fieldPath": "scanId", + "columnName": "scanId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "noDevicesFound", + "columnName": "noDevicesFound", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isManual", + "columnName": "isManual", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scanMode", + "columnName": "scanMode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "scanId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "location", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `firstDiscovery` TEXT NOT NULL, `lastSeen` TEXT NOT NULL, `longitude` REAL NOT NULL, `latitude` REAL NOT NULL, `altitude` REAL, `accuracy` REAL)", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "firstDiscovery", + "columnName": "firstDiscovery", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSeen", + "columnName": "lastSeen", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "longitude", + "columnName": "longitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "latitude", + "columnName": "latitude", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "altitude", + "columnName": "altitude", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "accuracy", + "columnName": "accuracy", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "locationId" + ] + }, + "indices": [ + { + "name": "index_location_latitude_longitude", + "unique": true, + "columnNames": [ + "latitude", + "longitude" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_location_latitude_longitude` ON `${TABLE_NAME}` (`latitude`, `longitude`)" + } + ], + "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, 'ae2d890772bfe46cb4203a3676128dc2')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/de/seemoo/at_tracking_detection/ExampleInstrumentedTest.kt b/app/src/androidTest/java/de/seemoo/at_tracking_detection/ExampleInstrumentedTest.kt index d4d57ed9..ae1c6350 100644 --- a/app/src/androidTest/java/de/seemoo/at_tracking_detection/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/de/seemoo/at_tracking_detection/ExampleInstrumentedTest.kt @@ -61,7 +61,8 @@ class ExampleInstrumentedTest { // latitude = 51.4839483, locationId = 1, mfg = null, - serviceUUIDs = null + serviceUUIDs = null, + "UNKNOWN" ), Beacon( receivedAt = LocalDateTime.of(2021, 11, 20, 10, 30), @@ -71,7 +72,8 @@ class ExampleInstrumentedTest { // latitude = 51.4839483, locationId = 1, mfg = null, - serviceUUIDs = null + serviceUUIDs = null, + "UNKNOWN" ) ) @@ -84,7 +86,8 @@ class ExampleInstrumentedTest { // latitude = 51.4839483, locationId = 1, mfg = null, - serviceUUIDs = null + serviceUUIDs = null, + "UNKNOWN" ), Beacon( receivedAt = LocalDateTime.of(2021, 11, 20, 10, 45), @@ -94,7 +97,8 @@ class ExampleInstrumentedTest { // latitude = 51.4839483, locationId = 1, mfg = null, - serviceUUIDs = null + serviceUUIDs = null, + "UNKNOWN" ) ) @@ -107,7 +111,8 @@ class ExampleInstrumentedTest { // latitude = 51.4839483, locationId = 1, mfg = null, - serviceUUIDs = null + serviceUUIDs = null, + "UNKNOWN" ), Beacon( receivedAt = LocalDateTime.of(2021, 11, 22, 10, 45), @@ -117,7 +122,8 @@ class ExampleInstrumentedTest { // latitude = 51.4839483, locationId = 1, mfg = null, - serviceUUIDs = null + serviceUUIDs = null, + "UNKNOWN" ) ) @@ -130,7 +136,8 @@ class ExampleInstrumentedTest { // latitude = 51.4839483, locationId = null, mfg = null, - serviceUUIDs = null + serviceUUIDs = null, + "UNKNOWN" ), Beacon( receivedAt = LocalDateTime.of(2021, 11, 20, 10, 0), @@ -140,7 +147,8 @@ class ExampleInstrumentedTest { // latitude = 51.4839483, locationId = null, mfg = null, - serviceUUIDs = null + serviceUUIDs = null, + "UNKNOWN" ) ) @@ -153,7 +161,8 @@ class ExampleInstrumentedTest { // latitude = 51.4839483, locationId = null, mfg = null, - serviceUUIDs = null + serviceUUIDs = null, + "UNKNOWN" ), Beacon( receivedAt = LocalDateTime.of(2021, 11, 20, 10, 29), @@ -163,7 +172,8 @@ class ExampleInstrumentedTest { // latitude = 51.4839483, locationId = null, mfg = null, - serviceUUIDs = null + serviceUUIDs = null, + "UNKNOWN" ) ) @@ -176,7 +186,8 @@ class ExampleInstrumentedTest { // latitude = 51.4839483, locationId = null, mfg = null, - serviceUUIDs = null + serviceUUIDs = null, + "UNKNOWN" ), Beacon( receivedAt = LocalDateTime.of(2021, 11, 20, 10, 20), @@ -186,7 +197,8 @@ class ExampleInstrumentedTest { // latitude = 51.4839483, locationId = null, mfg = null, - serviceUUIDs = null + serviceUUIDs = null, + "UNKNOWN" ) ) diff --git a/app/src/androidTest/java/de/seemoo/at_tracking_detection/LocationProviderTest.kt b/app/src/androidTest/java/de/seemoo/at_tracking_detection/LocationProviderTest.kt index 6c62f5d0..a1a46cf4 100644 --- a/app/src/androidTest/java/de/seemoo/at_tracking_detection/LocationProviderTest.kt +++ b/app/src/androidTest/java/de/seemoo/at_tracking_detection/LocationProviderTest.kt @@ -26,7 +26,7 @@ class LocationProviderTest { val context = ATTrackingDetectionApplication.getAppContext() val locationManager = context.getSystemService() assert(locationManager != null) - val locationProvider = LocationProvider(locationManager!!, DefaultBuildVersionProvider()) + val locationProvider = LocationProvider(locationManager!!) // Getting the current location val startTime = LocalDateTime.now() @@ -49,7 +49,7 @@ class LocationProviderTest { val context = ATTrackingDetectionApplication.getAppContext() val locationManager = context.getSystemService() assert(locationManager != null) - val locationProvider = LocationProvider(locationManager!!, TestBuildVersionProvider(21)) + val locationProvider = LocationProvider(locationManager!!) // Getting the current location val startTime = LocalDateTime.now() diff --git a/app/src/androidTest/java/de/seemoo/at_tracking_detection/ScanBluetoothWorkerTest.kt b/app/src/androidTest/java/de/seemoo/at_tracking_detection/ScanBluetoothWorkerTest.kt index 463d4371..c739b204 100644 --- a/app/src/androidTest/java/de/seemoo/at_tracking_detection/ScanBluetoothWorkerTest.kt +++ b/app/src/androidTest/java/de/seemoo/at_tracking_detection/ScanBluetoothWorkerTest.kt @@ -16,6 +16,7 @@ import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor import androidx.work.testing.TestForegroundUpdater import androidx.work.testing.TestProgressUpdater import de.seemoo.at_tracking_detection.database.AppDatabase +import de.seemoo.at_tracking_detection.detection.BackgroundBluetoothScanner import de.seemoo.at_tracking_detection.detection.LocationProvider import de.seemoo.at_tracking_detection.detection.LocationRequester import de.seemoo.at_tracking_detection.detection.ScanBluetoothWorker @@ -60,8 +61,8 @@ class ScanBluetoothWorkerTest { ).addMigrations(DatabaseModule.MIGRATION_5_7, DatabaseModule.MIGRATION_6_7) .allowMainThreadQueries() .build().apply { - openHelper.writableDatabase.close() - } + openHelper.writableDatabase.close() + } this.db = roomDB executor = Executors.newSingleThreadExecutor() @@ -75,10 +76,10 @@ class ScanBluetoothWorkerTest { val deviceRepository = DatabaseModule.provideDeviceRepository(DatabaseModule.provideDeviceDao(db)) val scanRepository = DatabaseModule.provideScanRepository(DatabaseModule.provideScanDao(db)) val locationRepository = DatabaseModule.provideLocationRepository(DatabaseModule.provideLocationDao(db)) - val locationProvider = LocationProvider(context.getSystemService()!!, DefaultBuildVersionProvider()) - + val locationProvider = LocationProvider(context.getSystemService()!!) val notificationService = ATTrackingDetectionApplication.getCurrentApp()!!.notificationService val backgroundWorkScheduler = ATTrackingDetectionApplication.getCurrentApp()!!.backgroundWorkScheduler +// val backgroundBluetoothScanner = BackgroundBluetoothScanner(backgroundWorkScheduler, notificationService, locationProvider, scanRepository) val params = WorkerParameters( UUID.randomUUID(), @@ -98,7 +99,7 @@ class ScanBluetoothWorkerTest { val worker = ScanBluetoothWorker( context, params, - scanRepository, locationProvider, notificationService, backgroundWorkScheduler) + backgroundWorkScheduler) runBlocking { val result = worker.doWork() @@ -117,6 +118,7 @@ class ScanBluetoothWorkerTest { val notificationService = ATTrackingDetectionApplication.getCurrentApp()!!.notificationService val backgroundWorkScheduler = ATTrackingDetectionApplication.getCurrentApp()!!.backgroundWorkScheduler +// val backgroundScanner = BackgroundBluetoothScanner(backgroundWorkScheduler, notificationService, locationProvider, scanRepository) val params = WorkerParameters( UUID.randomUUID(), @@ -133,15 +135,11 @@ class ScanBluetoothWorkerTest { TestForegroundUpdater() ) - val worker = ScanBluetoothWorker( - context, - params, - scanRepository, locationProvider, notificationService, backgroundWorkScheduler) runBlocking { - val result = worker.doWork() + val result = BackgroundBluetoothScanner.scanInBackground(startedFrom = "UnitTest") assertThat(result, instanceOf(ListenableWorker.Result.Success::class.java)) - Assert.assertNotNull(worker.location) + Assert.assertNotNull(BackgroundBluetoothScanner.location) } } @@ -157,6 +155,7 @@ class ScanBluetoothWorkerTest { val notificationService = ATTrackingDetectionApplication.getCurrentApp()!!.notificationService val backgroundWorkScheduler = ATTrackingDetectionApplication.getCurrentApp()!!.backgroundWorkScheduler +// val backgroundScanner = BackgroundBluetoothScanner(backgroundWorkScheduler, notificationService, locationProvider, scanRepository) val params = WorkerParameters( UUID.randomUUID(), @@ -173,15 +172,10 @@ class ScanBluetoothWorkerTest { TestForegroundUpdater() ) - val worker = ScanBluetoothWorker( - context, - params, - scanRepository, locationProvider, notificationService, backgroundWorkScheduler) - runBlocking { - val result = worker.doWork() + val result = BackgroundBluetoothScanner.scanInBackground(startedFrom = "UnitTest") assertThat(result, instanceOf(ListenableWorker.Result.Success::class.java)) - Assert.assertNull(worker.location) + Assert.assertNull(BackgroundBluetoothScanner.location) } } @@ -198,6 +192,7 @@ class ScanBluetoothWorkerTest { val notificationService = ATTrackingDetectionApplication.getCurrentApp()!!.notificationService val backgroundWorkScheduler = ATTrackingDetectionApplication.getCurrentApp()!!.backgroundWorkScheduler + val params = WorkerParameters( UUID.randomUUID(), Data.EMPTY, @@ -213,15 +208,11 @@ class ScanBluetoothWorkerTest { TestForegroundUpdater() ) - val worker = ScanBluetoothWorker( - context, - params, - scanRepository, locationProvider, notificationService, backgroundWorkScheduler) runBlocking { - val result = worker.doWork() + val result = BackgroundBluetoothScanner.scanInBackground(startedFrom = "UnitTest") assertThat(result, instanceOf(ListenableWorker.Result.Success::class.java)) - Assert.assertNull(worker.location) + Assert.assertNull(BackgroundBluetoothScanner.location) } } @@ -236,13 +227,13 @@ class ScanBluetoothWorkerTest { } } -class TestLocationProvider(private val lastLocationIsNull: Boolean, private val locationDelayMillis: Long, locationManager: LocationManager, versionProvider: BuildVersionProvider) : LocationProvider(locationManager, versionProvider) { - override fun getLastLocation(checkRequirements: Boolean): Location? { - if (lastLocationIsNull) { - return null - } - return super.getLastLocation(checkRequirements) - } +class TestLocationProvider(private val lastLocationIsNull: Boolean, private val locationDelayMillis: Long, locationManager: LocationManager, versionProvider: BuildVersionProvider) : LocationProvider(locationManager) { +// override fun getLastLocation(checkRequirements: Boolean): Location? { +// if (lastLocationIsNull) { +// return null +// } +// return super.getLastLocation(checkRequirements) +// } override fun lastKnownOrRequestLocationUpdates( locationRequester: LocationRequester, diff --git a/app/src/androidTest/java/de/seemoo/at_tracking_detection/ScanDBTests.kt b/app/src/androidTest/java/de/seemoo/at_tracking_detection/ScanDBTests.kt index 464758ab..70e4a490 100644 --- a/app/src/androidTest/java/de/seemoo/at_tracking_detection/ScanDBTests.kt +++ b/app/src/androidTest/java/de/seemoo/at_tracking_detection/ScanDBTests.kt @@ -112,7 +112,7 @@ class ScanDBTests() { val updatedScan = scanRepository.scanWithId(scanId.toInt()) assert(updatedScan != null) - assert(scanDB?.endDate == updatedScan?.endDate) + assert(scanDB.endDate == updatedScan?.endDate) assert(updatedScan?.noDevicesFound == 10) assert(updatedScan?.duration == 5) } @@ -123,7 +123,9 @@ class ScanDBTests() { unfinished.forEach{ assert(it.endDate == null) assert(it.startDate == null) - assert(it.startDate?.compareTo(RiskLevelEvaluator.relevantTrackingDate) ?: -1 >= 0) + assert( + (it.startDate?.compareTo(RiskLevelEvaluator.relevantTrackingDateForRiskCalculation) ?: -1) >= 0 + ) } } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 75f23306..a030e28e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,52 +1,76 @@ - + + + + + + + + + + + + + + + + + tools:targetApi="tiramisu" + android:dataExtractionRules="@xml/data_extraction_rules" + android:fullBackupContent="false"> + android:parentActivityName=".ui.MainActivity" + android:exported="false"> + + + + + - + - + + + - + + + android:enabled="true" + android:foregroundServiceType="connectedDevice" + android:exported="false"/> = 31) { + val br = SetExactAlarmPermissionChangedReceiver() + val filter = + IntentFilter(AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED) + val flags = ContextCompat.RECEIVER_NOT_EXPORTED + ContextCompat.registerReceiver(this, br, filter, flags) + } + } + companion object { private lateinit var instance: ATTrackingDetectionApplication fun getAppContext(): Context = instance.applicationContext @@ -178,11 +197,11 @@ class ATTrackingDetectionApplication : Application(), Configuration.Provider { null } } - fun getCurrentApp(): ATTrackingDetectionApplication? { + fun getCurrentApp(): ATTrackingDetectionApplication { return instance } //TODO: Add real survey URL - val SURVEY_URL = "https://survey.seemoo.tu-darmstadt.de/index.php/117478?G06Q39=AirGuardAppAndroid&newtest=Y&lang=en" - val SURVEY_IS_RUNNING = false + const val SURVEY_URL = "https://survey.seemoo.tu-darmstadt.de/index.php/117478?G06Q39=AirGuardAppAndroid&newtest=Y&lang=en" + const val SURVEY_IS_RUNNING = false } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/AppDatabase.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/AppDatabase.kt index 53471d6e..ebc7d78f 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/AppDatabase.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/AppDatabase.kt @@ -8,7 +8,7 @@ import de.seemoo.at_tracking_detection.database.models.device.BaseDevice import de.seemoo.at_tracking_detection.util.converter.DateTimeConverter @Database( - version = 12, + version = 16, entities = [ BaseDevice::class, Notification::class, @@ -26,6 +26,10 @@ import de.seemoo.at_tracking_detection.util.converter.DateTimeConverter AutoMigration(from=8, to=9, spec = AppDatabase.RenameScanMigrationSpec::class), AutoMigration(from=10, to=11), AutoMigration(from=11, to=12), + AutoMigration(from=12, to=13), + AutoMigration(from=13, to=14), + AutoMigration(from=14, to=15), + AutoMigration(from=15, to=16) ], exportSchema = true ) @@ -49,7 +53,5 @@ abstract class AppDatabase : RoomDatabase() { fromColumnName = "date", toColumnName = "endDate" ) - class RenameScanMigrationSpec: AutoMigrationSpec { - - } + class RenameScanMigrationSpec: AutoMigrationSpec } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/Converters.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/Converters.kt index f43b156d..c7b81735 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/Converters.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/Converters.kt @@ -1,7 +1,6 @@ package de.seemoo.at_tracking_detection.database import androidx.room.TypeConverter -import java.time.LocalDateTime class Converters { diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/BeaconDao.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/BeaconDao.kt index b58ab5db..3e240275 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/BeaconDao.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/BeaconDao.kt @@ -11,8 +11,8 @@ interface BeaconDao { fun getAllBeacons(): List // @Query("SELECT mfg FROM beacon WHERE mfg LIKE :Key LIMIT 1") - @Query("SELECT * FROM beacon WHERE mfg LIKE :ServiceData") - fun getBeaconsWithDataLike(ServiceData: String): List + @Query("SELECT * FROM beacon WHERE mfg LIKE :serviceData") + fun getBeaconsWithDataLike(serviceData: String): List @Query("SELECT * FROM beacon WHERE receivedAt >= :since") fun getLatestBeacons(since: LocalDateTime): List @@ -69,4 +69,10 @@ interface BeaconDao { @Delete suspend fun delete(beacon: Beacon) + + @Delete + suspend fun deleteBeacons(beacons: List) + + @Query("SELECT * FROM beacon LEFT JOIN notification ON beacon.deviceAddress = notification.deviceAddress WHERE receivedAt < :deleteEverythingBefore AND notification.deviceAddress IS NULL AND beacon.deviceAddress IS NOT NULL AND beacon.deviceAddress <> ''") + fun getBeaconsOlderThanWithoutNotifications(deleteEverythingBefore: LocalDateTime): List } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/DeviceDao.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/DeviceDao.kt index b6472947..523658a3 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/DeviceDao.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/DeviceDao.kt @@ -44,25 +44,25 @@ interface DeviceDao { @Query("UPDATE device SET `ignore` = :state WHERE address = :address") suspend fun setIgnoreFlag(address: String, state: Boolean) - @Query("SELECT COUNT(*) FROM device") + @Query("SELECT COUNT(*) FROM device WHERE safeTracker == 0") fun getTotalCount(): Flow - @Query("SELECT COUNT(*) FROM device WHERE lastSeen >= :since AND notificationSent == 0") + @Query("SELECT COUNT(*) FROM device WHERE lastSeen >= :since AND notificationSent == 0 AND safeTracker == 0") fun getCountNotTracking(since: LocalDateTime): Flow @Query("SELECT COUNT(*) FROM device WHERE `ignore` == 1") fun getCountIgnored(): Flow - @Query("SELECT COUNT(*) FROM device WHERE firstDiscovery >= :since") + @Query("SELECT COUNT(*) FROM device WHERE firstDiscovery >= :since AND safeTracker == 0") fun getTotalCountChange(since: LocalDateTime): Flow - @Query("SELECT COUNT(*) FROM device WHERE lastSeen >= :since") + @Query("SELECT COUNT(*) FROM device WHERE lastSeen >= :since AND safeTracker == 0") fun getCurrentlyMonitored(since: LocalDateTime): Flow - @Query("SELECT COUNT(*) FROM device WHERE lastSeen >= :since AND deviceType = :deviceType") + @Query("SELECT COUNT(*) FROM device WHERE lastSeen >= :since AND deviceType = :deviceType AND safeTracker == 0") fun getCountForType(deviceType: String, since: LocalDateTime): Flow - @Query("SELECT COUNT(*) FROM device WHERE lastSeen >= :since AND (deviceType = :deviceType1 OR deviceType = :deviceType2)") + @Query("SELECT COUNT(*) FROM device WHERE lastSeen >= :since AND (deviceType = :deviceType1 OR deviceType = :deviceType2) AND safeTracker == 0") fun getCountForTypes(deviceType1: String, deviceType2: String, since: LocalDateTime): Flow @Query("SELECT COUNT(DISTINCT(location.locationId)) FROM device, location, beacon WHERE beacon.locationId = location.locationId AND beacon.deviceAddress = device.address AND beacon.locationId != 0 AND device.address = :deviceAddress AND device.lastSeen >= :since") @@ -100,5 +100,11 @@ interface DeviceDao { suspend fun update(baseDevice: BaseDevice) @Delete - suspend fun delete(baseDevice: BaseDevice) + suspend fun deleteDevice(baseDevice: BaseDevice) + + @Delete + suspend fun deleteDevices(baseDevices: List) + + @Query("SELECT * FROM device WHERE lastSeen < :since AND notificationSent == 0") + fun getDevicesOlderThanWithoutNotifications(since: LocalDateTime): List } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/LocationDao.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/LocationDao.kt index 608adb3f..8e11b224 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/LocationDao.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/LocationDao.kt @@ -1,7 +1,6 @@ package de.seemoo.at_tracking_detection.database.daos import androidx.room.* -import de.seemoo.at_tracking_detection.database.models.Beacon import de.seemoo.at_tracking_detection.database.models.Location as LocationModel import kotlinx.coroutines.flow.Flow import java.time.LocalDateTime @@ -29,9 +28,24 @@ interface LocationDao { @Query("SELECT COUNT(*) FROM location, beacon WHERE location.locationId = :locationId AND location.locationId = beacon.locationId") fun getNumberOfBeaconsForLocation(locationId: Int): Int + @Query("SELECT * FROM location WHERE locationId NOT IN (SELECT DISTINCT locationId FROM beacon)") + fun getLocationsWithNoBeacons(): List + + @Query("SELECT l.* FROM location l INNER JOIN beacon b ON l.locationId = b.locationId WHERE b.deviceAddress = :deviceAddress") + fun getLocationsForDevice(deviceAddress: String): List + + @Query("SELECT l.* FROM location l INNER JOIN beacon b ON l.locationId = b.locationId WHERE b.deviceAddress = :deviceAddress AND b.receivedAt >= :since") + fun getLocationsForDeviceSince(deviceAddress: String, since: LocalDateTime): List + @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insert(location: LocationModel): Long @Update suspend fun update(location: LocationModel) + + @Delete + suspend fun delete(location: LocationModel) + + @Delete + suspend fun deleteLocations(locations: List) } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/NotificationDao.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/NotificationDao.kt index 50750aae..7cdade7b 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/NotificationDao.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/NotificationDao.kt @@ -1,8 +1,13 @@ package de.seemoo.at_tracking_detection.database.daos -import androidx.room.* +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.RewriteQueriesToDropUnusedColumns +import androidx.room.Transaction +import androidx.room.Update import de.seemoo.at_tracking_detection.database.models.Notification -import de.seemoo.at_tracking_detection.database.models.device.BaseDevice import de.seemoo.at_tracking_detection.database.relations.NotificationFeedback import kotlinx.coroutines.flow.Flow import java.time.LocalDateTime diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/ScanDao.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/ScanDao.kt index 59157f54..8591e6c6 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/ScanDao.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/ScanDao.kt @@ -1,11 +1,14 @@ package de.seemoo.at_tracking_detection.database.daos -import androidx.room.* -import de.seemoo.at_tracking_detection.database.models.Beacon +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update import de.seemoo.at_tracking_detection.database.models.Scan -import java.time.LocalDateTime import kotlinx.coroutines.flow.Flow -import kotlin.reflect.jvm.internal.impl.descriptors.Visibilities +import java.time.LocalDateTime @Dao interface ScanDao { diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Beacon.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Beacon.kt index 9c2b749a..30c02521 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Beacon.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Beacon.kt @@ -1,16 +1,20 @@ package de.seemoo.at_tracking_detection.database.models -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import androidx.room.TypeConverters +import androidx.room.* import de.seemoo.at_tracking_detection.database.Converters import de.seemoo.at_tracking_detection.util.converter.DateTimeConverter import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.time.format.FormatStyle -@Entity(tableName = "beacon") +@Entity( + tableName = "beacon", + indices = [ + Index(value = ["receivedAt"]), + Index(value = ["deviceAddress"]), + Index(value = ["connectionState"]) + ] +) @TypeConverters(DateTimeConverter::class, Converters::class) data class Beacon( @PrimaryKey(autoGenerate = true) val beaconId: Int, @@ -19,7 +23,8 @@ data class Beacon( @ColumnInfo(name = "deviceAddress") var deviceAddress: String, @ColumnInfo(name = "locationId") var locationId: Int?, @ColumnInfo(name = "mfg") var manufacturerData: ByteArray?, - @ColumnInfo(name = "serviceUUIDs") var serviceUUIDs: List? + @ColumnInfo(name = "serviceUUIDs") var serviceUUIDs: List?, + @ColumnInfo(name = "connectionState", defaultValue = "UNKNOWN") var connectionState: String ) { constructor( receivedAt: LocalDateTime, @@ -27,7 +32,8 @@ data class Beacon( deviceAddress: String, locationId: Int?, mfg: ByteArray?, - serviceUUIDs: List? + serviceUUIDs: List?, + connectionState: String ) : this( 0, receivedAt, @@ -35,7 +41,8 @@ data class Beacon( deviceAddress, locationId, mfg, - serviceUUIDs + serviceUUIDs, + connectionState ) fun getFormattedDate(): String = @@ -53,6 +60,7 @@ data class Beacon( if (rssi != other.rssi) return false if (deviceAddress != other.deviceAddress) return false if (locationId != other.locationId) return false + if (connectionState != other.connectionState) return false return true } @@ -60,7 +68,7 @@ data class Beacon( override fun hashCode(): Int { var result = beaconId result = 31 * result + receivedAt.hashCode() - result = 31 * result + rssi + result = 31 * result + rssi.hashCode() result = 31 * result + deviceAddress.hashCode() result = 31 * result + locationId.hashCode() return result diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Location.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Location.kt index 60c0e8f7..21a27ce4 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Location.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Location.kt @@ -11,20 +11,23 @@ data class Location( @ColumnInfo(name = "lastSeen") var lastSeen: LocalDateTime, @ColumnInfo(name = "longitude") var longitude: Double, @ColumnInfo(name = "latitude") var latitude: Double, + @ColumnInfo(name = "altitude") var altitude: Double?, @ColumnInfo(name = "accuracy") var accuracy: Float?, ) { constructor( firstDiscovery: LocalDateTime, longitude: Double, latitude: Double, + altitude: Double?, accuracy: Float? ): this( - 0, - null, - firstDiscovery, // firstDiscovery - firstDiscovery, // lastSeen - longitude, - latitude, - accuracy + locationId = 0, + name = null, + firstDiscovery = firstDiscovery, + lastSeen = firstDiscovery, + longitude = longitude, + latitude = latitude, + altitude = altitude, + accuracy = accuracy, ) } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Notification.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Notification.kt index 520e9b1d..837eb9e7 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Notification.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Notification.kt @@ -15,11 +15,13 @@ data class Notification( @ColumnInfo(name = "falseAlarm") val falseAlarm: Boolean, @ColumnInfo(name = "dismissed") val dismissed: Boolean?, @ColumnInfo(name = "clicked") val clicked: Boolean?, - @ColumnInfo(name = "createdAt") val createdAt: LocalDateTime + @ColumnInfo(name = "createdAt") val createdAt: LocalDateTime, + @ColumnInfo(name = "sensitivity", defaultValue = "0") val sensitivity: Int // 0 = unknown, 1 = low, 2 = medium, 3 = high ) { constructor( deviceAddress: String, falseAlarm: Boolean, - createdAt: LocalDateTime - ) : this(0, deviceAddress, falseAlarm, false, false, createdAt) + createdAt: LocalDateTime, + sensitivity: Int + ) : this(0, deviceAddress, falseAlarm, false, false, createdAt, sensitivity) } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/BaseDevice.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/BaseDevice.kt index 26e8c894..464088a1 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/BaseDevice.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/BaseDevice.kt @@ -1,7 +1,6 @@ package de.seemoo.at_tracking_detection.database.models.device import android.bluetooth.le.ScanResult -import android.os.Build import androidx.room.* import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication import de.seemoo.at_tracking_detection.R @@ -13,7 +12,16 @@ import java.time.format.FormatStyle import java.util.* import kotlin.experimental.and -@Entity(tableName = "device", indices = [Index(value = ["address"], unique = true)]) +@Entity( + tableName = "device", + indices = [ + Index(value = ["lastSeen"]), + Index(value = ["address"], unique = true), + Index(value = ["notificationSent"]), + Index(value = ["deviceType"]), + Index(value = ["lastSeen", "deviceType"]) + ] +) @TypeConverters(DateTimeConverter::class) data class BaseDevice( @PrimaryKey(autoGenerate = true) var deviceId: Int, @@ -32,6 +40,7 @@ data class BaseDevice( @ColumnInfo(name = "lastCalculatedRiskDate") var lastCalculatedRiskDate: LocalDateTime?, @ColumnInfo(name = "nextObservationNotification") var nextObservationNotification: LocalDateTime?, @ColumnInfo(name = "currentObservationDuration") var currentObservationDuration: Long?, + @ColumnInfo(name = "safeTracker", defaultValue = "false") var safeTracker: Boolean = false, ) { constructor( @@ -68,11 +77,7 @@ data class BaseDevice( getDeviceName(scanResult), false, scanResult.let { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - scanResult.isConnectable - } else { - null - } + scanResult.isConnectable }, scanResult.scanRecord?.getManufacturerSpecificData(76)?.get(2), LocalDateTime.now(), @@ -121,59 +126,49 @@ data class BaseDevice( companion object { fun getDeviceName(scanResult: ScanResult): String? { return when (DeviceManager.getDeviceType(scanResult)) { - DeviceType.GALAXY_SMART_TAG_PLUS -> null + DeviceType.GALAXY_SMART_TAG_PLUS, DeviceType.GALAXY_SMART_TAG -> null else -> scanResult.scanRecord?.deviceName } } - fun getPublicKey(scanResult: ScanResult): String{ - return when (DeviceManager.getDeviceType(scanResult)) { - DeviceType.SAMSUNG -> SamsungDevice.getPublicKey(scanResult) - DeviceType.GALAXY_SMART_TAG -> SamsungDevice.getPublicKey(scanResult) + fun getPublicKey(scanResult: ScanResult, deviceType: DeviceType = DeviceManager.getDeviceType(scanResult)): String { + return when (deviceType) { + DeviceType.SAMSUNG, + DeviceType.GALAXY_SMART_TAG, DeviceType.GALAXY_SMART_TAG_PLUS -> SamsungDevice.getPublicKey(scanResult) - else -> scanResult.device.address + else -> scanResult.device.address // Default case to handle unknown types } } - fun getConnectionState(scanResult: ScanResult): ConnectionState { - return when (DeviceManager.getDeviceType(scanResult)) { + fun getConnectionState(scanResult: ScanResult, deviceType: DeviceType = DeviceManager.getDeviceType(scanResult)): ConnectionState { + return when (deviceType) { DeviceType.TILE -> Tile.getConnectionState(scanResult) DeviceType.CHIPOLO -> Chipolo.getConnectionState(scanResult) - DeviceType.SAMSUNG -> SamsungDevice.getConnectionState(scanResult) - DeviceType.GALAXY_SMART_TAG -> SamsungDevice.getConnectionState(scanResult) + DeviceType.SAMSUNG, + DeviceType.GALAXY_SMART_TAG, DeviceType.GALAXY_SMART_TAG_PLUS -> SamsungDevice.getConnectionState(scanResult) - DeviceType.AIRPODS -> AppleDevice.getConnectionState(scanResult) - DeviceType.FIND_MY -> AppleDevice.getConnectionState(scanResult) - DeviceType.AIRTAG -> AppleDevice.getConnectionState(scanResult) + DeviceType.AIRPODS, + DeviceType.FIND_MY, + DeviceType.AIRTAG, DeviceType.APPLE -> AppleDevice.getConnectionState(scanResult) else -> ConnectionState.UNKNOWN } } - fun getBatteryState(scanResult: ScanResult): BatteryState { - return when (DeviceManager.getDeviceType(scanResult)) { - DeviceType.GALAXY_SMART_TAG -> SamsungDevice.getBatteryState(scanResult) + fun getBatteryState(scanResult: ScanResult, deviceType: DeviceType = DeviceManager.getDeviceType(scanResult)): BatteryState { + return when (deviceType) { + DeviceType.GALAXY_SMART_TAG, DeviceType.GALAXY_SMART_TAG_PLUS -> SamsungDevice.getBatteryState(scanResult) - DeviceType.FIND_MY -> AirTag.getBatteryState(scanResult) - DeviceType.AIRTAG -> AirTag.getBatteryState(scanResult) + DeviceType.FIND_MY, + DeviceType.AIRTAG, DeviceType.AIRPODS -> AirTag.getBatteryState(scanResult) else -> BatteryState.UNKNOWN } } - fun getConnectionStateAsString(scanResult: ScanResult): String { - return when (getConnectionState(scanResult)) { - ConnectionState.OFFLINE -> ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.connection_state_offline) - ConnectionState.PREMATURE_OFFLINE -> ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.connection_state_premature_offline) - ConnectionState.OVERMATURE_OFFLINE -> ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.connection_state_overmature_offline) - ConnectionState.CONNECTED -> ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.connection_state_connected) - ConnectionState.UNKNOWN -> ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.connection_state_unknown) - } - } - - fun getBatteryStateAsString(scanResult: ScanResult): String { - return when (getBatteryState(scanResult)) { + fun getBatteryStateAsString(scanResult: ScanResult, deviceType: DeviceType = DeviceManager.getDeviceType(scanResult)): String { + return when (getBatteryState(scanResult, deviceType)) { BatteryState.LOW -> ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.battery_low) BatteryState.VERY_LOW -> ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.battery_very_low) BatteryState.MEDIUM -> ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.battery_medium) diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/DeviceContext.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/DeviceContext.kt index da1a6c05..a8a61884 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/DeviceContext.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/DeviceContext.kt @@ -2,6 +2,7 @@ package de.seemoo.at_tracking_detection.database.models.device import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanResult +import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator interface DeviceContext { val bluetoothFilter: ScanFilter @@ -16,6 +17,21 @@ interface DeviceContext { val statusByteDeviceType: UInt + val numberOfHoursToBeConsideredForTrackingDetection: Long + get() = RiskLevelEvaluator.RELEVANT_HOURS_TRACKING + + val numberOfLocationsToBeConsideredForTrackingDetectionLow: Int + get() = RiskLevelEvaluator.NUMBER_OF_LOCATIONS_BEFORE_ALARM_LOW + + val numberOfLocationsToBeConsideredForTrackingDetectionMedium: Int + get() = RiskLevelEvaluator.NUMBER_OF_LOCATIONS_BEFORE_ALARM_MEDIUM + + val numberOfLocationsToBeConsideredForTrackingDetectionHigh: Int + get() = RiskLevelEvaluator.NUMBER_OF_LOCATIONS_BEFORE_ALARM_HIGH + + val websiteManufacturer: String + get() = "https://www.seemoo.tu-darmstadt.de/" + fun getConnectionState(scanResult: ScanResult): ConnectionState { return ConnectionState.UNKNOWN } diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/DeviceManager.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/DeviceManager.kt index 4bdb8f40..3573278b 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/DeviceManager.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/DeviceManager.kt @@ -12,46 +12,85 @@ object DeviceManager { val devices = listOf(AirTag, FindMy, AirPods, AppleDevice, SmartTag, SmartTagPlus, Tile, Chipolo) private val appleDevices = listOf(AirTag, FindMy, AirPods, AppleDevice) - val savedConnectionStates = listOf(ConnectionState.OVERMATURE_OFFLINE, ConnectionState.UNKNOWN) + val unsafeConnectionState = listOf(ConnectionState.OVERMATURE_OFFLINE, ConnectionState.UNKNOWN) + val savedConnectionStates = unsafeConnectionState //enumValues().toList() + + private val deviceTypeCache = mutableMapOf() fun getDeviceType(scanResult: ScanResult): DeviceType { - Timber.d("Checking device type for ${scanResult.device.address}") - - val manufacturerData = scanResult.scanRecord?.getManufacturerSpecificData(0x004c) - val services = scanResult.scanRecord?.serviceUuids - if (manufacturerData != null) { - val statusByte: Byte = manufacturerData[2] -// Timber.d("Status byte $statusByte, ${statusByte.toString(2)}") - // Get the correct int from the byte - val deviceTypeInt = (statusByte.and(0x30).toInt() shr 4) -// Timber.d("Device type int: $deviceTypeInt") - - var deviceTypeCheck: DeviceType? = null - - for (device in appleDevices) { - // Implementation of device detection is incorrect. - if (device.statusByteDeviceType == deviceTypeInt.toUInt()) { - deviceTypeCheck = device.deviceType + val deviceAddress = scanResult.device.address + + // Check cache, before calculating again + var deviceType: DeviceType? = getDeviceTypeFromCache(deviceAddress) + if (deviceType != null) { + return deviceType + } else { + Timber.d("Device type not in cache, calculating...") + deviceType = calculateDeviceType(scanResult) + deviceTypeCache[deviceAddress] = deviceType + return deviceType + } + } + + fun getDeviceTypeFromCache(deviceAddress: String): DeviceType? { + deviceTypeCache[deviceAddress]?.let { cachedDeviceType -> + return cachedDeviceType + } + return null + } + + private fun calculateDeviceType(scanResult: ScanResult): DeviceType { + Timber.d("Retrieving device type for ${scanResult.device.address}") + + scanResult.scanRecord?.let { scanRecord -> + scanRecord.getManufacturerSpecificData(0x004c)?.let { manufacturerData -> + if (manufacturerData.size >= 3) { // Ensure array size is sufficient + val statusByte: Byte = manufacturerData[2] + val deviceTypeInt = (statusByte.and(0x30).toInt() shr 4) + + for (device in appleDevices) { + if (device.statusByteDeviceType == deviceTypeInt.toUInt()) { + return device.deviceType + } + } } } - return deviceTypeCheck ?: Unknown.deviceType - }else if (services != null) { - //Check if this device is a Tile - if (services.contains(Tile.offlineFindingServiceUUID)) { - return Tile.deviceType - } - else if(services.contains(Chipolo.offlineFindingServiceUUID)){ - return Chipolo.deviceType - } - else if(services.contains(SmartTag.offlineFindingServiceUUID)){ - return SamsungDevice.getSamsungDeviceType(scanResult) + scanRecord.serviceUuids?.let { services -> + when { + services.contains(Tile.offlineFindingServiceUUID) -> return Tile.deviceType + services.contains(Chipolo.offlineFindingServiceUUID) -> return Chipolo.deviceType + services.contains(SmartTag.offlineFindingServiceUUID) -> return SamsungDevice.getSamsungDeviceType(scanResult) + else -> return Unknown.deviceType + } } - } return Unknown.deviceType } + fun getWebsiteURL(deviceType: DeviceType): String { + return when (deviceType) { + DeviceType.UNKNOWN -> Unknown.websiteManufacturer + DeviceType.AIRTAG -> AirTag.websiteManufacturer + DeviceType.APPLE -> AppleDevice.websiteManufacturer + DeviceType.AIRPODS -> AirPods.websiteManufacturer + DeviceType.TILE -> Tile.websiteManufacturer + DeviceType.FIND_MY -> FindMy.websiteManufacturer + DeviceType.CHIPOLO -> Chipolo.websiteManufacturer + DeviceType.SAMSUNG -> SamsungDevice.websiteManufacturer + DeviceType.GALAXY_SMART_TAG -> SmartTag.websiteManufacturer + DeviceType.GALAXY_SMART_TAG_PLUS -> SmartTagPlus.websiteManufacturer + } + } + + fun deviceTypeToString(deviceType: DeviceType): String { + return deviceType.name + } + + fun stringToDeviceType(deviceTypeString: String): DeviceType { + return DeviceType.valueOf(deviceTypeString) + } + val scanFilter: List = devices.map { it.bluetoothFilter } val gattIntentFilter: IntentFilter = IntentFilter().apply { diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/DeviceType.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/DeviceType.kt index 7abc4279..639882d4 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/DeviceType.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/DeviceType.kt @@ -80,4 +80,64 @@ enum class DeviceType { else -> false } } + + fun getNumberOfHoursToBeConsideredForTrackingDetection(): Long { + return when (this) { + TILE -> Tile.numberOfHoursToBeConsideredForTrackingDetection + CHIPOLO -> Chipolo.numberOfHoursToBeConsideredForTrackingDetection + UNKNOWN -> Unknown.numberOfHoursToBeConsideredForTrackingDetection + AIRPODS -> AirPods.numberOfHoursToBeConsideredForTrackingDetection + AIRTAG -> AirTag.numberOfHoursToBeConsideredForTrackingDetection + APPLE -> AppleDevice.numberOfHoursToBeConsideredForTrackingDetection + FIND_MY -> FindMy.numberOfHoursToBeConsideredForTrackingDetection + SAMSUNG -> SamsungDevice.numberOfHoursToBeConsideredForTrackingDetection + GALAXY_SMART_TAG -> SmartTag.numberOfHoursToBeConsideredForTrackingDetection + GALAXY_SMART_TAG_PLUS -> SmartTagPlus.numberOfHoursToBeConsideredForTrackingDetection + } + } + + fun getNumberOfLocationsToBeConsideredForTrackingDetectionLow(): Int { + return when (this) { + TILE -> Tile.numberOfLocationsToBeConsideredForTrackingDetectionLow + CHIPOLO -> Chipolo.numberOfLocationsToBeConsideredForTrackingDetectionLow + UNKNOWN -> Unknown.numberOfLocationsToBeConsideredForTrackingDetectionLow + AIRPODS -> AirPods.numberOfLocationsToBeConsideredForTrackingDetectionLow + AIRTAG -> AirTag.numberOfLocationsToBeConsideredForTrackingDetectionLow + APPLE -> AppleDevice.numberOfLocationsToBeConsideredForTrackingDetectionLow + FIND_MY -> FindMy.numberOfLocationsToBeConsideredForTrackingDetectionLow + SAMSUNG -> SamsungDevice.numberOfLocationsToBeConsideredForTrackingDetectionLow + GALAXY_SMART_TAG -> SmartTag.numberOfLocationsToBeConsideredForTrackingDetectionLow + GALAXY_SMART_TAG_PLUS -> SmartTagPlus.numberOfLocationsToBeConsideredForTrackingDetectionLow + } + } + + fun getNumberOfLocationsToBeConsideredForTrackingDetectionMedium(): Int { + return when (this) { + TILE -> Tile.numberOfLocationsToBeConsideredForTrackingDetectionMedium + CHIPOLO -> Chipolo.numberOfLocationsToBeConsideredForTrackingDetectionMedium + UNKNOWN -> Unknown.numberOfLocationsToBeConsideredForTrackingDetectionMedium + AIRPODS -> AirPods.numberOfLocationsToBeConsideredForTrackingDetectionMedium + AIRTAG -> AirTag.numberOfLocationsToBeConsideredForTrackingDetectionMedium + APPLE -> AppleDevice.numberOfLocationsToBeConsideredForTrackingDetectionMedium + FIND_MY -> FindMy.numberOfLocationsToBeConsideredForTrackingDetectionMedium + SAMSUNG -> SamsungDevice.numberOfLocationsToBeConsideredForTrackingDetectionMedium + GALAXY_SMART_TAG -> SmartTag.numberOfLocationsToBeConsideredForTrackingDetectionMedium + GALAXY_SMART_TAG_PLUS -> SmartTagPlus.numberOfLocationsToBeConsideredForTrackingDetectionMedium + } + } + + fun getNumberOfLocationsToBeConsideredForTrackingDetectionHigh(): Int { + return when (this) { + TILE -> Tile.numberOfLocationsToBeConsideredForTrackingDetectionHigh + CHIPOLO -> Chipolo.numberOfLocationsToBeConsideredForTrackingDetectionHigh + UNKNOWN -> Unknown.numberOfLocationsToBeConsideredForTrackingDetectionHigh + AIRPODS -> AirPods.numberOfLocationsToBeConsideredForTrackingDetectionHigh + AIRTAG -> AirTag.numberOfLocationsToBeConsideredForTrackingDetectionHigh + APPLE -> AppleDevice.numberOfLocationsToBeConsideredForTrackingDetectionHigh + FIND_MY -> FindMy.numberOfLocationsToBeConsideredForTrackingDetectionHigh + SAMSUNG -> SamsungDevice.numberOfLocationsToBeConsideredForTrackingDetectionHigh + GALAXY_SMART_TAG -> SmartTag.numberOfLocationsToBeConsideredForTrackingDetectionHigh + GALAXY_SMART_TAG_PLUS -> SmartTagPlus.numberOfLocationsToBeConsideredForTrackingDetectionHigh + } + } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/AirPods.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/AirPods.kt index 993fde09..7c024380 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/AirPods.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/AirPods.kt @@ -162,6 +162,9 @@ class AirPods(val id: Int) : Device(), Connectable { override val deviceType: DeviceType get() = DeviceType.AIRPODS + override val websiteManufacturer: String + get() = "https://www.apple.com/airpods/" + override val defaultDeviceName: String get() = "AirPods" diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/AirTag.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/AirTag.kt index c8a894bc..ea815acb 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/AirTag.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/AirTag.kt @@ -96,6 +96,7 @@ class AirTag(val id: Int) : Device(), Connectable { super.onCharacteristicWrite(gatt, characteristic, status) } + @Deprecated("Deprecated in Java") override fun onCharacteristicRead( gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, @@ -142,6 +143,9 @@ class AirTag(val id: Int) : Device(), Connectable { override val statusByteDeviceType: UInt get() = 1u + override val websiteManufacturer: String + get() = "https://www.apple.com/airtag/" + override fun getBatteryState(scanResult: ScanResult): BatteryState { val mfg: ByteArray? = scanResult.scanRecord?.getManufacturerSpecificData(0x4C) diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/AppleDevice.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/AppleDevice.kt index afd7da41..7251ab81 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/AppleDevice.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/AppleDevice.kt @@ -48,6 +48,9 @@ class AppleDevice(val id: Int) : Device() { override val statusByteDeviceType: UInt get() = 0u + override val websiteManufacturer: String + get() = "https://www.apple.com/" + override fun getConnectionState(scanResult: ScanResult): ConnectionState { val mfg: ByteArray? = scanResult.scanRecord?.getManufacturerSpecificData(0x4C) diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/Chipolo.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/Chipolo.kt index 2329c13c..0e8dbe3c 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/Chipolo.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/Chipolo.kt @@ -37,6 +37,11 @@ class Chipolo(val id: Int) : Device() { override val statusByteDeviceType: UInt get() = 0u + override val websiteManufacturer: String + get() = "https://chipolo.net/" + + + val offlineFindingServiceUUID: ParcelUuid = ParcelUuid.fromString("0000FE33-0000-1000-8000-00805F9B34FB") override fun getConnectionState(scanResult: ScanResult): ConnectionState { diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/FindMy.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/FindMy.kt index c45804ea..e4b0576c 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/FindMy.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/FindMy.kt @@ -179,6 +179,9 @@ class FindMy(val id: Int) : Device(), Connectable { override val deviceType: DeviceType get() = DeviceType.FIND_MY + override val websiteManufacturer: String + get() = "https://www.apple.com/" + override val defaultDeviceName: String get() = "FindMy Device" diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/SamsungDevice.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/SamsungDevice.kt index 38435872..3d719410 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/SamsungDevice.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/SamsungDevice.kt @@ -35,8 +35,8 @@ open class SamsungDevice(open val id: Int) : Device(){ // Twelve Byte: // 04, 00 --> UWB off, // 04, 04 --> UWB on - byteArrayOf((0x13).toByte()), - byteArrayOf((0xFF).toByte()) + byteArrayOf((0x10).toByte()), + byteArrayOf((0xF8).toByte()) ) .build() @@ -49,6 +49,9 @@ open class SamsungDevice(open val id: Int) : Device(){ override val statusByteDeviceType: UInt get() = 0u + override val websiteManufacturer: String + get() = "https://www.samsung.com/" + private val offlineFindingServiceUUID: ParcelUuid = ParcelUuid.fromString("0000FD5A-0000-1000-8000-00805F9B34FB") override fun getConnectionState(scanResult: ScanResult): ConnectionState { @@ -102,16 +105,19 @@ open class SamsungDevice(open val id: Int) : Device(){ return BatteryState.UNKNOWN } - override fun getPublicKey(scanResult: ScanResult): String{ - val serviceData = scanResult.scanRecord?.getServiceData(SmartTag.offlineFindingServiceUUID) + override fun getPublicKey(scanResult: ScanResult): String { + try { + val serviceData = scanResult.scanRecord?.getServiceData(SmartTag.offlineFindingServiceUUID) - fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) } + fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) } - return if (serviceData == null || serviceData.size < 12) { - scanResult.device.address - } else { - byteArrayOf(serviceData[4], serviceData[5], serviceData[6], serviceData[7], serviceData[8], serviceData[9], serviceData[10], serviceData[11]).toHexString() + if (serviceData != null && serviceData.size >= 12) { + return byteArrayOf(serviceData[4], serviceData[5], serviceData[6], serviceData[7], serviceData[8], serviceData[9], serviceData[10], serviceData[11]).toHexString() + } + } catch (e: Exception) { + Timber.e(e, "Error getting public key") } + return scanResult.device.address } fun getSamsungDeviceType(scanResult: ScanResult): DeviceType{ diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/SmartTag.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/SmartTag.kt index 582a4589..03720ff8 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/SmartTag.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/SmartTag.kt @@ -2,10 +2,8 @@ package de.seemoo.at_tracking_detection.database.models.device.types import android.bluetooth.le.ScanFilter import android.os.ParcelUuid -import androidx.annotation.DrawableRes import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication import de.seemoo.at_tracking_detection.R -import de.seemoo.at_tracking_detection.database.models.device.Device import de.seemoo.at_tracking_detection.database.models.device.DeviceContext import de.seemoo.at_tracking_detection.database.models.device.DeviceType @@ -44,8 +42,11 @@ class SmartTag(override val id: Int) : SamsungDevice(id) { override val deviceType: DeviceType get() = DeviceType.GALAXY_SMART_TAG + override val websiteManufacturer: String + get() = "https://www.samsung.com/" + override val defaultDeviceName: String - get() = "SmartTag" + get() = ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.smarttag_no_uwb) override val statusByteDeviceType: UInt get() = 0u diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/SmartTagPlus.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/SmartTagPlus.kt index ba0d0a93..3b8a1208 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/SmartTagPlus.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/SmartTagPlus.kt @@ -2,10 +2,8 @@ package de.seemoo.at_tracking_detection.database.models.device.types import android.bluetooth.le.ScanFilter import android.os.ParcelUuid -import androidx.annotation.DrawableRes import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication import de.seemoo.at_tracking_detection.R -import de.seemoo.at_tracking_detection.database.models.device.Device import de.seemoo.at_tracking_detection.database.models.device.DeviceContext import de.seemoo.at_tracking_detection.database.models.device.DeviceType @@ -45,11 +43,14 @@ class SmartTagPlus(override val id: Int) : SamsungDevice(id) { get() = DeviceType.GALAXY_SMART_TAG_PLUS override val defaultDeviceName: String - get() = "SmartTag Plus" + get() = ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.smarttag_uwb) override val statusByteDeviceType: UInt get() = 0u + override val websiteManufacturer: String + get() = "https://www.samsung.com/" + private val offlineFindingServiceUUID: ParcelUuid = ParcelUuid.fromString("0000FD5A-0000-1000-8000-00805F9B34FB") } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/Tile.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/Tile.kt index 1c9aacc3..187413d9 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/Tile.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/types/Tile.kt @@ -6,7 +6,6 @@ import androidx.annotation.DrawableRes import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication import de.seemoo.at_tracking_detection.R import de.seemoo.at_tracking_detection.database.models.device.* -import timber.log.Timber class Tile(val id: Int) : Device(){ override val imageResource: Int @@ -36,9 +35,21 @@ class Tile(val id: Int) : Device(){ override val defaultDeviceName: String get() = "Tile" + override val websiteManufacturer: String + get() = "https://www.tile.com/" + override val statusByteDeviceType: UInt get() = 0u + override val numberOfLocationsToBeConsideredForTrackingDetectionLow: Int + get() = 5 + + override val numberOfLocationsToBeConsideredForTrackingDetectionMedium: Int + get() = 4 + + override val numberOfLocationsToBeConsideredForTrackingDetectionHigh: Int + get() = 3 + val offlineFindingServiceUUID: ParcelUuid = ParcelUuid.fromString("0000FEED-0000-1000-8000-00805F9B34FB") } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/BeaconRepository.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/BeaconRepository.kt index 7db923a6..ebc6efeb 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/BeaconRepository.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/BeaconRepository.kt @@ -1,6 +1,5 @@ package de.seemoo.at_tracking_detection.database.repository -import android.os.ParcelUuid import androidx.annotation.WorkerThread import de.seemoo.at_tracking_detection.database.daos.BeaconDao import de.seemoo.at_tracking_detection.database.models.Beacon @@ -58,4 +57,11 @@ class BeaconRepository @Inject constructor( suspend fun update(beacon: Beacon) { beaconDao.update(beacon) } + + @WorkerThread + suspend fun deleteBeacons(beacons: List) { + beaconDao.deleteBeacons(beacons) + } + + fun getBeaconsOlderThanWithoutNotifications(deleteEverythingBefore: LocalDateTime): List = beaconDao.getBeaconsOlderThanWithoutNotifications(deleteEverythingBefore) } diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/DeviceRepository.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/DeviceRepository.kt index c4d85d3e..fd02962a 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/DeviceRepository.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/DeviceRepository.kt @@ -27,7 +27,7 @@ class DeviceRepository @Inject constructor(private val deviceDao: DeviceDao) { fun updateRiskLevelCache(deviceAddress: String, riskLevel: Int, lastCalculatedRiskDate: LocalDateTime) = deviceDao.updateRiskLevelCache(deviceAddress, riskLevel, lastCalculatedRiskDate) - fun trackingDevicesSinceCount(since: LocalDateTime) = deviceDao.trackingDevicesCount(since) + // fun trackingDevicesSinceCount(since: LocalDateTime) = deviceDao.trackingDevicesCount(since) val totalCount: Flow = deviceDao.getTotalCount() @@ -37,34 +37,35 @@ class DeviceRepository @Inject constructor(private val deviceDao: DeviceDao) { fun devicesCurrentlyMonitored(since: LocalDateTime): Flow = deviceDao.getCurrentlyMonitored(since) - fun deviceCountSince(since: LocalDateTime): Flow = - deviceDao.getCurrentlyMonitored(since) + // fun deviceCountSince(since: LocalDateTime): Flow = deviceDao.getCurrentlyMonitored(since) - val ignoredDevices: Flow> = deviceDao.getIgnored() + // val ignoredDevices: Flow> = deviceDao.getIgnored() val ignoredDevicesSync: List = deviceDao.getIgnoredSync() fun getDevice(deviceAddress: String): BaseDevice? = deviceDao.getByAddress(deviceAddress) - val countNotTracking = deviceDao.getCountNotTracking(RiskLevelEvaluator.relevantTrackingDate) + val countNotTracking = deviceDao.getCountNotTracking(RiskLevelEvaluator.relevantTrackingDateForRiskCalculation) val countIgnored = deviceDao.getCountIgnored() - fun countForDeviceType(deviceType: DeviceType) = deviceDao.getCountForType(deviceType.name, RiskLevelEvaluator.relevantTrackingDate) - fun countForDeviceTypes(deviceType1: DeviceType, deviceType2: DeviceType) = deviceDao.getCountForTypes(deviceType1.name, deviceType2.name, RiskLevelEvaluator.relevantTrackingDate) + fun countForDeviceType(deviceType: DeviceType) = deviceDao.getCountForType(deviceType.name, RiskLevelEvaluator.relevantTrackingDateForRiskCalculation) + fun countForDeviceTypes(deviceType1: DeviceType, deviceType2: DeviceType) = deviceDao.getCountForTypes(deviceType1.name, deviceType2.name, RiskLevelEvaluator.relevantTrackingDateForRiskCalculation) fun getNumberOfLocationsForDeviceSince(deviceAddress: String, since: LocalDateTime): Int = deviceDao.getNumberOfLocationsForDevice(deviceAddress, since) fun getNumberOfLocationsForDeviceWithAccuracyLimitSince(deviceAddress: String, maxAccuracy: Float, since: LocalDateTime): Int = deviceDao.getNumberOfLocationsForWithAccuracyLimitDevice(deviceAddress, maxAccuracy, since) - @WorkerThread - suspend fun getDeviceBeaconsSince(dateTime: String?): List { - return if (dateTime != null) { - deviceDao.getDeviceBeaconsSince(LocalDateTime.parse(dateTime)) - } else { - deviceDao.getDeviceBeacons() - } - } + fun getDevicesOlderThanWithoutNotifications(since: LocalDateTime): List = deviceDao.getDevicesOlderThanWithoutNotifications(since) + +// @WorkerThread +// suspend fun getDeviceBeaconsSince(dateTime: String?): List { +// return if (dateTime != null) { +// deviceDao.getDeviceBeaconsSince(LocalDateTime.parse(dateTime)) +// } else { +// deviceDao.getDeviceBeacons() +// } +// } suspend fun getDeviceBeaconsSinceDate(dateTime: LocalDateTime?): List { return if (dateTime != null) { @@ -88,4 +89,14 @@ class DeviceRepository @Inject constructor(private val deviceDao: DeviceDao) { suspend fun setIgnoreFlag(deviceAddress: String, state: Boolean) { deviceDao.setIgnoreFlag(deviceAddress, state) } + + @WorkerThread + suspend fun delete(baseDevice: BaseDevice) { + deviceDao.deleteDevice(baseDevice) + } + + @WorkerThread + suspend fun deleteDevices(baseDevices: List) { + deviceDao.deleteDevices(baseDevices) + } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/LocationRepository.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/LocationRepository.kt index dc2fea6c..07d066c0 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/LocationRepository.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/LocationRepository.kt @@ -24,6 +24,13 @@ class LocationRepository @Inject constructor( fun getNumberOfBeaconsForLocation(locationId: Int): Int = locationDao.getNumberOfBeaconsForLocation(locationId) + fun getLocationsForBeacon(deviceAddress: String): List = locationDao.getLocationsForDevice(deviceAddress) + + fun getLocationsForBeaconSince(deviceAddress: String, since: LocalDateTime): List = locationDao.getLocationsForDeviceSince(deviceAddress, since) + + + fun getLocationsWithNoBeacons(): List = locationDao.getLocationsWithNoBeacons() + @WorkerThread suspend fun insert(location: LocationModel) { locationDao.insert(location) @@ -34,4 +41,14 @@ class LocationRepository @Inject constructor( locationDao.update(location) } + @WorkerThread + suspend fun delete(location: LocationModel) { + locationDao.delete(location) + } + + @WorkerThread + suspend fun deleteLocations(locations: List) { + locationDao.deleteLocations(locations) + } + } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/ScanRepository.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/ScanRepository.kt index 74af3196..972048fc 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/ScanRepository.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/ScanRepository.kt @@ -5,7 +5,6 @@ import de.seemoo.at_tracking_detection.database.daos.ScanDao import de.seemoo.at_tracking_detection.database.models.Scan import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator import kotlinx.coroutines.flow.Flow -import java.time.LocalDateTime import javax.inject.Inject class ScanRepository @Inject constructor( @@ -15,16 +14,13 @@ class ScanRepository @Inject constructor( var lastScan = scanDao.lastScan() var relevantScans = - scanDao.getScansSince(RiskLevelEvaluator.relevantTrackingDate) + scanDao.getScansSince(RiskLevelEvaluator.getRelevantTrackingDateForTrackingDetection()) - fun relevantScans(manual: Boolean, limit: Int): List = scanDao.getScansSince(RiskLevelEvaluator.relevantTrackingDate, manual, limit) + fun relevantScans(manual: Boolean, limit: Int): List = scanDao.getScansSince(RiskLevelEvaluator.getRelevantTrackingDateForTrackingDetection(), manual, limit) - val relevantDebugScans = scanDao.getDebugScansSince(RiskLevelEvaluator.relevantTrackingDate) + val relevantDebugScans = scanDao.getDebugScansSince(RiskLevelEvaluator.getRelevantTrackingDateForTrackingDetection()) - var flowRelevantScans = - scanDao.getFlowScansSince(RiskLevelEvaluator.relevantTrackingDate) - - val flowDebugScans = scanDao.getFlowDebugRelevantScans(RiskLevelEvaluator.relevantTrackingDate) + val flowDebugScans = scanDao.getFlowDebugRelevantScans(RiskLevelEvaluator.getRelevantTrackingDateForTrackingDetection()) var allScans: List = scanDao.getAllScans() @@ -32,15 +28,15 @@ class ScanRepository @Inject constructor( val totalCount: Int = scanDao.getNumberOfScans() - var countInRelevantTime: Int = scanDao.getNumberOfScansSince(RiskLevelEvaluator.relevantTrackingDate) + var countInRelevantTime: Int = scanDao.getNumberOfScansSince(RiskLevelEvaluator.getRelevantTrackingDateForTrackingDetection()) - val relevantUnfinishedScans: List = scanDao.unfinishedScans(RiskLevelEvaluator.relevantTrackingDate) + val relevantUnfinishedScans: List = scanDao.unfinishedScans(RiskLevelEvaluator.getRelevantTrackingDateForTrackingDetection()) @WorkerThread suspend fun insert(scan: Scan): Long = scanDao.insert(scan) @WorkerThread - suspend fun deleteIrrelevantScans() = scanDao.deleteUntil(RiskLevelEvaluator.relevantTrackingDate) + suspend fun deleteIrrelevantScans() = scanDao.deleteUntil(RiskLevelEvaluator.relevantTrackingDateForRiskCalculation) @WorkerThread suspend fun update(scan: Scan) = scanDao.update(scan) diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/viewmodel/NotificationViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/viewmodel/NotificationViewModel.kt index 78c48af8..1b134b44 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/viewmodel/NotificationViewModel.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/viewmodel/NotificationViewModel.kt @@ -2,6 +2,7 @@ package de.seemoo.at_tracking_detection.database.viewmodel import de.seemoo.at_tracking_detection.database.models.Notification import de.seemoo.at_tracking_detection.database.repository.NotificationRepository +import de.seemoo.at_tracking_detection.util.Utility import java.time.LocalDateTime import java.time.ZoneOffset import javax.inject.Inject @@ -9,7 +10,8 @@ import javax.inject.Inject class NotificationViewModel @Inject constructor(private val notificationRepository: NotificationRepository) { suspend fun insert(deviceAddress: String): Int { - val notification = Notification(deviceAddress, false, LocalDateTime.now(ZoneOffset.UTC)) + val sensitivity = Utility.getSensitivity() + val notification = Notification(deviceAddress, false, LocalDateTime.now(ZoneOffset.UTC), sensitivity) return notificationRepository.insert(notification).toInt() } diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt new file mode 100644 index 00000000..7725ce24 --- /dev/null +++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt @@ -0,0 +1,473 @@ +package de.seemoo.at_tracking_detection.detection + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.os.PowerManager +import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication +import de.seemoo.at_tracking_detection.BuildConfig +import de.seemoo.at_tracking_detection.database.models.Beacon +import de.seemoo.at_tracking_detection.database.models.Location +import de.seemoo.at_tracking_detection.database.models.Scan +import de.seemoo.at_tracking_detection.database.models.device.BaseDevice +import de.seemoo.at_tracking_detection.database.models.device.ConnectionState +import de.seemoo.at_tracking_detection.database.models.device.DeviceManager +import de.seemoo.at_tracking_detection.database.models.device.DeviceType +import de.seemoo.at_tracking_detection.database.repository.ScanRepository +import de.seemoo.at_tracking_detection.notifications.NotificationService +import de.seemoo.at_tracking_detection.ui.scan.ScanResultWrapper +import de.seemoo.at_tracking_detection.util.SharedPrefs +import de.seemoo.at_tracking_detection.util.Utility +import de.seemoo.at_tracking_detection.util.ble.BLEScanCallback +import de.seemoo.at_tracking_detection.worker.BackgroundWorkScheduler +import kotlinx.coroutines.delay +import timber.log.Timber +import java.time.LocalDateTime +import java.util.concurrent.ConcurrentHashMap +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +object BackgroundBluetoothScanner { + private lateinit var bluetoothAdapter: BluetoothAdapter + + private var scanResultDictionary: ConcurrentHashMap = ConcurrentHashMap() + + private var applicationContext: Context = ATTrackingDetectionApplication.getAppContext() + + var location: android.location.Location? = null + set(value) { + field = value + if (value != null) { + locationRetrievedCallback?.let { it() } + } + } + + private var locationRetrievedCallback: (() -> Unit)? = null + + private var locationFetchStarted: Long? = null + + val backgroundWorkScheduler: BackgroundWorkScheduler + get() { + return ATTrackingDetectionApplication.getCurrentApp().backgroundWorkScheduler + } + + val notificationService: NotificationService + get() { + return ATTrackingDetectionApplication.getCurrentApp().notificationService + } + private val locationProvider: LocationProvider + get() { + return ATTrackingDetectionApplication.getCurrentApp().locationProvider + } + + private val scanRepository: ScanRepository + get() { + return ATTrackingDetectionApplication.getCurrentApp().scanRepository + } + + private var isScanning = false + class BackgroundScanResults(var duration: Long, var scanMode: Int, var numberDevicesFound: Int, var failed: Boolean) + suspend fun scanInBackground(startedFrom: String): BackgroundScanResults { + if (isScanning) { + Timber.w("BackgroundBluetoothScanner scan already running") + return BackgroundScanResults(0, 0, 0, true) + } + + Timber.d("Starting BackgroundBluetoothScanner from $startedFrom") + val scanMode = getScanMode() + val scanId = scanRepository.insert(Scan(startDate = LocalDateTime.now(), isManual = false, scanMode = scanMode)) + + if (!Utility.checkBluetoothPermission()) { + Timber.d("Permission to perform bluetooth scan missing") + return BackgroundScanResults(0, 0, 0, true) + } + try { + val bluetoothManager = + applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + bluetoothAdapter = bluetoothManager.adapter + if (bluetoothAdapter.bluetoothLeScanner == null) { + Timber.e("BluetoothLeScanner not found!") + return BackgroundScanResults(0, 0, 0, true) + } + } catch (e: Throwable) { + Timber.e("BluetoothAdapter not found!") + return BackgroundScanResults(0, 0, 0, true) + } + + scanResultDictionary = ConcurrentHashMap() + isScanning = true + location = null + + // Set a wake lock to keep the CPU running while we complete the scanning + val powerManager = applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager + val wakeLock: PowerManager.WakeLock? = powerManager.run { + try { + newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::MyWakelockTag").apply { + acquire(5 * 60 * 1000L /*5 minutes*/) + } + } catch (e: SecurityException) { + Timber.w("Failed to acquire wake lock: ${e.message}") + null + } + } + + val useLocation = SharedPrefs.useLocationInTrackingDetection + if (useLocation) { + // Returns the last known location if this matches our requirements or starts new location updates + locationFetchStarted = System.currentTimeMillis() + location = locationProvider.lastKnownOrRequestLocationUpdates(locationRequester = locationRequester, timeoutMillis = LOCATION_UPDATE_MAX_TIME_MS - 2000L) + if (location == null) { + Timber.e("Failed to retrieve location") + } + } + + //Starting BLE Scan + Timber.d("Start Scanning for bluetooth le devices...") + val scanSettings = + ScanSettings.Builder().setScanMode(scanMode).build() + + SharedPrefs.isScanningInBackground = true + BLEScanCallback.startScanning(bluetoothAdapter.bluetoothLeScanner, DeviceManager.scanFilter, scanSettings, leScanCallback) + + val scanDuration: Long = getScanDuration() + delay(scanDuration) + BLEScanCallback.stopScanning(bluetoothAdapter.bluetoothLeScanner) + isScanning = false + + Timber.d("Scanning for bluetooth le devices stopped!. Discovered ${scanResultDictionary.size} devices") + + //Waiting for updated location to come in + Timber.d("Waiting for location update") + val fetchedLocation = waitForRequestedLocation() + Timber.d("Fetched location? $fetchedLocation") + if (location == null) { + // Get the last location no matter if the requirements match or not + location = locationProvider.getLastLocation(checkRequirements = false) + } + + val validDeviceTypes = DeviceType.getAllowedDeviceTypesFromSettings() + + //Adding all scan results to the database after the scan has finished + scanResultDictionary.forEach { (_, discoveredDevice) -> + val deviceType = discoveredDevice.wrappedScanResult.deviceType + + if (deviceType in validDeviceTypes) { + insertScanResult( + wrappedScanResult = discoveredDevice.wrappedScanResult, + latitude = location?.latitude, + longitude = location?.longitude, + altitude = location?.altitude, + accuracy = location?.accuracy, + discoveryDate = discoveredDevice.discoveryDate, + ) + } + } + + SharedPrefs.lastScanDate = LocalDateTime.now() + SharedPrefs.isScanningInBackground = false + val scan = scanRepository.scanWithId(scanId.toInt()) + if (scan != null) { + scan.endDate = LocalDateTime.now() + scan.duration = scanDuration.toInt() / 1000 + scan.noDevicesFound = scanResultDictionary.size + scanRepository.update(scan) + } + + Timber.d("Scheduling tracking detector worker") + backgroundWorkScheduler.scheduleTrackingDetector() + BackgroundWorkScheduler.scheduleAlarmWakeupIfScansFail() + + // Release the wake lock when we are done + wakeLock?.release() + + Timber.d("Finished Background Scan") + return BackgroundScanResults( + duration = scanDuration, + scanMode = scanMode, + numberDevicesFound = scanResultDictionary.size, + failed = false + ) + } + + private val leScanCallback: ScanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, scanResult: ScanResult) { + super.onScanResult(callbackType, scanResult) + val wrappedScanResult = ScanResultWrapper(scanResult) + //Checks if the device has been found already + if (!scanResultDictionary.containsKey(wrappedScanResult.uniqueIdentifier)) { + Timber.d("Found ${wrappedScanResult.uniqueIdentifier} at ${LocalDateTime.now()}") + scanResultDictionary[wrappedScanResult.uniqueIdentifier] = + DiscoveredDevice(wrappedScanResult, LocalDateTime.now()) + } + } + + override fun onScanFailed(errorCode: Int) { + super.onScanFailed(errorCode) + Timber.e("Bluetooth scan failed $errorCode") + if (BuildConfig.DEBUG) { + notificationService.sendBLEErrorNotification() + } + } + } + + private val locationRequester: LocationRequester = object : LocationRequester() { + override fun receivedAccurateLocationUpdate(location: android.location.Location) { + val started = locationFetchStarted ?: System.currentTimeMillis() + Timber.d("Got location in ${(System.currentTimeMillis()-started)/1000}s") + this@BackgroundBluetoothScanner.location = location + this@BackgroundBluetoothScanner.locationRetrievedCallback?.let { it() } + } + } + + private fun getScanMode(): Int { + val useLowPower = SharedPrefs.useLowPowerBLEScan + return if (useLowPower) { + ScanSettings.SCAN_MODE_LOW_POWER + } else { + ScanSettings.SCAN_MODE_LOW_LATENCY + } + } + + private fun getScanDuration(): Long { + val useLowPower = SharedPrefs.useLowPowerBLEScan + return if (useLowPower) { + 30_000L + } else { + 20_000L + } + } + + private suspend fun waitForRequestedLocation(): Boolean { + if (location != null || !SharedPrefs.useLocationInTrackingDetection) { + //Location already there. Just return + return true + } + + return suspendCoroutine { cont -> + var coroutineFinished = false + + val handler = Handler(Looper.getMainLooper()) + val runnable = Runnable { + if (!coroutineFinished) { + coroutineFinished = true + locationRetrievedCallback = null + Timber.d("Could not get location update in time.") + cont.resume(false) + } + } + + locationRetrievedCallback = { + if (!coroutineFinished) { + handler.removeCallbacks(runnable) + coroutineFinished = true + cont.resume(true) + } + } + + // Fallback if no location is fetched in time + val maximumLocationDurationMillis = LOCATION_UPDATE_MAX_TIME_MS + handler.postDelayed(runnable, maximumLocationDurationMillis) + } + } + + class DiscoveredDevice(var wrappedScanResult: ScanResultWrapper, var discoveryDate: LocalDateTime) + + const val MAX_DISTANCE_UNTIL_NEW_LOCATION: Float = 150f // in meters + const val TIME_BETWEEN_BEACONS: Long = + 15 // 15 minutes until the same beacon gets saved again in the db + private const val LOCATION_UPDATE_MAX_TIME_MS: Long = + 122_000L // Wait maximum 122s to get a location update + + suspend fun insertScanResult( + wrappedScanResult: ScanResultWrapper, + latitude: Double?, + longitude: Double?, + altitude: Double?, + accuracy: Float?, + discoveryDate: LocalDateTime, + ): Pair { + if (altitude != null && altitude > TrackingDetectorConstants.IGNORE_DEVICE_ABOVE_ALTITUDE) { + Timber.d("Ignoring device for locations above ${TrackingDetectorConstants.IGNORE_DEVICE_ABOVE_ALTITUDE}m, we assume the User is on a plane!") + // Do not save device at all in case we assume it is on a plane + return Pair(null, null) + } + + val deviceSaved = saveDevice(wrappedScanResult, discoveryDate) ?: return Pair( + null, + null + ) // return when device does not qualify to be saved + + // set locationId to null if gps location could not be retrieved + val locId: Int? = saveLocation( + latitude = latitude, + longitude = longitude, + altitude = altitude, + discoveryDate = discoveryDate, + accuracy = accuracy + )?.locationId + + val beaconSaved = + saveBeacon(wrappedScanResult, discoveryDate, locId) ?: return Pair(null, null) + + return Pair(deviceSaved, beaconSaved) + } + + private suspend fun saveBeacon( + wrappedScanResult: ScanResultWrapper, + discoveryDate: LocalDateTime, + locId: Int? + ): Beacon? { + val beaconRepository = + ATTrackingDetectionApplication.getCurrentApp().beaconRepository + val uuids = wrappedScanResult.serviceUuids + val uniqueIdentifier = wrappedScanResult.uniqueIdentifier + + val connectionState: ConnectionState = wrappedScanResult.connectionState + val connectionStateString = Utility.connectionStateToString(connectionState) + + var beacon: Beacon? = null + val beacons = beaconRepository.getDeviceBeaconsSince( + deviceAddress = uniqueIdentifier, + since = discoveryDate.minusMinutes(TIME_BETWEEN_BEACONS) + ) // sorted by newest first + + if (beacons.isEmpty()) { + Timber.d("Add new Beacon to the database!") + beacon = if (BuildConfig.DEBUG) { + // Save the manufacturer data to the beacon + Beacon( + discoveryDate, wrappedScanResult.rssiValue, wrappedScanResult.uniqueIdentifier, locId, + wrappedScanResult.mfg, uuids, connectionStateString + ) + } else { + Beacon( + discoveryDate, wrappedScanResult.rssiValue, wrappedScanResult.uniqueIdentifier, locId, + null, uuids, connectionStateString + ) + } + beaconRepository.insert(beacon) + } else if (beacons[0].locationId == null && locId != null && locId != 0) { + // Update beacon within the last TIME_BETWEEN_BEACONS minutes with location + Timber.d("Beacon already in the database... Adding Location") + beacon = beacons[0] + beacon.locationId = locId + if (beacon.connectionState == "UNKNOWN" && connectionState != ConnectionState.UNKNOWN) { + beacon.connectionState = connectionStateString + } + beaconRepository.update(beacon) + } + + Timber.d("Beacon: $beacon") + + return beacon + } + + private suspend fun saveDevice( + wrappedScanResult: ScanResultWrapper, + discoveryDate: LocalDateTime + ): BaseDevice? { + val deviceRepository = + ATTrackingDetectionApplication.getCurrentApp().deviceRepository + + val deviceAddress = wrappedScanResult.uniqueIdentifier + + // Checks if Device already exists in device database + var device = deviceRepository.getDevice(deviceAddress) + if (device == null) { + // Do not Save Samsung Devices + device = BaseDevice(wrappedScanResult.scanResult) + + // Check if ConnectionState qualifies Device to be saved + // Only Save when Device is offline long enough + if (wrappedScanResult.connectionState !in DeviceManager.savedConnectionStates) { + Timber.d("Device not in a saved connection state... Skipping!") + return null + } + + if (wrappedScanResult.connectionState !in DeviceManager.unsafeConnectionState) { + Timber.d("Device is safe and will be hidden to the user!") + device.safeTracker = true + } + + Timber.d("Add new Device to the database!") + deviceRepository.insert(device) + } else { + Timber.d("Device already in the database... Updating the last seen date!") + device.lastSeen = discoveryDate + deviceRepository.update(device) + } + + Timber.d("Device: $device") + return device + } + + private suspend fun saveLocation( + latitude: Double?, + longitude: Double?, + altitude: Double?, + discoveryDate: LocalDateTime, + accuracy: Float? + ): Location? { + if (altitude != null && altitude > TrackingDetectorConstants.IGNORE_LOCATION_ABOVE_ALTITUDE) { + Timber.d("Ignoring location above ${TrackingDetectorConstants.IGNORE_LOCATION_ABOVE_ALTITUDE}m, we assume the User might be on a plane!") + // Do not save location object + return null + } + + val locationRepository = + ATTrackingDetectionApplication.getCurrentApp().locationRepository + + // set location to null if gps location could not be retrieved + var location: Location? = null + + if (latitude != null && longitude != null) { + // Get closest location from database + location = locationRepository.closestLocation(latitude, longitude) + + var distanceBetweenLocations: Float = Float.MAX_VALUE + + if (location != null) { + val locationA = TrackingDetectorWorker.getLocation(latitude, longitude) + val locationB = + TrackingDetectorWorker.getLocation(location.latitude, location.longitude) + distanceBetweenLocations = locationA.distanceTo(locationB) + } + + if (location == null || distanceBetweenLocations > MAX_DISTANCE_UNTIL_NEW_LOCATION) { + // Create new location entry + Timber.d("Add new Location to the database!") + location = Location( + firstDiscovery = discoveryDate, + longitude = longitude, + latitude = latitude, + altitude = altitude, + accuracy = accuracy, + ) + locationRepository.insert(location) + } + else { + // If location is within the set limit, just use that location and update lastSeen + Timber.d("Location already in the database... Updating the last seen date!") + location.lastSeen = discoveryDate + if (altitude != null) { + location.altitude = altitude + } + if (accuracy != null && (location.accuracy == null || location.accuracy!! > accuracy)) { + location.accuracy = accuracy + location.longitude = longitude + location.latitude = latitude + } + locationRepository.update(location) + } + + Timber.d("Location: $location") + } + return location + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BluetoothReceiver.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BluetoothReceiver.kt index d10d5c03..7a7f37dc 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BluetoothReceiver.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BluetoothReceiver.kt @@ -6,7 +6,6 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build -import androidx.annotation.RequiresApi import dagger.hilt.android.AndroidEntryPoint import de.seemoo.at_tracking_detection.database.repository.BeaconRepository import de.seemoo.at_tracking_detection.database.repository.DeviceRepository @@ -22,12 +21,14 @@ class BluetoothReceiver : BroadcastReceiver() { @Inject lateinit var deviceRepository: DeviceRepository - @RequiresApi(Build.VERSION_CODES.O) override fun onReceive(context: Context, intent: Intent) { when (intent.action) { TrackingDetectorConstants.BLUETOOTH_DEVICE_FOUND_ACTION -> { - val scanResult = - intent.getParcelableArrayListExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT) + val scanResult = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableArrayListExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT, ScanResult::class.java) + } else { + intent.getParcelableArrayListExtra(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT) + } if (scanResult != null) { for (result: ScanResult in scanResult) { Timber.d("Found ${result.device.address}") diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/LocationProvider.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/LocationProvider.kt index 08c1d481..f87df307 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/LocationProvider.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/LocationProvider.kt @@ -1,33 +1,42 @@ package de.seemoo.at_tracking_detection.detection import android.Manifest -import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.location.Location import android.location.LocationListener import android.location.LocationManager -import android.os.* +import android.os.Handler +import android.os.Looper +import android.provider.Settings import androidx.core.content.ContextCompat import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication -import de.seemoo.at_tracking_detection.util.BuildVersionProvider import timber.log.Timber -import java.util.* +import java.util.Date import javax.inject.Inject import javax.inject.Singleton @Singleton open class LocationProvider @Inject constructor( - private val locationManager: LocationManager, - private val versionProvider: BuildVersionProvider): LocationListener { + private val locationManager: LocationManager): LocationListener { private val handler: Handler = Handler(Looper.getMainLooper()) private var bestLastLocation: Location? = null private val locationRequesters = ArrayList() - open fun getLastLocation(checkRequirements: Boolean = true): Location? { - if (ContextCompat.checkSelfPermission(ATTrackingDetectionApplication.getAppContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + fun getLastLocation(checkRequirements: Boolean = true): Location? { + if (ContextCompat.checkSelfPermission( + ATTrackingDetectionApplication.getAppContext(), + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + ATTrackingDetectionApplication.getAppContext(), + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { return null } @@ -35,12 +44,19 @@ open class LocationProvider @Inject constructor( } /** - * Fetches the most recent location from network and gps and returns the one that has been recveived more recently + * Fetches the most recent location from network and gps and returns the one that has been received more recently * @return the most recent location across multiple providers */ - @SuppressLint("InlinedApi") // Suppressed, because we use a custom version provider which is injectable for testing private fun getLastLocationFromAnyProvider(checkRequirements: Boolean): Location? { - if (ContextCompat.checkSelfPermission(ATTrackingDetectionApplication.getAppContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission( + ATTrackingDetectionApplication.getAppContext(), + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + ATTrackingDetectionApplication.getAppContext(), + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { return null } @@ -63,53 +79,46 @@ open class LocationProvider @Inject constructor( } private fun legacyGetLastLocationFromAnyProvider(checkRequirements: Boolean): Location? { - if (ContextCompat.checkSelfPermission(ATTrackingDetectionApplication.getAppContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // Check for location permission + if (ContextCompat.checkSelfPermission( + ATTrackingDetectionApplication.getAppContext(), + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + ATTrackingDetectionApplication.getAppContext(), + Manifest.permission.ACCESS_COARSE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { return null } - // On older versions we use both providers to get the best location signal + // Get the last known locations from both providers val networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER) + val gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) - if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { - val gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) - - if (gpsLocation != null && networkLocation != null) { - // Got to past locations, lets check which passes our requirements - val gpsRequirements = locationMatchesMinimumRequirements(gpsLocation) - val networkRequirements = locationMatchesMinimumRequirements(networkLocation) - if (gpsRequirements && networkRequirements) { - // Check which one is more current - if (gpsLocation.time > networkLocation.time) { - return gpsLocation - }else { - return networkLocation - } - }else if (gpsRequirements) { - // Only GPS satisfies the requirements. Return it - return gpsLocation - }else if (networkRequirements) { - // Only network satisfies. Return it - return networkLocation - }else if (!checkRequirements) { - if (gpsLocation.time > networkLocation.time) { - return gpsLocation - } - return networkLocation - } - }else if (gpsLocation != null && locationMatchesMinimumRequirements(gpsLocation)) { - // Only gps satisfies and network does not exist - return gpsLocation + // If both locations are available, return the one that is more current and meets the minimum requirements + if (networkLocation != null && gpsLocation != null) { + val bestLocation = if (gpsLocation.time > networkLocation.time) gpsLocation else networkLocation + if (locationMatchesMinimumRequirements(bestLocation)) { + return bestLocation } } + // If only one location is available, return it if it meets the minimum requirements if (networkLocation != null && locationMatchesMinimumRequirements(networkLocation)) { return networkLocation - }else if (!checkRequirements) { - return networkLocation + } + if (gpsLocation != null && locationMatchesMinimumRequirements(gpsLocation)) { + return gpsLocation } - Timber.d("No last know location matched the requirements") - return null + // If neither location meets the minimum requirements, return null + if (checkRequirements) { + return null + } + + // If no location requirements are specified, return the last known location from either provider, or null if none are available + return networkLocation ?: gpsLocation } private fun getSecondsSinceLocation(location: Location): Long { @@ -121,7 +130,16 @@ open class LocationProvider @Inject constructor( } private fun locationMatchesMinimumRequirements(location: Location): Boolean { - return location.accuracy <= MIN_ACCURACY_METER && getSecondsSinceLocation(location) <= MAX_AGE_SECONDS + if (location.accuracy <= MIN_ACCURACY_METER) { + if (getSecondsSinceLocation(location) <= MAX_AGE_SECONDS) { + return true + }else { + Timber.d("Location too old") + } + }else { + Timber.d("Location accuracy is not good enough") + } + return false } @@ -132,27 +150,38 @@ open class LocationProvider @Inject constructor( * @param timeoutMillis: After the timeout the last location will be returned no matter if it matches the requirements or not * @return the last known location if this already satisfies our requirements */ - @SuppressLint("InlinedApi") // Suppressed, because we use a custom version provider which is injectable for testing - open fun lastKnownOrRequestLocationUpdates(locationRequester: LocationRequester, timeoutMillis: Long?): Location? { - if (ContextCompat.checkSelfPermission(ATTrackingDetectionApplication.getAppContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + open fun lastKnownOrRequestLocationUpdates( + locationRequester: LocationRequester, + timeoutMillis: Long? = null + ): Location? { + // Check for location permission + if (ContextCompat.checkSelfPermission( + ATTrackingDetectionApplication.getAppContext(), + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED) { return null } + // Get the last known location val lastLocation = getLastLocation() + + // If the last location is available and meets the minimum requirements, return it if (lastLocation != null && locationMatchesMinimumRequirements(lastLocation)) { return lastLocation } + // Add the location requester to the list of active requesters this.locationRequesters.add(locationRequester) - // The fused location provider does not work reliably with Samsung + Android 12 - // We just stay with the legacy location, because this just works + // Request location updates from all enabled providers requestLocationUpdatesFromAnyProvider() + // If a timeout is specified, set a timeout for the location update if (timeoutMillis != null) { - setTimeoutForLocationUpdate(requester = locationRequester, timeoutMillis= timeoutMillis) + setTimeoutForLocationUpdate(requester = locationRequester, timeoutMillis = timeoutMillis) } + // Return null, since we don't have a location immediately available return null } @@ -164,55 +193,59 @@ open class LocationProvider @Inject constructor( * @param timeoutMillis milliseconds after which the timeout will be executed */ private fun setTimeoutForLocationUpdate(requester: LocationRequester, timeoutMillis: Long) { - val handler = Handler(Looper.getMainLooper()) - - val runnable = kotlinx.coroutines.Runnable { - if (this@LocationProvider.locationRequesters.size == 0) { - // The location was already returned + // Create a runnable to handle the timeout + val runnable = Runnable { + // If the location requester list is empty, the location has already been returned + if (this@LocationProvider.locationRequesters.isEmpty()) { return@Runnable } + // Log the timeout and get the last known location, regardless of whether it meets the requirements Timber.d("Location request timed out") val lastLocation = this@LocationProvider.getLastLocation(checkRequirements = false) + + // If the last location is available, notify the requester lastLocation?.let { - requester.receivedAccurateLocationUpdate(location = lastLocation) + requester.receivedAccurateLocationUpdate(location = it) } + + // If there is only one requester left, stop location updates and clear the list if (this@LocationProvider.locationRequesters.size == 1) { this@LocationProvider.stopLocationUpdates() this@LocationProvider.locationRequesters.clear() - }else { + } else { + // Otherwise, remove the requester from the list this@LocationProvider.locationRequesters.remove(requester) } } + // Schedule the runnable to be executed after the timeout period + val handler = Handler(Looper.getMainLooper()) handler.postDelayed(runnable, timeoutMillis) + + // Log the timeout settings Timber.d("Location request timeout set to $timeoutMillis") } - private fun requestLocationUpdatesFromAnyProvider() { - if (ContextCompat.checkSelfPermission(ATTrackingDetectionApplication.getAppContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // Check for location permission + if (ContextCompat.checkSelfPermission( + ATTrackingDetectionApplication.getAppContext(), + Manifest.permission.ACCESS_FINE_LOCATION + ) != PackageManager.PERMISSION_GRANTED + ) { + Timber.w("Not requesting location, permission not granted") return } - Timber.d("Requesting location updates") - val gpsProviderEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) - val networkProviderEnabled = - locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + // Get the list of enabled location providers + val enabledProviders = locationManager.allProviders + .filter { locationManager.isProviderEnabled(it) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && locationManager.isProviderEnabled(LocationManager.FUSED_PROVIDER)) { + // Request location updates from all enabled providers + enabledProviders.forEach { locationManager.requestLocationUpdates( - LocationManager.FUSED_PROVIDER, - MIN_UPDATE_TIME_MS, - MIN_DISTANCE_METER, - this, - handler.looper - ) - } - - if (networkProviderEnabled) { - locationManager.requestLocationUpdates( - LocationManager.NETWORK_PROVIDER, + it, MIN_UPDATE_TIME_MS, MIN_DISTANCE_METER, this, @@ -220,38 +253,22 @@ open class LocationProvider @Inject constructor( ) } - if (gpsProviderEnabled) { - // Using GPS and Network provider, because the GPS provider does notwork indoors (it will never call the callback) - locationManager.requestLocationUpdates( - LocationManager.GPS_PROVIDER, - MIN_UPDATE_TIME_MS, - MIN_DISTANCE_METER, - this, - handler.looper - ) - } + Timber.i("Requesting location updates from $enabledProviders") - if (!networkProviderEnabled && !gpsProviderEnabled) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (!locationManager.isProviderEnabled(LocationManager.FUSED_PROVIDER)) { - // Error - Timber.e("ERROR: No location provider available") - stopLocationUpdates() - } - }else { - //Error - Timber.e("ERROR: No location provider available") - stopLocationUpdates() - } + // If no location providers are enabled, log an error and stop location updates + if (enabledProviders.isEmpty()) { + Timber.e("ERROR: No location provider available") + stopLocationUpdates() } } - fun stopLocationUpdates() { + private fun stopLocationUpdates() { locationManager.removeUpdates(this) + Timber.i("Stopping location updates") } override fun onLocationChanged(location: Location) { - Timber.d("Location updated: ${location.latitude} ${location.longitude}") + Timber.d("Location updated: ${location.latitude} ${location.longitude}, accuracy: ${location.accuracy}, date: ${Date(location.time)}") val bestLastLocation = this.bestLastLocation if (bestLastLocation == null) { this.bestLastLocation = location @@ -276,9 +293,6 @@ open class LocationProvider @Inject constructor( } } - @Deprecated("Deprecated in Java") - override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} - // Android Phones with SDK < 30 need these methods override fun onProviderEnabled(provider: String) {} @@ -289,6 +303,13 @@ open class LocationProvider @Inject constructor( const val MIN_DISTANCE_METER = 0.0F const val MAX_AGE_SECONDS = 120L const val MIN_ACCURACY_METER = 120L + + fun isLocationTurnedOn(): Boolean { + val context = ATTrackingDetectionApplication.getAppContext() + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled( + LocationManager.FUSED_PROVIDER) + } } } diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/ScanBluetoothWorker.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/ScanBluetoothWorker.kt index 873d6671..420060db 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/ScanBluetoothWorker.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/ScanBluetoothWorker.kt @@ -40,337 +40,25 @@ import kotlin.coroutines.suspendCoroutine class ScanBluetoothWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted workerParams: WorkerParameters, - private val scanRepository: ScanRepository, - private val locationProvider: LocationProvider, - private val notificationService: NotificationService, - var backgroundWorkScheduler: BackgroundWorkScheduler + var backgroundWorkScheduler: BackgroundWorkScheduler, ) : CoroutineWorker(appContext, workerParams) { - private lateinit var bluetoothAdapter: BluetoothAdapter - - private var scanResultDictionary: ConcurrentHashMap = ConcurrentHashMap() - - var location: Location? = null - set(value) { - field = value - if (value != null) { - locationRetrievedCallback?.let { it() } - } - } - - private var locationRetrievedCallback: (() -> Unit)? = null - - private var locationFetchStarted: Long? = null override suspend fun doWork(): Result { - Timber.d("Bluetooth scanning worker started!") - val scanMode = getScanMode() - val scanId = scanRepository.insert(Scan(startDate = LocalDateTime.now(), isManual = false, scanMode = scanMode)) - - if (!Utility.checkBluetoothPermission()) { - Timber.d("Permission to perform bluetooth scan missing") - return Result.retry() - } - try { - val bluetoothManager = - applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager - bluetoothAdapter = bluetoothManager.adapter - } catch (e: Throwable) { - Timber.e("BluetoothAdapter not found!") - return Result.retry() - } - - scanResultDictionary = ConcurrentHashMap() - - val useLocation = SharedPrefs.useLocationInTrackingDetection - if (useLocation) { - // Returns the last known location if this matches our requirements or starts new location updates - locationFetchStarted = System.currentTimeMillis() - location = locationProvider.lastKnownOrRequestLocationUpdates(locationRequester = locationRequester, timeoutMillis = LOCATION_UPDATE_MAX_TIME_MS - 2000L) - } - - //Starting BLE Scan - Timber.d("Start Scanning for bluetooth le devices...") - val scanSettings = - ScanSettings.Builder().setScanMode(scanMode).build() - - SharedPrefs.isScanningInBackground = true - BLEScanCallback.startScanning(bluetoothAdapter.bluetoothLeScanner, DeviceManager.scanFilter, scanSettings, leScanCallback) - - val scanDuration: Long = getScanDuration() - delay(scanDuration) - BLEScanCallback.stopScanning(bluetoothAdapter.bluetoothLeScanner) - Timber.d("Scanning for bluetooth le devices stopped!. Discovered ${scanResultDictionary.size} devices") - - //Waiting for updated location to come in - val fetchedLocation = waitForRequestedLocation() - Timber.d("Fetched location? $fetchedLocation") - if (location == null) { - // Get the last location no matter if the requirements match or not - location = locationProvider.getLastLocation(checkRequirements = false) - } - - val validDeviceTypes = DeviceType.getAllowedDeviceTypesFromSettings() - //Adding all scan results to the database after the scan has finished - scanResultDictionary.forEach { (_, discoveredDevice) -> - val deviceType = DeviceManager.getDeviceType(discoveredDevice.scanResult) + val results = BackgroundBluetoothScanner.scanInBackground(startedFrom = "ScanBluetoothWorker") - if (deviceType in validDeviceTypes) { - insertScanResult( - discoveredDevice.scanResult, - location?.latitude, - location?.longitude, - location?.accuracy, - discoveredDevice.discoveryDate, - ) - } - } - - SharedPrefs.lastScanDate = LocalDateTime.now() - SharedPrefs.isScanningInBackground = false - val scan = scanRepository.scanWithId(scanId.toInt()) - if (scan != null) { - scan.endDate = LocalDateTime.now() - scan.duration = scanDuration.toInt() / 1000 - scan.noDevicesFound = scanResultDictionary.size - scanRepository.update(scan) + if (results.failed) { + return Result.retry() } - Timber.d("Scheduling tracking detector worker") - backgroundWorkScheduler.scheduleTrackingDetector() - BackgroundWorkScheduler.scheduleAlarmWakeupIfScansFail() - return Result.success( Data.Builder() - .putLong("duration", scanDuration) - .putInt("mode", scanMode) - .putInt("devicesFound", scanResultDictionary.size) + .putLong("duration", results.duration) + .putInt("mode", results.scanMode) + .putInt("devicesFound", results.numberDevicesFound) .build() ) } - - private val leScanCallback: ScanCallback = object : ScanCallback() { - override fun onScanResult(callbackType: Int, scanResult: ScanResult) { - super.onScanResult(callbackType, scanResult) - //Checks if the device has been found already - if (!scanResultDictionary.containsKey(getPublicKey(scanResult))) { - Timber.d("Found ${scanResult.device.address} at ${LocalDateTime.now()}") - scanResultDictionary[getPublicKey(scanResult)] = - DiscoveredDevice(scanResult, LocalDateTime.now()) - } - } - - override fun onScanFailed(errorCode: Int) { - super.onScanFailed(errorCode) - Timber.e("Bluetooth scan failed $errorCode") - if (BuildConfig.DEBUG) { - notificationService.sendBLEErrorNotification() - } - } - } - - private val locationRequester: LocationRequester = object : LocationRequester() { - override fun receivedAccurateLocationUpdate(location: Location) { - val started = locationFetchStarted ?: System.currentTimeMillis() - Timber.d("Got location in ${(System.currentTimeMillis()-started)/1000}s") - this@ScanBluetoothWorker.location = location - this@ScanBluetoothWorker.locationRetrievedCallback?.let { it() } - } - } - - private fun getScanMode(): Int { - val useLowPower = SharedPrefs.useLowPowerBLEScan - return if (useLowPower) { - ScanSettings.SCAN_MODE_LOW_POWER - } else { - ScanSettings.SCAN_MODE_LOW_LATENCY - } - } - - private fun getScanDuration(): Long { - val useLowPower = SharedPrefs.useLowPowerBLEScan - return if (useLowPower) { - 30_000L - } else { - 20_000L - } - } - - private suspend fun waitForRequestedLocation(): Boolean { - if (location != null || !SharedPrefs.useLocationInTrackingDetection) { - //Location already there. Just return - return true - } - - return suspendCoroutine { cont -> - var coroutineFinished = false - - val handler = Handler(Looper.getMainLooper()) - val runnable = Runnable { - if (!coroutineFinished) { - coroutineFinished = true - locationRetrievedCallback = null - Timber.d("Could not get location update in time.") - cont.resume(false) - } - } - - locationRetrievedCallback = { - if (!coroutineFinished) { - handler.removeCallbacks(runnable) - coroutineFinished = true - cont.resume(true) - } - } - - // Fallback if no location is fetched in time - val maximumLocationDurationMillis = LOCATION_UPDATE_MAX_TIME_MS - handler.postDelayed(runnable, maximumLocationDurationMillis) - } - } - - class DiscoveredDevice(var scanResult: ScanResult, var discoveryDate: LocalDateTime) - - companion object { - const val MAX_DISTANCE_UNTIL_NEW_LOCATION: Float = 150f // in meters - const val TIME_BETWEEN_BEACONS: Long = 15 // 15 minutes until the same beacon gets saved again in the db - const val LOCATION_UPDATE_MAX_TIME_MS: Long = 122_000L // Wait maximum 122s to get a location update - - suspend fun insertScanResult( - scanResult: ScanResult, - latitude: Double?, - longitude: Double?, - accuracy: Float?, - discoveryDate: LocalDateTime, - ) { - saveDevice(scanResult, discoveryDate) ?: return // return when device does not qualify to be saved - - // set locationId to null if gps location could not be retrieved - val locId: Int? = saveLocation(latitude, longitude, discoveryDate, accuracy)?.locationId - - saveBeacon(scanResult, discoveryDate, locId) - } - - private suspend fun saveBeacon( - scanResult: ScanResult, - discoveryDate: LocalDateTime, - locId: Int? - ): Beacon? { - val beaconRepository = ATTrackingDetectionApplication.getCurrentApp()?.beaconRepository ?: return null - val uuids = scanResult.scanRecord?.serviceUuids?.map { it.toString() }?.toList() - val uniqueIdentifier = getPublicKey(scanResult) - - var beacon: Beacon? = null - val beacons = beaconRepository.getDeviceBeaconsSince( - deviceAddress = uniqueIdentifier, - since = discoveryDate.minusMinutes(TIME_BETWEEN_BEACONS) - ) // sorted by newest first - - if (beacons.isEmpty()) { - Timber.d("Add new Beacon to the database!") - beacon = if (BuildConfig.DEBUG) { - // Save the manufacturer data to the beacon - Beacon( - discoveryDate, scanResult.rssi, getPublicKey(scanResult), locId, - scanResult.scanRecord?.bytes, uuids - ) - } else { - Beacon( - discoveryDate, scanResult.rssi, getPublicKey(scanResult), locId, - null, uuids - ) - } - beaconRepository.insert(beacon) - } else if (beacons[0].locationId == null && locId != null && locId != 0){ - // Update beacon within the last TIME_BETWEEN_BEACONS minutes with location - Timber.d("Beacon already in the database... Adding Location") - beacon = beacons[0] - beacon.locationId = locId - beaconRepository.update(beacon) - } - - Timber.d("Beacon: $beacon") - - return beacon - } - - private suspend fun saveDevice( - scanResult: ScanResult, - discoveryDate: LocalDateTime - ): BaseDevice? { - val deviceRepository = ATTrackingDetectionApplication.getCurrentApp()?.deviceRepository ?: return null - - val deviceAddress = getPublicKey(scanResult) - - // Checks if Device already exists in device database - var device = deviceRepository.getDevice(deviceAddress) - if (device == null) { - // Do not Save Samsung Devices - device = BaseDevice(scanResult) - - // Check if ConnectionState qualifies Device to be saved - // Only Save when Device is offline long enough - when(BaseDevice.getConnectionState(scanResult)){ - ConnectionState.OVERMATURE_OFFLINE -> {} - // ConnectionState.OFFLINE -> {} - // ConnectionState.PREMATURE_OFFLINE -> {} - ConnectionState.UNKNOWN -> {} - else -> return null - } - - Timber.d("Add new Device to the database!") - deviceRepository.insert(device) - } else { - Timber.d("Device already in the database... Updating the last seen date!") - device.lastSeen = discoveryDate - deviceRepository.update(device) - } - - Timber.d("Device: $device") - return device - } - - private suspend fun saveLocation( - latitude: Double?, - longitude: Double?, - discoveryDate: LocalDateTime, - accuracy: Float? - ): LocationModel? { - val locationRepository = ATTrackingDetectionApplication.getCurrentApp()?.locationRepository ?: return null - - // set location to null if gps location could not be retrieved - var location: LocationModel? = null - - if (latitude != null && longitude != null) { - // Get closest location from database - location = locationRepository.closestLocation(latitude, longitude) - - var distanceBetweenLocations: Float = Float.MAX_VALUE - - if (location != null) { - val locationA = getLocation(latitude, longitude) - val locationB = getLocation(location.latitude, location.longitude) - distanceBetweenLocations = locationA.distanceTo(locationB) - } - - if (location == null || distanceBetweenLocations > MAX_DISTANCE_UNTIL_NEW_LOCATION) { - // Create new location entry - Timber.d("Add new Location to the database!") - location = LocationModel(discoveryDate, longitude, latitude, accuracy) - locationRepository.insert(location) - } else { - // If location is within the set limit, just use that location and update lastSeen - Timber.d("Location already in the database... Updating the last seen date!") - location.lastSeen = discoveryDate - locationRepository.update(location) - - } - - Timber.d("Location: $location") - } - return location - } - } } diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/TrackingDetectorConstants.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/TrackingDetectorConstants.kt index 09d17997..48abf60b 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/TrackingDetectorConstants.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/TrackingDetectorConstants.kt @@ -1,6 +1,8 @@ package de.seemoo.at_tracking_detection.detection object TrackingDetectorConstants { + const val IGNORE_DEVICE_ABOVE_ALTITUDE = 9000.0 + const val IGNORE_LOCATION_ABOVE_ALTITUDE = 3000.0 const val BLUETOOTH_DEVICE_FOUND_ACTION = "de.seemoo.at_tracking_detection.BLUETOOTH_DEVICE_FOUND" } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/TrackingDetectorWorker.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/TrackingDetectorWorker.kt index 929fa273..ca9c1268 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/TrackingDetectorWorker.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/TrackingDetectorWorker.kt @@ -9,6 +9,7 @@ import androidx.work.Data import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject +import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication import de.seemoo.at_tracking_detection.database.repository.BeaconRepository import de.seemoo.at_tracking_detection.database.repository.DeviceRepository import de.seemoo.at_tracking_detection.database.models.Beacon @@ -16,11 +17,10 @@ import de.seemoo.at_tracking_detection.database.models.device.BaseDevice import de.seemoo.at_tracking_detection.database.repository.NotificationRepository import de.seemoo.at_tracking_detection.notifications.NotificationService import de.seemoo.at_tracking_detection.util.SharedPrefs -import de.seemoo.at_tracking_detection.util.risk.RiskLevel import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator import timber.log.Timber +import java.time.Duration import java.time.LocalDateTime -import java.time.temporal.ChronoUnit import java.util.concurrent.ConcurrentHashMap @HiltWorker @@ -34,6 +34,8 @@ class TrackingDetectorWorker @AssistedInject constructor( ) : CoroutineWorker(appContext, workerParams) { override suspend fun doWork(): Result { + deleteOldAndSafeTrackers() + Timber.d("Tracking detection background job started!") // Just writing a new comment in here. val ignoredDevices = deviceRepository.ignoredDevicesSync @@ -51,7 +53,7 @@ class TrackingDetectorWorker @AssistedInject constructor( val device = deviceRepository.getDevice(mapEntry.key) ?: return@forEach val useLocation = SharedPrefs.useLocationInTrackingDetection - if (RiskLevelEvaluator.checkRiskLevelForDevice(device, useLocation) != RiskLevel.LOW && checkLastNotification(device)) { + if (throwNotification(device, useLocation)) { // Send Notification Timber.d("Conditions for device ${device.address} being a tracking device are true... Sending Notification!") notificationService.sendTrackingNotification(device) @@ -82,12 +84,77 @@ class TrackingDetectorWorker @AssistedInject constructor( //Gets all beacons found in the last scan. Then we get all beacons for the device that emitted one of those beaconRepository.getLatestBeacons(since).forEach { // Only retrieve the last two weeks since they are only relevant for tracking - val beacons = beaconRepository.getDeviceBeaconsSince(it.deviceAddress, RiskLevelEvaluator.relevantTrackingDate) + val beacons = beaconRepository.getDeviceBeaconsSince(it.deviceAddress, RiskLevelEvaluator.relevantTrackingDateForRiskCalculation) beaconsPerDevice[it.deviceAddress] = beacons } return beaconsPerDevice } + private fun throwNotification(device: BaseDevice, useLocation: Boolean): Boolean { + val minNumberOfLocations: Int = RiskLevelEvaluator.getNumberOfLocationsToBeConsideredForTrackingDetection(device.deviceType) + val minTrackedTime: Long = RiskLevelEvaluator.getMinutesAtLeastTrackedBeforeAlarm() // in minutes + + val deviceIdentifier: String = device.address + val relevantHours: Long = device.deviceType?.getNumberOfHoursToBeConsideredForTrackingDetection() ?: RiskLevelEvaluator.RELEVANT_HOURS_TRACKING + var considerDetectionEventSince: LocalDateTime = RiskLevelEvaluator.getRelevantTrackingDateForTrackingDetection(relevantHours) + + val lastNotificationSent = device.lastNotificationSent + if (lastNotificationSent != null && lastNotificationSent > considerDetectionEventSince && lastNotificationSent < LocalDateTime.now()) { + considerDetectionEventSince = lastNotificationSent + } + + val detectionEvents: List = beaconRepository.getDeviceBeaconsSince(deviceIdentifier, considerDetectionEventSince) + + val detectionEventsSorted: List = detectionEvents.sortedBy { it.receivedAt } + val earliestDetectionEvent: Beacon = detectionEventsSorted.firstOrNull() ?: return false + val timeFollowing: Long = Duration.between(earliestDetectionEvent.receivedAt, LocalDateTime.now()).toMinutes() + + val filteredDetectionEvents = detectionEvents.filter { it.locationId != null && it.locationId != 0 } + val distinctDetectionEvent = filteredDetectionEvents.map { it.locationId }.distinct() + val locations = distinctDetectionEvent.size + + if (timeFollowing >= minTrackedTime) { + if (locations >= minNumberOfLocations || !useLocation) { + return true + } + } + return false + } + + private suspend fun deleteOldAndSafeTrackers() { + // Delete old devices and beacons from the database + Timber.d("Start deleting old and safe Trackers") + val deleteSafeTrackersBefore = RiskLevelEvaluator.deleteBeforeDate + val beaconsToBeDeleted = beaconRepository.getBeaconsOlderThanWithoutNotifications(deleteSafeTrackersBefore) + if (beaconsToBeDeleted.isNotEmpty()) { + Timber.d("Deleting ${beaconsToBeDeleted.size} beacons") + beaconRepository.deleteBeacons(beaconsToBeDeleted) + Timber.d("Deleting Beacons successful") + } else { + Timber.d("No old beacons to delete") + } + + val devicesToBeDeleted = deviceRepository.getDevicesOlderThanWithoutNotifications(deleteSafeTrackersBefore) + if (devicesToBeDeleted.isNotEmpty()) { + Timber.d("Deleting ${devicesToBeDeleted.size} devices") + deviceRepository.deleteDevices(devicesToBeDeleted) + Timber.d("Deleting Devices successful") + } + if (beaconsToBeDeleted.isEmpty() && devicesToBeDeleted.isEmpty()) { + Timber.d("No old devices or beacons to delete") + } + + val locationRepository = ATTrackingDetectionApplication.getCurrentApp()?.locationRepository ?: return + val locationsToBeDeleted = locationRepository.getLocationsWithNoBeacons() + if (locationsToBeDeleted.isNotEmpty()) { + Timber.d("Deleting ${locationsToBeDeleted.size} locations") + locationRepository.deleteLocations(locationsToBeDeleted) + Timber.d("Deleting Locations successful") + } else { + Timber.d("No locations to delete") + } + } + companion object { fun getLocation(latitude: Double, longitude: Double): Location { val location = Location(LocationManager.GPS_PROVIDER) @@ -95,19 +162,6 @@ class TrackingDetectorWorker @AssistedInject constructor( location.longitude = longitude return location } - - /** - * Checks if the last notification was sent more than x hours ago - */ - private fun checkLastNotification(device: BaseDevice): Boolean { - val lastNotificationSent = device.lastNotificationSent - return lastNotificationSent == null || isTimeToNotify(lastNotificationSent) - } - - private fun isTimeToNotify(lastNotificationSent: LocalDateTime): Boolean { - val hoursPassed = lastNotificationSent.until(LocalDateTime.now(), ChronoUnit.HOURS) - return hoursPassed >= RiskLevelEvaluator.HOURS_AT_LEAST_UNTIL_NEXT_NOTIFICATION - } } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/hilt/ApiModule.kt b/app/src/main/java/de/seemoo/at_tracking_detection/hilt/ApiModule.kt index ae5f3488..7ac60a54 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/hilt/ApiModule.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/hilt/ApiModule.kt @@ -20,7 +20,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object ApiModule { - val HTTP_TIMEOUT: Long = 60 + private const val HTTP_TIMEOUT: Long = 60 @Provides @Singleton fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder() diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/hilt/DatabaseModule.kt b/app/src/main/java/de/seemoo/at_tracking_detection/hilt/DatabaseModule.kt index a5f0d8ef..cd270e84 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/hilt/DatabaseModule.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/hilt/DatabaseModule.kt @@ -13,7 +13,7 @@ import dagger.hilt.components.SingletonComponent import de.seemoo.at_tracking_detection.database.AppDatabase import de.seemoo.at_tracking_detection.database.daos.* import de.seemoo.at_tracking_detection.database.repository.* -import de.seemoo.at_tracking_detection.detection.ScanBluetoothWorker.Companion.MAX_DISTANCE_UNTIL_NEW_LOCATION +import de.seemoo.at_tracking_detection.detection.BackgroundBluetoothScanner import de.seemoo.at_tracking_detection.detection.TrackingDetectorWorker.Companion.getLocation import timber.log.Timber import javax.inject.Singleton @@ -23,9 +23,9 @@ import javax.inject.Singleton object DatabaseModule { val MIGRATION_5_7 = object : Migration(5, 7) { - override fun migrate(database: SupportSQLiteDatabase) { + override fun migrate(db: SupportSQLiteDatabase) { try { - database.execSQL("ALTER TABLE `beacon` ADD COLUMN `serviceUUIDs` TEXT DEFAULT NULL") + db.execSQL("ALTER TABLE `beacon` ADD COLUMN `serviceUUIDs` TEXT DEFAULT NULL") }catch (e: SQLiteException) { Timber.e("Could not create new column ${e}") } @@ -34,17 +34,17 @@ object DatabaseModule { } val MIGRATION_6_7 = object : Migration(6, 7) { - override fun migrate(database: SupportSQLiteDatabase) { + override fun migrate(db: SupportSQLiteDatabase) { } } val MIGRATION_9_10 = object : Migration(9, 10) { - override fun migrate(database: SupportSQLiteDatabase) { + override fun migrate(db: SupportSQLiteDatabase) { // add location table and locationID to beacon try { - database.execSQL("CREATE TABLE `location` (`locationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `firstDiscovery` TEXT NOT NULL, `lastSeen` TEXT NOT NULL, `longitude` REAL NOT NULL, `latitude` REAL NOT NULL, `accuracy` REAL)") - database.execSQL("CREATE UNIQUE INDEX `index_location_latitude_longitude` ON `location` (`latitude`, `longitude`)") - database.execSQL("ALTER TABLE `beacon` ADD COLUMN `locationId` INTEGER") + db.execSQL("CREATE TABLE `location` (`locationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `firstDiscovery` TEXT NOT NULL, `lastSeen` TEXT NOT NULL, `longitude` REAL NOT NULL, `latitude` REAL NOT NULL, `accuracy` REAL)") + db.execSQL("CREATE UNIQUE INDEX `index_location_latitude_longitude` ON `location` (`latitude`, `longitude`)") + db.execSQL("ALTER TABLE `beacon` ADD COLUMN `locationId` INTEGER") }catch (e: SQLiteException) { Timber.e("Could not create location ${e}") } @@ -52,7 +52,7 @@ object DatabaseModule { var sql: String while (true) { sql = "SELECT * FROM `beacon` WHERE `locationId` IS NULL AND `latitude` IS NOT NULL AND `longitude` IS NOT NULL LIMIT 1" - val beacon = database.query(sql) + val beacon = db.query(sql) if (beacon.count == 0) { // If there are no more locations left to do, then break @@ -68,7 +68,7 @@ object DatabaseModule { // println("Longitude: $longitude") sql = "SELECT `longitude`, `latitude` FROM `location` ORDER BY ABS(`latitude` - $latitude) + ABS(`longitude` - $longitude) ASC LIMIT 1" - val closestLocation = database.query(sql) + val closestLocation = db.query(sql) var insertNewLocation = false @@ -80,7 +80,7 @@ object DatabaseModule { val locationA = getLocation(latitude, longitude) val locationB = getLocation(closestLatitude, closestLongitude) val distanceBetweenLocations = locationA.distanceTo(locationB) - if (distanceBetweenLocations > MAX_DISTANCE_UNTIL_NEW_LOCATION){ + if (distanceBetweenLocations > BackgroundBluetoothScanner.MAX_DISTANCE_UNTIL_NEW_LOCATION){ // println("Insert New, because far enough away") insertNewLocation = true } else { @@ -103,7 +103,7 @@ object DatabaseModule { var lastSeen = firstDiscovery // receivedAt sql = "SELECT `firstDiscovery`, `lastSeen` FROM `device` WHERE `address` = '$deviceAddress'" - val device = database.query(sql) + val device = db.query(sql) if (device.count > 0) { // println("Successfully got timestamps from device table") @@ -113,11 +113,11 @@ object DatabaseModule { } sql = "INSERT INTO `location` (`firstDiscovery`, `lastSeen`, `longitude`, `latitude`) VALUES ('$firstDiscovery', '$lastSeen', $longitude, $latitude)" - database.execSQL(sql) + db.execSQL(sql) } sql = "SELECT `locationId` FROM `location` WHERE `latitude` = $latitude AND `longitude` = $longitude" - val location = database.query(sql) + val location = db.query(sql) println(location.count) if (location.count > 0) { // else: locationId stays null location.moveToFirst() @@ -126,25 +126,25 @@ object DatabaseModule { val beaconId = beacon.getInt(0) println("beaconId: $beaconId") sql = "UPDATE `beacon` SET `locationId` = $locationId WHERE `locationId` IS NULL AND `beaconId` = $beaconId" - database.execSQL(sql) + db.execSQL(sql) sql = "SELECT * FROM `beacon` WHERE `locationId` IS NOT NULL" - println(database.query(sql).count) + println(db.query(sql).count) } } try { - database.execSQL("CREATE TABLE `beacon_backup` (`beaconId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `receivedAt` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `deviceAddress` TEXT NOT NULL, `locationId` INTEGER, `mfg` BLOB, `serviceUUIDs` TEXT)") - database.execSQL("INSERT INTO `beacon_backup` SELECT `beaconId`, `receivedAt`, `rssi`, `deviceAddress`, `locationId`, `mfg`, `serviceUUIDs` FROM `beacon`") - database.execSQL("DROP TABLE `beacon`") + db.execSQL("CREATE TABLE `beacon_backup` (`beaconId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `receivedAt` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `deviceAddress` TEXT NOT NULL, `locationId` INTEGER, `mfg` BLOB, `serviceUUIDs` TEXT)") + db.execSQL("INSERT INTO `beacon_backup` SELECT `beaconId`, `receivedAt`, `rssi`, `deviceAddress`, `locationId`, `mfg`, `serviceUUIDs` FROM `beacon`") + db.execSQL("DROP TABLE `beacon`") } catch (e: SQLiteException) { Timber.e("Could not create beacon_backup ${e}") } try { - database.execSQL("CREATE TABLE `beacon` (`beaconId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `receivedAt` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `deviceAddress` TEXT NOT NULL, `locationId` INTEGER, `mfg` BLOB, `serviceUUIDs` TEXT)") - database.execSQL("INSERT INTO `beacon` SELECT `beaconId`, `receivedAt`, `rssi`, `deviceAddress`, `locationId`, `mfg`, `serviceUUIDs` FROM `beacon_backup`") - database.execSQL("DROP TABLE `beacon_backup`") + db.execSQL("CREATE TABLE `beacon` (`beaconId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `receivedAt` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `deviceAddress` TEXT NOT NULL, `locationId` INTEGER, `mfg` BLOB, `serviceUUIDs` TEXT)") + db.execSQL("INSERT INTO `beacon` SELECT `beaconId`, `receivedAt`, `rssi`, `deviceAddress`, `locationId`, `mfg`, `serviceUUIDs` FROM `beacon_backup`") + db.execSQL("DROP TABLE `beacon_backup`") } catch (e: SQLiteException) { Timber.e("Could not create beacon ${e}") } diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationBuilder.kt b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationBuilder.kt index e0379385..e4b2e0d9 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationBuilder.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationBuilder.kt @@ -90,11 +90,7 @@ class NotificationBuilder @Inject constructor( context, code, intent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.FLAG_IMMUTABLE - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } + PendingIntent.FLAG_IMMUTABLE ) } @@ -104,9 +100,11 @@ class NotificationBuilder @Inject constructor( ): Notification { Timber.d("Notification with id $notificationId for device $deviceAddress has been build!") val bundle: Bundle = packBundle(deviceAddress, notificationId) - val notifyText = context.getString( - R.string.notification_text_base, - RiskLevelEvaluator.getMinutesAtLeastTrackedBeforeAlarm() + val minutesAtLeastTracked = RiskLevelEvaluator.getMinutesAtLeastTrackedBeforeAlarm() + val notifyText = context.resources.getQuantityString( + R.plurals.notification_text_base, + minutesAtLeastTracked.toInt(), + minutesAtLeastTracked ) var notification = NotificationCompat.Builder(context, NotificationConstants.CHANNEL_ID) @@ -127,7 +125,7 @@ class NotificationBuilder @Inject constructor( ) ) - val deviceRepository = ATTrackingDetectionApplication.getCurrentApp()?.deviceRepository!! + val deviceRepository = ATTrackingDetectionApplication.getCurrentApp().deviceRepository val device = deviceRepository.getDevice(deviceAddress) if (device?.deviceType != null && device.deviceType.canBeIgnored()) { @@ -233,16 +231,11 @@ class NotificationBuilder @Inject constructor( val bundle: Bundle = packBundle(deviceAddress, notificationId) val notifyText = if (observationPositive) { - if (observationDuration == 1L) { - context.getString( - R.string.notification_observe_tracker_positive_singular, - ) - } else { - context.getString( - R.string.notification_observe_tracker_positive_plural, - observationDuration - ) - } + context.resources.getQuantityString( + R.plurals.notification_observe_tracker_positive, + observationDuration.toInt(), + observationDuration + ) } else { context.getString( R.string.notification_observe_tracker_negative, @@ -270,6 +263,20 @@ class NotificationBuilder @Inject constructor( } + fun buildObserveTrackerFailedNotification(notificationId: Int): Notification { + val bundle: Bundle = Bundle().apply { putInt("notificationId", notificationId) } + + return NotificationCompat.Builder(context, NotificationConstants.CHANNEL_ID) + .setContentTitle(context.getString(R.string.notification_observe_tracker_title_base)) + .setContentText(context.getString(R.string.notification_observe_tracker_error)) + .setPriority(getNotificationPriority()) + .setContentIntent(pendingNotificationIntent(bundle, notificationId)) + .setCategory(Notification.CATEGORY_ERROR) + .setSmallIcon(R.drawable.ic_scan_icon) + .setAutoCancel(true) + .build() + } + fun buildBluetoothErrorNotification(): Notification { val notificationId = -100 val bundle: Bundle = Bundle().apply { putInt("notificationId", notificationId) } diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationService.kt b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationService.kt index c34ebbe3..91c01e9c 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationService.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationService.kt @@ -70,6 +70,20 @@ class NotificationService @Inject constructor( } } + @SuppressLint("MissingPermission") + fun sendObserveTrackerFailedNotification() { + val notificationId = generateNotificationId() + with(notificationManagerCompat) { + if (this.areNotificationsEnabled()) { + notify( + OBSERVE_TRACKER_NOTIFICATION_TAG, + notificationId, + notificationBuilder.buildObserveTrackerFailedNotification(notificationId) + ) + } + } + } + /* @SuppressLint("MissingPermission") suspend fun sendObserveTrackerNotification(baseDevice: BaseDevice) { @@ -132,7 +146,7 @@ class NotificationService @Inject constructor( // Do not send multiple notifications if (!ATTrackingDetectionApplication.SURVEY_IS_RUNNING) {return} - if (SharedPrefs.surveyNotficationSent && !replace) {return} + if (SharedPrefs.surveyNotificationSent && !replace) {return} //Check if already scheduled val notificationDate = SharedPrefs.surveyNotificationDate if ( replace || notificationDate == null || notificationDate < LocalDateTime.now()) { @@ -162,28 +176,26 @@ class NotificationService @Inject constructor( } fun setup() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - Timber.d("Setting up NotificationManager") - // Register the channel with the system - val channel = NotificationChannelCompat.Builder( - NotificationConstants.CHANNEL_ID, - NotificationManagerCompat.IMPORTANCE_HIGH - ) - .setName(NotificationConstants.NOTIFICATION_CHANNEL_NAME) - .build() - - notificationManagerCompat.createNotificationChannel(channel) - - //Register the info channel - val infoChannel = NotificationChannelCompat.Builder( - NotificationConstants.INFO_CHANNEL_ID, - NotificationManagerCompat.IMPORTANCE_LOW - ) - .setName(NotificationConstants.NOTIFICATION_CHANNEL_INFO) - .build() - - notificationManagerCompat.createNotificationChannel(infoChannel) - } + Timber.d("Setting up NotificationManager") + // Register the channel with the system + val channel = NotificationChannelCompat.Builder( + NotificationConstants.CHANNEL_ID, + NotificationManagerCompat.IMPORTANCE_HIGH + ) + .setName(NotificationConstants.NOTIFICATION_CHANNEL_NAME) + .build() + + notificationManagerCompat.createNotificationChannel(channel) + + //Register the info channel + val infoChannel = NotificationChannelCompat.Builder( + NotificationConstants.INFO_CHANNEL_ID, + NotificationManagerCompat.IMPORTANCE_LOW + ) + .setName(NotificationConstants.NOTIFICATION_CHANNEL_INFO) + .build() + + notificationManagerCompat.createNotificationChannel(infoChannel) } companion object { diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/ScheduledNotificationReceiver.kt b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/ScheduledNotificationReceiver.kt index c3c2cd36..6b837113 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/ScheduledNotificationReceiver.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/ScheduledNotificationReceiver.kt @@ -3,7 +3,6 @@ package de.seemoo.at_tracking_detection.notifications import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication import de.seemoo.at_tracking_detection.util.SharedPrefs import timber.log.Timber @@ -11,7 +10,7 @@ class ScheduledNotificationReceiver: BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { Timber.d("Broadcast received ${intent?.action}") - val notificationService = ATTrackingDetectionApplication.getCurrentApp()?.notificationService + // val notificationService = ATTrackingDetectionApplication.getCurrentApp()?.notificationService SharedPrefs.dismissSurveyInformation = true } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/statistics/SendStatisticsWorker.kt b/app/src/main/java/de/seemoo/at_tracking_detection/statistics/SendStatisticsWorker.kt index c7548fd3..f27e10b4 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/statistics/SendStatisticsWorker.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/statistics/SendStatisticsWorker.kt @@ -83,8 +83,7 @@ class SendStatisticsWorker @AssistedInject constructor( beacon.receivedAt >= uploadDateTime } it.beacons.forEach { beacon -> - // beacon.latitude = null - // beacon.longitude = null + // Remove location and device address beacon.locationId = null beacon.deviceAddress = "" } diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/MainActivity.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/MainActivity.kt index d9a08f70..8a54b931 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/MainActivity.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/MainActivity.kt @@ -17,6 +17,7 @@ import de.seemoo.at_tracking_detection.BuildConfig import de.seemoo.at_tracking_detection.R import de.seemoo.at_tracking_detection.util.SharedPrefs import de.seemoo.at_tracking_detection.util.ble.BLEScanner +import de.seemoo.at_tracking_detection.worker.BackgroundWorkScheduler import org.osmdroid.config.Configuration import timber.log.Timber import java.io.File @@ -25,13 +26,18 @@ import java.time.ZoneOffset import javax.inject.Inject @AndroidEntryPoint -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener { @Inject lateinit var sharedPreferences: SharedPreferences + @Inject + lateinit var backgroundWorkScheduler: BackgroundWorkScheduler + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + sharedPreferences.registerOnSharedPreferenceChangeListener(this) + window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this) val configuration = Configuration.getInstance() configuration.load(this, PreferenceManager.getDefaultSharedPreferences(this)) @@ -64,6 +70,13 @@ class MainActivity : AppCompatActivity() { if (BuildConfig.DEBUG) { appBarItems.plus(R.id.navigation_debug) } + + if (!SharedPrefs.advancedMode) { + val menu = navView.menu + val item = menu.findItem(R.id.navigation_allDevicesFragment) + item.isVisible = false + } + val appBarConfiguration = AppBarConfiguration(appBarItems) setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) @@ -86,6 +99,9 @@ class MainActivity : AppCompatActivity() { super.onResume() Timber.d("MainActivity onResume called") BLEScanner.startBluetoothScan(this.applicationContext) + + Timber.d("Scheduling an immediate background scan onResume of MainActivity") + backgroundWorkScheduler.scheduleImmediateBackgroundScan() } @@ -97,6 +113,7 @@ class MainActivity : AppCompatActivity() { override fun onDestroy() { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) SharedPrefs.lastTimeOpened = dateTime super.onDestroy() } @@ -109,4 +126,15 @@ class MainActivity : AppCompatActivity() { companion object { private val dateTime = LocalDateTime.now(ZoneOffset.UTC) } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + // Check if the changed preference is the advancedMode + if (key == "advanced_mode") { + // Update the visibility of the All Devices fragment menu item + val navView: BottomNavigationView = findViewById(R.id.main_nav_view) + val menu = navView.menu + val item = menu.findItem(R.id.navigation_allDevicesFragment) + item.isVisible = sharedPreferences?.getBoolean(key, false) ?: false + } + } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/MarkdownViewerActivity.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/MarkdownViewerActivity.kt deleted file mode 100644 index 244dd9de..00000000 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/MarkdownViewerActivity.kt +++ /dev/null @@ -1,45 +0,0 @@ -package de.seemoo.at_tracking_detection.ui - -import android.os.Bundle -import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity -import de.seemoo.at_tracking_detection.R -import io.noties.markwon.Markwon - -class MarkdownViewerActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_markdown_viewer) - - val markdown = """ - # Hello Markdown - - This is a sample Markdown file rendered using Markwon library in Kotlin. - - - List item 1 - - List item 2 - - List item 3 - - **Bold Text** - - *Italic Text* - - ![Image](https://example.com/image.jpg) - - `Inline Code` - - ```kotlin - fun main() { - println("Hello, Markdown!") - } - ``` - """.trimIndent() - - val markwon = Markwon.builder(this) - .build() - - val markdownTextView = findViewById(R.id.markdownTextView) - markwon.setMarkdown(markdownTextView, markdown) - } -} \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/OnboardingActivity.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/OnboardingActivity.kt index 31af1aa7..2d35eeb2 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/OnboardingActivity.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/OnboardingActivity.kt @@ -77,7 +77,7 @@ class OnboardingActivity : AppIntro() { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK }) } else { - onBackPressed() + finish() } } @@ -218,9 +218,7 @@ class OnboardingActivity : AppIntro() { notificationSlide(slideNumber + 3) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - addSlide(IgnoreBatteryOptimizationFragment.newInstance()) - } + addSlide(IgnoreBatteryOptimizationFragment.newInstance()) addSlide(ShareDataFragment.newInstance()) diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt index 5e0bc7f5..4a4577fc 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt @@ -2,6 +2,7 @@ package de.seemoo.at_tracking_detection.ui import android.content.Intent import android.os.Bundle +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment @@ -18,30 +19,35 @@ class TrackingNotificationActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_tracking) - val navHostFragment = - supportFragmentManager.findFragmentById(R.id.tracking_host_fragment) as NavHostFragment + val navHostFragment = supportFragmentManager.findFragmentById(R.id.tracking_host_fragment) as NavHostFragment navController = navHostFragment.navController + val deviceAddress = intent.getStringExtra("deviceAddress") val notificationId = intent.getIntExtra("notificationId", -1) Timber.d("Tracking Activity with device $deviceAddress and notification $notificationId started!") + if (deviceAddress == null) { Timber.e("Device address is needed! Going home...") this.onSupportNavigateUp() } else { - val args = TrackingFragmentArgs(deviceAddress, notificationId).toBundle() + val args = TrackingFragmentArgs( + deviceAddress = deviceAddress, + notificationId = notificationId + ).toBundle() navController.setGraph(R.navigation.tracking_navigation, args) } - } - override fun onBackPressed() { - onNavigateUp() + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + onNavigateUp() + } + }) } override fun onSupportNavigateUp(): Boolean { if (!navController.navigateUp()) { startActivity(Intent(this, MainActivity::class.java).apply { - flags = - Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK }) } return true diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/Article.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/Article.kt new file mode 100644 index 00000000..848aa18d --- /dev/null +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/Article.kt @@ -0,0 +1,96 @@ +package de.seemoo.at_tracking_detection.ui.dashboard + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication +import de.seemoo.at_tracking_detection.R +import timber.log.Timber +import java.io.BufferedReader +import java.io.InputStreamReader +import java.net.HttpURLConnection +import java.net.URL + +data class Article( + val title: String, + val author: String, + val readingTime: Int, + val previewText: String, + val cardColor: String, + val preview_image: String, // TODO: Rename when in production to PreviewImage, also in JSON + val filename: String +) + +fun parseArticles(jsonString: String): List
{ + val gson = Gson() + val listType = object : TypeToken>() {}.type + val articleMap: Map = gson.fromJson(jsonString, listType) + return articleMap.values.toList() +} + +fun getURL(filename: String): String { + return "https://tpe.seemoo.tu-darmstadt.de/articles/$filename" +} + +fun downloadJson(): String { + val url = ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.article_download_url) + + val articleOfflineTitle = ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.article_offline_header) + val articleOfflineText = ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.article_offline_text) + val iveGotANotification = ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.i_got_a_notification_what_should_i_do) + val searchManually = ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.notification_help) + val iCanNotFindTracker = ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.i_cannot_find_the_tracker) + val findTackerHelp = ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.find_tracker_help) + + val errorReturnValue = """{ + "article0": { + "title": "$articleOfflineTitle", + "author": "Dennis Arndt", + "readingTime": 0, + "previewText": "$articleOfflineText", + "cardColor": "blue_card_background", + "filename": "" + }, + "article1": { + "title": "$iveGotANotification", + "author": "Alexander Heinrich", + "readingTime": 0, + "previewText": "$searchManually", + "cardColor": "gray_card_background", + "filename": "" + }, + "article2": { + "title": "$iCanNotFindTracker", + "author": "Alexander Heinrich", + "readingTime": 0, + "previewText": "$findTackerHelp", + "cardColor": "gray_card_background", + "filename": "" + } + } + """.trimIndent() + + val connection = URL(url).openConnection() as HttpURLConnection + + return try { + connection.requestMethod = "GET" + val responseCode = connection.responseCode + + if (responseCode == HttpURLConnection.HTTP_OK) { + val reader = BufferedReader(InputStreamReader(connection.inputStream)) + val response = StringBuilder() + var inputLine: String? + while (reader.readLine().also { inputLine = it } != null) { + response.append(inputLine) + } + reader.close() + response.toString() + } else { + errorReturnValue + } + } catch (e: Exception) { + Timber.e(e) + errorReturnValue + } finally { + connection.disconnect() + } +} \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ArticleFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ArticleFragment.kt new file mode 100644 index 00000000..7de7f5ae --- /dev/null +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ArticleFragment.kt @@ -0,0 +1,88 @@ +package de.seemoo.at_tracking_detection.ui.dashboard + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import java.net.URL +import com.mukesh.MarkDown +import de.seemoo.at_tracking_detection.R +import timber.log.Timber + + +class ArticleFragment : Fragment() { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the fragment layout + val view = inflater.inflate(R.layout.fragment_article, container, false) + + fun errorHandling() { + Toast.makeText(requireContext(), "No internet connection. Cannot load article.", Toast.LENGTH_SHORT).show() + } + + val titleTextView = view.findViewById(R.id.article_title) + val authorTextView = view.findViewById(R.id.article_author) + val markdownView = view.findViewById(R.id.markdown_view) + val articleReadingTimeView = view.findViewById(R.id.article_reading_time) + + val title = arguments?.getString("title") + val author = arguments?.getString("author") + val readingTime = arguments?.getInt("readingTime") + val filename = arguments?.getString("filename") + + if (filename == null) { + Timber.e("Filename is null") + errorHandling() + return view + } + + val url = getURL(filename) + + titleTextView.text = title + authorTextView.text = author + articleReadingTimeView.text = context?.getString(R.string.article_reading_time, readingTime) + + val modifier = Modifier.fillMaxSize() + + val connectivityManager = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + + if ((networkCapabilities != null) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + try { + markdownView.apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + MaterialTheme { + MarkDown( + url = URL(url), + modifier = modifier + ) + } + } + } + } catch (e: Exception) { + Timber.d(e) + errorHandling() + } + + } else { + errorHandling() + } + + return view + } +} + diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt index 07c8a04a..5a69ddfd 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt @@ -1,18 +1,30 @@ package de.seemoo.at_tracking_detection.ui.dashboard +import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDirections import androidx.navigation.fragment.findNavController +import com.bumptech.glide.Glide import com.google.android.material.card.MaterialCardView import dagger.hilt.android.AndroidEntryPoint import de.seemoo.at_tracking_detection.R import de.seemoo.at_tracking_detection.databinding.FragmentDashboardRiskBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import timber.log.Timber @AndroidEntryPoint @@ -40,10 +52,10 @@ class DashboardRiskFragment : Fragment() { } + @SuppressLint("DiscouragedApi") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val riskCard: MaterialCardView = view.findViewById(R.id.risk_card) riskCard.setOnClickListener { val directions: NavDirections = @@ -51,6 +63,85 @@ class DashboardRiskFragment : Fragment() { findNavController().navigate(directions) } + val articlesContainer = view.findViewById(R.id.articles_container) + val progressBar = view.findViewById(R.id.loading_progress_bar) + + lifecycleScope.launch(Dispatchers.IO) { + progressBar.visibility = View.VISIBLE + + val articlesJSON = downloadJson() + Timber.d("Articles JSON: %s", articlesJSON) + + withContext(Dispatchers.Main) { + val articles = parseArticles(articlesJSON) + Timber.d("Number of Articles: %s", articles.size) + + // Create a new LinearLayout to hold the ArticleCards + val articleCardsLinearLayout = LinearLayout(context) + articleCardsLinearLayout.orientation = LinearLayout.VERTICAL + articleCardsLinearLayout.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + + for (article in articles) { + val articleCard = MaterialCardView(context) + + val layout = LayoutInflater.from(context).inflate(R.layout.include_article_card, null) + val textViewTitle = layout.findViewById(R.id.card_title) + val textViewPreviewText = layout.findViewById(R.id.card_text_preview) + val imageViewPreview = layout.findViewById(R.id.preview_image) + val materialCard = layout.findViewById(R.id.material_card) + + textViewTitle.text = article.title + if (article.previewText.isNotEmpty()){ + textViewPreviewText.text = article.previewText + } else { + textViewPreviewText.visibility = View.GONE + } + + val colorResourceId = resources.getIdentifier(article.cardColor, "color", context?.packageName) + materialCard.setBackgroundColor(colorResourceId) + + articleCard.addView(layout) + Timber.tag("CardAdded").d("Article card added: %s", article.title) + + articleCard.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT, + ).apply { + topMargin = 22 + } + + if (!article.preview_image.isNullOrEmpty()) { // TODO: Rename when in production to PreviewImage, also in JSON + val imageURL = getURL(article.preview_image) // TODO: Rename when in production to PreviewImage, also in JSON + context?.let { + Glide.with(it) + .load(imageURL) + .fitCenter() + .into(imageViewPreview) + } + } else { + imageViewPreview.visibility = View.GONE + } + + if (!article.filename.isNullOrEmpty()) { + articleCard.setOnClickListener { + val directions: NavDirections = + DashboardRiskFragmentDirections.actionNavigationDashboardToArticleFragment( + author = article.author, + title = article.title, + filename = article.filename, + readingTime = article.readingTime + ) + findNavController().navigate(directions) + } + } + + articleCardsLinearLayout.addView(articleCard) + } + + articlesContainer.addView(articleCardsLinearLayout) + progressBar.visibility = View.GONE + } + } } override fun onStart() { diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardViewModel.kt index e19c44be..3d35eac0 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardViewModel.kt @@ -20,10 +20,10 @@ import javax.inject.Inject @HiltViewModel class DashboardViewModel @Inject constructor( val beaconRepository: BeaconRepository, - notificationRepository: NotificationRepository, + val notificationRepository: NotificationRepository, val deviceRepository: DeviceRepository, private val sharedPreferences: SharedPreferences, - backgroundWorkScheduler: BackgroundWorkScheduler + val backgroundWorkScheduler: BackgroundWorkScheduler ) : ViewModel() { private var lastScan: LocalDateTime diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DeviceMapFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DeviceMapFragment.kt index 1463b427..0cf77a13 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DeviceMapFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DeviceMapFragment.kt @@ -17,6 +17,7 @@ import de.seemoo.at_tracking_detection.R import de.seemoo.at_tracking_detection.database.models.Location import de.seemoo.at_tracking_detection.databinding.FragmentDeviceMapBinding import de.seemoo.at_tracking_detection.util.Utility +import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator import kotlinx.coroutines.launch import org.osmdroid.views.MapView @@ -57,49 +58,20 @@ class DeviceMapFragment : Fragment() { viewModel.isMapLoading.postValue(true) Utility.enableMyLocationOverlay(map) - val deviceAddress = this.deviceAddress - if (!deviceAddress.isNullOrEmpty()) { - viewModel.markerLocations.observe(viewLifecycleOwner) { - lifecycleScope.launch { - val locationList = arrayListOf() - val locationRepository = ATTrackingDetectionApplication.getCurrentApp()?.locationRepository ?: return@launch - - it.filter { it.locationId != null && it.locationId != 0 } - .map { - val location = locationRepository.getLocationWithId(it.locationId!!) - if (location != null) { - locationList.add(location) - } - } - - Utility.setGeoPointsFromListOfLocations(locationList.toList(), map, true) - }.invokeOnCompletion { - viewModel.isMapLoading.postValue(false) - } + lifecycleScope.launch { + val locationRepository = ATTrackingDetectionApplication.getCurrentApp().locationRepository + val relevantTrackingDate = RiskLevelEvaluator.relevantTrackingDateForRiskCalculation + val locationList: List = if (!deviceAddress.isNullOrEmpty()) { + locationRepository.getLocationsForBeaconSince(deviceAddress!!, relevantTrackingDate) + } else { + locationRepository.locationsSince(relevantTrackingDate) } - } else { - viewModel.allLocations.observe(viewLifecycleOwner) { - lifecycleScope.launch { - val locationList = arrayListOf() - val locationRepository = - ATTrackingDetectionApplication.getCurrentApp()?.locationRepository ?: return@launch - - it.filter { it.locationId != null && it.locationId != 0 } - .map { - val location = locationRepository.getLocationWithId(it.locationId!!) - if (location != null) { - locationList.add(location) - } - } - Utility.setGeoPointsFromListOfLocations(locationList.toList(), map) - }.invokeOnCompletion { - viewModel.isMapLoading.postValue(false) - } + try { + Utility.setGeoPointsFromListOfLocations(locationList, map) + } finally { + viewModel.isMapLoading.postValue(false) } } - - } - } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DeviceMapViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DeviceMapViewModel.kt index 1b5ef4d0..6f43c99e 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DeviceMapViewModel.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DeviceMapViewModel.kt @@ -17,7 +17,7 @@ class DeviceMapViewModel @Inject constructor( beaconRepository.getDeviceBeacons(it) } - val allLocations: LiveData> = beaconRepository.getBeaconsSince(RiskLevelEvaluator.relevantTrackingDate).asLiveData() + val allLocations: LiveData> = beaconRepository.getBeaconsSince(RiskLevelEvaluator.relevantTrackingDateForRiskCalculation).asLiveData() val isMapLoading = MutableLiveData(false) diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RallyLineGraphChart.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RallyLineGraphChart.kt index 3239c6cf..7b9a33ec 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RallyLineGraphChart.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RallyLineGraphChart.kt @@ -122,12 +122,12 @@ class RallyLineGraphChart : View { drawVerticalBars(viewCanvas) } - override fun onDraw(canvas: Canvas?) { + override fun onDraw(canvas: Canvas) { super.onDraw(canvas) drawBezierCurve(canvas) bitmap?.let { - canvas?.drawBitmap(it, 0f, 0f, bitmapPaint) + canvas.drawBitmap(it, 0f, 0f, bitmapPaint) } } @@ -177,7 +177,7 @@ class RallyLineGraphChart : View { canvas?.drawPath(borderPath, borderPathPaint) - } catch (e: Exception) { + } catch (_: Exception) { } } @@ -202,7 +202,7 @@ class RallyLineGraphChart : View { conPoint1.add(PointF((points[i].x + points[i - 1].x) / 2, points[i - 1].y)) conPoint2.add(PointF((points[i].x + points[i - 1].x) / 2, points[i].y)) } - } catch (e: Exception) { + } catch (_: Exception) { } } diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskCardViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskCardViewModel.kt index 02eda66e..14514247 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskCardViewModel.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskCardViewModel.kt @@ -1,6 +1,8 @@ package de.seemoo.at_tracking_detection.ui.dashboard +import android.content.Context import android.content.SharedPreferences +import android.content.pm.PackageManager import androidx.core.content.ContextCompat import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -12,9 +14,10 @@ import de.seemoo.at_tracking_detection.util.risk.RiskLevel import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator import java.text.DateFormat import java.time.LocalDateTime -import java.time.ZoneOffset +import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.format.FormatStyle +import java.util.Date import javax.inject.Inject @HiltViewModel @@ -30,7 +33,7 @@ class RiskCardViewModel @Inject constructor( var trackersFoundModel: MutableLiveData = MutableLiveData() var lastUpdateModel: MutableLiveData = MutableLiveData() var lastDiscoveryModel: MutableLiveData = MutableLiveData() - var dismissSurveyInformation: MutableLiveData = MutableLiveData(SharedPrefs.dismissSurveyInformation) + private var dismissSurveyInformation: MutableLiveData = MutableLiveData(SharedPrefs.dismissSurveyInformation) private var lastScan: LocalDateTime? = null private var sharedPreferencesListener: SharedPreferences.OnSharedPreferenceChangeListener = @@ -54,7 +57,7 @@ class RiskCardViewModel @Inject constructor( updateRiskLevel() } - fun updateLastUpdateModel() { + private fun updateLastUpdateModel() { val context = ATTrackingDetectionApplication.getAppContext() val lastScanString = if (lastScan != null) { @@ -75,6 +78,8 @@ class RiskCardViewModel @Inject constructor( val dateFormat = DateFormat.getDateTimeInstance() val lastDiscoveryDate = riskLevelEvaluator.getLastTrackerDiscoveryDate() val lastDiscoveryDateString = dateFormat.format(lastDiscoveryDate) + val earliestTrackingDate = getEarliestTrackingDate() + val earliestTrackingDateString = dateFormat.format(earliestTrackingDate) val totalAlerts = riskLevelEvaluator.getNumberRelevantTrackers() updateLastUpdateModel() @@ -87,7 +92,7 @@ class RiskCardViewModel @Inject constructor( riskColor = ContextCompat.getColor(context, R.color.risk_low) trackersFoundModel.postValue(RiskRowViewModel( - context.getString(R.string.no_trackers_found, RiskLevelEvaluator.RELEVANT_DAYS), + context.getString(R.string.no_trackers_found, earliestTrackingDateString), ContextCompat.getDrawable(context, R.drawable.ic_baseline_location_on_24)!! )) lastDiscoveryModel.postValue(RiskRowViewModel( @@ -106,7 +111,7 @@ class RiskCardViewModel @Inject constructor( context.getString( R.string.found_x_trackers, totalAlerts, - RiskLevelEvaluator.RELEVANT_DAYS + RiskLevelEvaluator.RELEVANT_DAYS_RISK_LEVEL ), ContextCompat.getDrawable(context, R.drawable.ic_baseline_location_on_24)!! )) @@ -128,7 +133,7 @@ class RiskCardViewModel @Inject constructor( context.getString( R.string.found_x_trackers, totalAlerts, - RiskLevelEvaluator.RELEVANT_DAYS + RiskLevelEvaluator.RELEVANT_DAYS_RISK_LEVEL ), ContextCompat.getDrawable(context, R.drawable.ic_baseline_location_on_24)!! )) @@ -140,4 +145,31 @@ class RiskCardViewModel @Inject constructor( } } } + + private fun getEarliestTrackingDate(): Date { + val context = ATTrackingDetectionApplication.getAppContext() + val installDate = getInstallDate(context) + val oldestDatePossible = RiskLevelEvaluator.relevantTrackingDateForRiskCalculation + val oldestDatePossibleAsDate = Date.from(oldestDatePossible.atZone(ZoneId.systemDefault()).toInstant()) + + return if (installDate != null && installDate > oldestDatePossibleAsDate) { + installDate + } else { + oldestDatePossibleAsDate + } + } + + private fun getInstallDate(context: Context): Date? { + try { + val packageManager = context.packageManager + val packageInfo = packageManager.getPackageInfo(context.packageName, 0) + val installTimeMillis = packageInfo.firstInstallTime + + // Convert milliseconds to Date + return Date(installTimeMillis) + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + } + return null + } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskDetailFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskDetailFragment.kt index 7d2cfef2..616097ea 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskDetailFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskDetailFragment.kt @@ -13,6 +13,7 @@ import com.google.android.material.card.MaterialCardView import dagger.hilt.android.AndroidEntryPoint import de.seemoo.at_tracking_detection.R import de.seemoo.at_tracking_detection.databinding.FragmentRiskDetailBinding +import de.seemoo.at_tracking_detection.util.SharedPrefs import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator import javax.inject.Inject @@ -77,6 +78,12 @@ class RiskDetailFragment : Fragment() { findNavController().navigate(directions) } + if (!SharedPrefs.advancedMode) { + view.findViewById(R.id.card_devices_found).visibility = View.GONE + } else { + view.findViewById(R.id.card_devices_found).visibility = View.VISIBLE + } + // view.findViewById(R.id.card_beacons_found).setOnClickListener { // val directions = // RiskDetailFragmentDirections.actionRiskDetailFragmentToDeviceMapFragment() diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskDetailViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskDetailViewModel.kt index 3697037f..f6c8fc91 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskDetailViewModel.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskDetailViewModel.kt @@ -10,14 +10,11 @@ import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication import de.seemoo.at_tracking_detection.R import de.seemoo.at_tracking_detection.database.repository.BeaconRepository import de.seemoo.at_tracking_detection.database.repository.DeviceRepository -import de.seemoo.at_tracking_detection.database.models.Beacon -import de.seemoo.at_tracking_detection.database.models.Location as LocationModel import de.seemoo.at_tracking_detection.database.models.device.BaseDevice import de.seemoo.at_tracking_detection.database.repository.LocationRepository import de.seemoo.at_tracking_detection.database.repository.ScanRepository import de.seemoo.at_tracking_detection.util.risk.RiskLevel import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator -import kotlinx.coroutines.flow.Flow import timber.log.Timber import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -29,19 +26,19 @@ class RiskDetailViewModel @Inject constructor( deviceRepository: DeviceRepository, scanRepository: ScanRepository, val beaconRepository: BeaconRepository, - val locationRepository: LocationRepository, + private val locationRepository: LocationRepository, ) : ViewModel() { - private val relevantDate = RiskLevelEvaluator.relevantTrackingDate + private val relevantDate = RiskLevelEvaluator.relevantTrackingDateForRiskCalculation private val trackersFound: List = deviceRepository.trackingDevicesNotIgnoredSince(relevantDate) private val lastSeenDates = trackersFound.map { DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).format(it.lastSeen) } var riskColor: Int - val numberOfTrackersFound = deviceRepository.trackingDevicesNotIgnoredSinceCount(RiskLevelEvaluator.relevantTrackingDate).asLiveData() + val numberOfTrackersFound = deviceRepository.trackingDevicesNotIgnoredSinceCount(RiskLevelEvaluator.relevantTrackingDateForRiskCalculation).asLiveData() - val totalLocationsTrackedCount= locationRepository.locationsSinceCount(relevantDate).asLiveData() + val totalLocationsTrackedCount = locationRepository.locationsSinceCount(relevantDate).asLiveData() // val discoveredBeacons: List = beaconRepository.getBeaconsForDevices(trackersFound) @@ -59,10 +56,6 @@ class RiskDetailViewModel @Inject constructor( scanDates.joinToString(separator = "\n") } - fun allBeacons(): Flow> { - return beaconRepository.getBeaconsSince(relevantDate) - } - init { val context = ATTrackingDetectionApplication.getAppContext() riskColor = when (riskLevelEvaluator.evaluateRiskLevel()) { diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskRowViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskRowViewModel.kt index 47c59c9f..95c0e5ae 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskRowViewModel.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/RiskRowViewModel.kt @@ -1,12 +1,9 @@ package de.seemoo.at_tracking_detection.ui.dashboard import android.graphics.drawable.Drawable -import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -class RiskRowViewModel constructor( +class RiskRowViewModel( val text: String, val image: Drawable -) {} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugLogViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugLogViewModel.kt index 8f03d92e..2f96b352 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugLogViewModel.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugLogViewModel.kt @@ -4,7 +4,6 @@ import android.text.Editable import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication import fr.bipi.tressence.file.FileLoggerTree import timber.log.Timber import java.io.File @@ -12,11 +11,11 @@ import javax.inject.Inject @HiltViewModel class DebugLogViewModel @Inject constructor(): ViewModel() { - var fullLogText: List + private var fullLogText: List var logText: MutableLiveData = MutableLiveData() var filterText: MutableLiveData = MutableLiveData() - val logFile: File + private val logFile: File init { val trees = Timber.forest() diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugScanViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugScanViewModel.kt index 311f3a72..8ed8f545 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugScanViewModel.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugScanViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.asLiveData import dagger.hilt.android.lifecycle.HiltViewModel import de.seemoo.at_tracking_detection.database.models.Scan import de.seemoo.at_tracking_detection.database.repository.ScanRepository -import java.time.LocalDateTime import javax.inject.Inject @HiltViewModel @@ -14,7 +13,7 @@ class DebugScanViewModel @Inject constructor( scanRepository: ScanRepository ): ViewModel() { - val scansLive: LiveData> + private val scansLive: LiveData> val scans: List init { diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugScansFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugScansFragment.kt index a195d52b..83a1afa8 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugScansFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugScansFragment.kt @@ -1,11 +1,14 @@ package de.seemoo.at_tracking_detection.ui.debug -import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Divider @@ -20,24 +23,13 @@ import androidx.compose.ui.unit.Dp import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.asFlow -import androidx.lifecycle.asLiveData import com.google.android.material.composethemeadapter.MdcTheme import dagger.hilt.android.AndroidEntryPoint -import dagger.hilt.android.lifecycle.HiltViewModel import de.seemoo.at_tracking_detection.R import de.seemoo.at_tracking_detection.database.models.Scan -import de.seemoo.at_tracking_detection.database.repository.BeaconRepository -import de.seemoo.at_tracking_detection.database.repository.DeviceRepository -import de.seemoo.at_tracking_detection.database.repository.ScanRepository import de.seemoo.at_tracking_detection.databinding.FragmentDebugScansBinding -import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator -import java.text.DateFormat import java.time.format.DateTimeFormatter import java.time.format.FormatStyle -import javax.inject.Inject @AndroidEntryPoint class DebugScansFragment: Fragment() { diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugViewModel.kt index d25ad3de..4f7b1635 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugViewModel.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/debug/DebugViewModel.kt @@ -2,11 +2,10 @@ package de.seemoo.at_tracking_detection.ui.debug import android.content.SharedPreferences import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import de.seemoo.at_tracking_detection.database.repository.DeviceRepository import de.seemoo.at_tracking_detection.util.SharedPrefs +import java.time.LocalDateTime import javax.inject.Inject @HiltViewModel @@ -26,17 +25,21 @@ class DebugViewModel @Inject constructor( var scanText = MutableLiveData("Not scanning") + var nextScanDate = MutableLiveData(SharedPrefs.nextScanDate.toString()) + var lastScanDate = MutableLiveData(SharedPrefs.lastScanDate.toString()) init { sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferencesListener) updateScanText() } - fun updateScanText() { + private fun updateScanText() { if (SharedPrefs.isScanningInBackground) { scanText.postValue("Scanning in background") }else { scanText.postValue("Not scanning") } + nextScanDate.postValue(SharedPrefs.nextScanDate.toString()) + lastScanDate.postValue(SharedPrefs.lastScanDate.toString()) } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/AllDevicesFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/AllDevicesFragment.kt index d7d2bd3d..30e4b931 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/AllDevicesFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/AllDevicesFragment.kt @@ -28,10 +28,6 @@ class AllDevicesFragment : Fragment() { private val viewModel: AllDevicesViewModel by viewModels() - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/AllDevicesViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/AllDevicesViewModel.kt index 49135171..09efdb49 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/AllDevicesViewModel.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/AllDevicesViewModel.kt @@ -20,7 +20,7 @@ class AllDevicesViewModel @Inject constructor( val countNotTracking = deviceRepository.countNotTracking.asLiveData() val countIgnored = deviceRepository.countIgnored.asLiveData() - val countTracking = deviceRepository.trackingDevicesNotIgnoredSinceCount(RiskLevelEvaluator.relevantTrackingDate).asLiveData() + val countTracking = deviceRepository.trackingDevicesNotIgnoredSinceCount(RiskLevelEvaluator.relevantTrackingDateForRiskCalculation).asLiveData() val countAirTags = deviceRepository.countForDeviceType(DeviceType.AIRTAG).asLiveData() val countFindMy = deviceRepository.countForDeviceType(DeviceType.FIND_MY).asLiveData() diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/DeviceAdapter.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/DeviceAdapter.kt index 03c544cb..da901e55 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/DeviceAdapter.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/DeviceAdapter.kt @@ -10,7 +10,7 @@ import de.seemoo.at_tracking_detection.R import de.seemoo.at_tracking_detection.database.models.device.BaseDevice import de.seemoo.at_tracking_detection.databinding.ItemDeviceBinding -class DeviceAdapter constructor( +class DeviceAdapter( private val devicesViewModel: DevicesViewModel, private val onClickListener: OnClickListener ) : diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/DevicesFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/DevicesFragment.kt index 7074b57d..19d947be 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/DevicesFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/DevicesFragment.kt @@ -4,12 +4,14 @@ import android.graphics.Canvas import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.os.Bundle +import android.text.InputFilter import android.transition.TransitionInflater import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.EditText +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.view.doOnPreDraw @@ -32,6 +34,7 @@ import de.seemoo.at_tracking_detection.ui.devices.filter.models.DeviceTypeFilter import de.seemoo.at_tracking_detection.ui.devices.filter.models.IgnoredFilter import de.seemoo.at_tracking_detection.ui.devices.filter.models.NotifiedFilter import de.seemoo.at_tracking_detection.ui.devices.filter.models.DateRangeFilter +import de.seemoo.at_tracking_detection.ui.tracking.TrackingFragment import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator import timber.log.Timber import java.time.LocalDate @@ -75,7 +78,7 @@ abstract class DevicesFragment( ) ) } else { - val relevantTrackingStartDate = RiskLevelEvaluator.relevantTrackingDate.toLocalDate() + val relevantTrackingStartDate = RiskLevelEvaluator.relevantTrackingDateForRiskCalculation.toLocalDate() devicesViewModel.addOrRemoveFilter( DateRangeFilter.build( relevantTrackingStartDate, @@ -271,16 +274,25 @@ abstract class DevicesFragment( val device = deviceAdapter.currentList[viewHolder.bindingAdapterPosition] if (direction == ItemTouchHelper.LEFT) { - val editName = EditText(context) + val editName = EditText(context).apply { + maxLines = 1 + filters = arrayOf(InputFilter.LengthFilter(TrackingFragment.MAX_CHARACTER_LIMIT)) + setText(device.getDeviceNameWithID()) + } editName.setText(device.getDeviceNameWithID()) MaterialAlertDialogBuilder(requireContext()) .setIcon(R.drawable.ic_baseline_edit_24) .setTitle(getString(R.string.devices_edit_title)).setView(editName) .setNegativeButton(getString(R.string.cancel_button), null) .setPositiveButton(R.string.ok_button) { _, _ -> - device.name = editName.text.toString() - devicesViewModel.update(device) - Timber.d("Renamed device to ${device.name}") + val newName = editName.text.toString() + if (newName.isNotEmpty()) { + device.name = newName + devicesViewModel.update(device) + Timber.d("Renamed device to ${device.name}") + } else { + Toast.makeText(context, R.string.device_name_cannot_be_empty, Toast.LENGTH_SHORT).show() + } } .setOnDismissListener { deviceAdapter.notifyItemChanged(viewHolder.bindingAdapterPosition) diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/filter/models/DeviceTypeFilter.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/filter/models/DeviceTypeFilter.kt index 16fae1d2..bce9c7be 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/filter/models/DeviceTypeFilter.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/filter/models/DeviceTypeFilter.kt @@ -1,7 +1,6 @@ package de.seemoo.at_tracking_detection.ui.devices.filter.models import androidx.collection.ArraySet -import androidx.collection.arraySetOf import de.seemoo.at_tracking_detection.database.models.device.BaseDevice import de.seemoo.at_tracking_detection.database.models.device.DeviceType @@ -18,10 +17,9 @@ class DeviceTypeFilter(deviceTypes: Set) : Filter() { fun remove(deviceType: DeviceType) = deviceTypes.remove(deviceType) - var deviceTypes: ArraySet + var deviceTypes: ArraySet = ArraySet() init { - this.deviceTypes = ArraySet() this.deviceTypes.addAll(deviceTypes) } diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/feedback/FeedbackFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/feedback/FeedbackFragment.kt index 01e78ee1..3159da36 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/feedback/FeedbackFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/feedback/FeedbackFragment.kt @@ -4,12 +4,15 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.navArgs -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup +import com.google.android.material.card.MaterialCardView import dagger.hilt.android.AndroidEntryPoint import de.seemoo.at_tracking_detection.R import de.seemoo.at_tracking_detection.databinding.FragmentFeedbackBinding @@ -21,6 +24,9 @@ class FeedbackFragment : Fragment() { private val safeArgs: FeedbackFragmentArgs by navArgs() + // This gives the option to highlight the selected feedback location in future updates + private var selectedLocationCard: MaterialCardView? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -35,30 +41,73 @@ class FeedbackFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val locationChipGroup = view.findViewById(R.id.feedback_location_chip_group) - val locations = arrayOf( - R.string.feedback_location_backpack, R.string.feedback_location_clothes, - R.string.feedback_location_car, R.string.feedback_location_bike + val locations = listOf( + LocationItem(getString(R.string.feedback_location_backpack), "Bag", R.drawable.ic_baseline_backpack_24), + LocationItem(getString(R.string.feedback_location_clothes), "Clothes", R.drawable.ic_baseline_person_24), + LocationItem(getString(R.string.feedback_location_car), "Car", R.drawable.ic_baseline_car_24), + LocationItem(getString(R.string.feedback_location_bike), "Bike", R.drawable.ic_baseline_bike_scooter_24), + LocationItem(getString(R.string.feedback_location_other), "Other", R.drawable.ic_baseline_more_horiz_24), + LocationItem(getString(R.string.feedback_location_not_found), "NotFound", R.drawable.ic_baseline_cancel_24) ) - for (location in locations) { - val chip = - layoutInflater.inflate( - R.layout.include_choice_chip, - locationChipGroup, - false - ) as Chip - chip.setText(location) - feedbackViewModel.location.observe(viewLifecycleOwner) { - if (it == getString(location)) { - chip.isChecked = true - } + + val locationsLinearLayout = LinearLayout(context) + locationsLinearLayout.orientation = LinearLayout.VERTICAL + locationsLinearLayout.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + topMargin = 24 + } + + val locationLayout = view.findViewById(R.id.feedback_location_layout) + + for (locationItem in locations) { + val locationCard = MaterialCardView(context) + + val layout = LayoutInflater.from(context).inflate(R.layout.item_feedback_selection, null) + val text = layout.findViewById(R.id.text) + val icon = layout.findViewById(R.id.icon) + + icon.setImageResource(locationItem.imageResId) + text.text = locationItem.visibleString + + locationCard.addView(layout) + + locationCard.setOnClickListener { + // Clear previously selected location + selectedLocationCard?.isChecked = false + + // Mark the current location as selected + locationCard.isChecked = true + selectedLocationCard = locationCard + + // Update ViewModel with selected location + feedbackViewModel.location.postValue(locationItem.backendString) + + // Show a Toast message indicating success + Toast.makeText(requireContext(), R.string.feedback_success, Toast.LENGTH_SHORT).show() + } + + // Set layout params for location card + locationCard.layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ).apply { + topMargin = 16 } - chip.setOnClickListener { - feedbackViewModel.location.postValue(getString(location)) + + locationsLinearLayout.addView(locationCard) + + // Check if this location is already selected + if (locationItem.backendString == feedbackViewModel.location.value) { + // Mark the current location as selected + locationCard.isChecked = true + selectedLocationCard = locationCard } - locationChipGroup.addView(chip) } + + locationLayout.addView(locationsLinearLayout) } override fun onPause() { diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/feedback/LocationItem.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/feedback/LocationItem.kt new file mode 100644 index 00000000..d62465ef --- /dev/null +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/feedback/LocationItem.kt @@ -0,0 +1,7 @@ +package de.seemoo.at_tracking_detection.ui.feedback + +data class LocationItem( + val visibleString: String, + val backendString: String, + val imageResId: Int +) diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/onboarding/IgnoreBatteryOptimizationFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/onboarding/IgnoreBatteryOptimizationFragment.kt index c2fc7364..fa7acb52 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/onboarding/IgnoreBatteryOptimizationFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/onboarding/IgnoreBatteryOptimizationFragment.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Build import android.os.Bundle import android.os.PowerManager import android.provider.Settings @@ -12,7 +11,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button -import androidx.annotation.RequiresApi import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import dagger.hilt.android.AndroidEntryPoint @@ -41,16 +39,13 @@ class IgnoreBatteryOptimizationFragment : Fragment() { val ignoreBatteryOptimizationButton = view.findViewById