diff --git a/app/build.gradle b/app/build.gradle index 5dc9741b..d9bf3605 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,6 +5,7 @@ plugins { id 'kotlin-kapt' id 'dagger.hilt.android.plugin' id 'com.mikepenz.aboutlibraries.plugin' + id 'org.jetbrains.kotlin.android' } def apiPropertiesFile = rootProject.file("api.properties") @@ -12,8 +13,6 @@ def apiProperties = new Properties() apiProperties.load(new FileInputStream(apiPropertiesFile)) android { - compileSdkVersion 33 - buildFeatures { viewBinding true dataBinding true @@ -22,10 +21,11 @@ android { defaultConfig { applicationId "de.seemoo.at_tracking_detection" - minSdkVersion 21 - targetSdkVersion 31 - versionCode 37 - versionName "2.0" + minSdkVersion 28 + targetSdkVersion 33 + compileSdk 33 + versionCode 38 + versionName "2.1" buildConfigField "String", "API_KEY", apiProperties["API_KEY"] buildConfigField "String", "API_BASE_ADDRESS", apiProperties["API_BASE_ADDRESS"] @@ -56,15 +56,15 @@ android { } compileOptions { coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } composeOptions { - kotlinCompilerExtensionVersion "1.2.0-beta02" + kotlinCompilerExtensionVersion "1.4.8" } @@ -91,26 +91,27 @@ 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.0' - implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.work:work-runtime-ktx:2.8.1' + implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.8.0' + implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.vectordrawable:vectordrawable:1.1.0' - implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' - implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' - implementation 'androidx.preference:preference-ktx:1.2.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.preference:preference-ktx:1.2.1' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'androidx.recyclerview:recyclerview:1.3.1' 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.google.code.gson:gson:2.9.0' + 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.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 "com.google.dagger:hilt-android:$hilt_version" @@ -119,11 +120,13 @@ dependencies { implementation 'com.github.AppIntro:AppIntro:6.1.0' - implementation 'org.osmdroid:osmdroid-android:6.1.11' + implementation 'org.osmdroid:osmdroid-android:6.1.16' implementation 'com.github.ybq:Android-SpinKit:1.4.0' - implementation 'com.mikepenz:aboutlibraries:8.9.3' + implementation "com.mikepenz:aboutlibraries:$about_libraries_version" + + implementation 'io.noties.markwon:core:4.6.2' kapt "com.google.dagger:hilt-compiler:$hilt_version" @@ -135,32 +138,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.0" - androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' + androidTestImplementation 'androidx.room:room-testing:2.5.2' + androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' 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.2' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' //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.6.1' + implementation 'androidx.activity:activity-compose:1.7.2' // Compose Material Design - implementation 'androidx.compose.material:material:1.3.1' + implementation 'androidx.compose.material:material:1.4.3' // Animations - implementation 'androidx.compose.animation:animation:1.3.3' + implementation 'androidx.compose.animation:animation:1.4.3' // Tooling support (Previews, etc.) - implementation 'androidx.compose.ui:ui-tooling:1.3.3' + implementation 'androidx.compose.ui:ui-tooling:1.4.3' // Integration with ViewModels - implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1' // UI Tests - androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.3.3' + androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.4.3' // When using a MDC theme implementation "com.google.android.material:compose-theme-adapter:1.2.1" diff --git a/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/11.json b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/11.json new file mode 100644 index 00000000..fea38b55 --- /dev/null +++ b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/11.json @@ -0,0 +1,385 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "32dda256caf1b416e343c92640e01c9a", + "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)", + "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 + } + ], + "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)", + "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 + } + ], + "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)", + "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 + } + ], + "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, '32dda256caf1b416e343c92640e01c9a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/12.json b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/12.json new file mode 100644 index 00000000..11791379 --- /dev/null +++ b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/12.json @@ -0,0 +1,397 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "9fbd2ce7b83a6c2d60a8285880ec3f56", + "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)", + "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 + } + ], + "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)", + "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 + } + ], + "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, '9fbd2ce7b83a6c2d60a8285880ec3f56')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c15a2f63..75f23306 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ + @@ -34,10 +35,11 @@ android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" - android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.ATTrackingDetection" - android:largeHeap="true"> + android:localeConfig="@xml/locales_config" + android:largeHeap="true" + tools:targetApi="tiramisu"> + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index f3b960c7..b498f75f 100644 Binary files a/app/src/main/ic_launcher-playstore.png and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/ic_launcher_new-playstore.png b/app/src/main/ic_launcher_new-playstore.png new file mode 100644 index 00000000..6337e57f Binary files /dev/null and b/app/src/main/ic_launcher_new-playstore.png differ diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ATTrackingDetectionApplication.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ATTrackingDetectionApplication.kt index f6522277..630c765b 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ATTrackingDetectionApplication.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ATTrackingDetectionApplication.kt @@ -7,7 +7,6 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager -import android.location.Location import android.os.Build import android.util.Log import androidx.core.content.ContextCompat @@ -20,9 +19,7 @@ import de.seemoo.at_tracking_detection.database.repository.DeviceRepository import de.seemoo.at_tracking_detection.database.repository.LocationRepository import de.seemoo.at_tracking_detection.database.repository.NotificationRepository import de.seemoo.at_tracking_detection.detection.LocationProvider -import de.seemoo.at_tracking_detection.detection.LocationRequester import de.seemoo.at_tracking_detection.notifications.NotificationService -import de.seemoo.at_tracking_detection.statistics.api.Api import de.seemoo.at_tracking_detection.ui.OnboardingActivity import de.seemoo.at_tracking_detection.util.ATTDLifecycleCallbacks import de.seemoo.at_tracking_detection.util.SharedPrefs @@ -32,7 +29,6 @@ import fr.bipi.tressence.file.FileLoggerTree import timber.log.Timber import java.io.File import java.time.LocalDateTime -import java.util.* import javax.inject.Inject @@ -121,23 +117,23 @@ class ATTrackingDetectionApplication : Application(), Configuration.Provider { BackgroundWorkScheduler.scheduleAlarmWakeupIfScansFail() if (BuildConfig.DEBUG) { - // Get a location for testing - Timber.d("Request location") - val startTime = Date() - val locationRequester: LocationRequester = object : LocationRequester() { - override fun receivedAccurateLocationUpdate(location: Location) { - val endTime = Date() - val duration = (endTime.time - startTime.time) / 1000 - Timber.d("Got location $location after $duration s") - } - } - val location = locationProvider.lastKnownOrRequestLocationUpdates(locationRequester, 20_000L) - if (location != null) { - Timber.d("Using last known location") - } - - // Printing time zone and user agent - Timber.d("Timezone: ${Api.TIME_ZONE} useragent ${Api.USER_AGENT}") +// // Get a location for testing +// Timber.d("Request location") +// val startTime = Date() +// val locationRequester: LocationRequester = object : LocationRequester() { +// override fun receivedAccurateLocationUpdate(location: Location) { +// val endTime = Date() +// val duration = (endTime.time - startTime.time) / 1000 +// Timber.d("Got location $location after $duration s") +// } +// } +// val location = locationProvider.lastKnownOrRequestLocationUpdates(locationRequester, 20_000L) +// if (location != null) { +// Timber.d("Using last known location") +// } +// +// // Printing time zone and user agent +// Timber.d("Timezone: ${Api.TIME_ZONE} useragent ${Api.USER_AGENT}") } } @@ -150,6 +146,9 @@ class ATTrackingDetectionApplication : Application(), Configuration.Provider { requiredPermissions.add(Manifest.permission.BLUETOOTH_SCAN) requiredPermissions.add(Manifest.permission.BLUETOOTH_CONNECT) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requiredPermissions.add(Manifest.permission.POST_NOTIFICATIONS) + } for (permission in requiredPermissions) { val granted = ContextCompat.checkSelfPermission( @@ -184,6 +183,6 @@ class ATTrackingDetectionApplication : Application(), Configuration.Provider { } //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 = true + 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 27de0332..53471d6e 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 = 10, + version = 12, entities = [ BaseDevice::class, Notification::class, @@ -24,6 +24,8 @@ import de.seemoo.at_tracking_detection.util.converter.DateTimeConverter AutoMigration(from=5, to=6), AutoMigration(from=7, to=8), AutoMigration(from=8, to=9, spec = AppDatabase.RenameScanMigrationSpec::class), + AutoMigration(from=10, to=11), + AutoMigration(from=11, to=12), ], exportSchema = true ) 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 7d450606..a65e3d47 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 @@ -71,6 +71,15 @@ interface DeviceDao { @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 accuracy is not NULL AND accuracy <= :maxAccuracy AND device.lastSeen >= :since") fun getNumberOfLocationsForWithAccuracyLimitDevice(deviceAddress: String, maxAccuracy: Float, since: LocalDateTime): Int + @Query("SELECT riskLevel FROM device WHERE address = :deviceAddress") + fun getCachedRiskLevel(deviceAddress: String): Int + + @Query("SELECT lastCalculatedRiskDate FROM device WHERE address = :deviceAddress") + fun getLastCachedRiskLevelDate(deviceAddress: String): LocalDateTime? + + @Query("UPDATE device SET riskLevel = :riskLevel, lastCalculatedRiskDate = :lastCalculatedRiskDate WHERE address == :deviceAddress") + fun updateRiskLevelCache(deviceAddress: String, riskLevel: Int, lastCalculatedRiskDate: LocalDateTime) + @Transaction @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM device JOIN beacon ON beacon.deviceAddress = deviceAddress WHERE beacon.receivedAt >= :dateTime") 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 097bdeff..50750aae 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 @@ -64,4 +64,7 @@ interface NotificationDao { @Update suspend fun update(notification: Notification) + + @Query("SELECT COUNT(*) FROM notification WHERE deviceAddress == :deviceAddress AND falseAlarm = 0 LIMIT 1") + fun existsNotificationForDevice(deviceAddress: String): Boolean } \ 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 2ca1eec0..26e8c894 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 @@ -3,8 +3,9 @@ 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 import de.seemoo.at_tracking_detection.database.models.device.types.* -import de.seemoo.at_tracking_detection.database.models.device.types.SamsungDevice.Companion.getPublicKey import de.seemoo.at_tracking_detection.util.converter.DateTimeConverter import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -26,7 +27,11 @@ data class BaseDevice( @ColumnInfo(name = "lastSeen") var lastSeen: LocalDateTime, @ColumnInfo(name = "notificationSent") var notificationSent: Boolean, @ColumnInfo(name = "lastNotificationSent") var lastNotificationSent: LocalDateTime?, - @ColumnInfo(name = "deviceType") val deviceType: DeviceType? + @ColumnInfo(name = "deviceType") val deviceType: DeviceType?, + @ColumnInfo(name = "riskLevel", defaultValue = "0") var riskLevel: Int, + @ColumnInfo(name = "lastCalculatedRiskDate") var lastCalculatedRiskDate: LocalDateTime?, + @ColumnInfo(name = "nextObservationNotification") var nextObservationNotification: LocalDateTime?, + @ColumnInfo(name = "currentObservationDuration") var currentObservationDuration: Long?, ) { constructor( @@ -49,7 +54,11 @@ data class BaseDevice( lastSeen, false, null, - deviceType + deviceType, + 0, + lastSeen, + null, + null, ) constructor(scanResult: ScanResult) : this( @@ -66,8 +75,15 @@ data class BaseDevice( } }, scanResult.scanRecord?.getManufacturerSpecificData(76)?.get(2), - LocalDateTime.now(), LocalDateTime.now(), false, null, - DeviceManager.getDeviceType(scanResult) + LocalDateTime.now(), + LocalDateTime.now(), + false, + null, + DeviceManager.getDeviceType(scanResult), + 0, + LocalDateTime.now(), + null, + null, ) fun getDeviceNameWithID(): String = name ?: device.defaultDeviceNameWithId @@ -111,6 +127,15 @@ data class BaseDevice( } } + fun getPublicKey(scanResult: ScanResult): String{ + return when (DeviceManager.getDeviceType(scanResult)) { + DeviceType.SAMSUNG -> SamsungDevice.getPublicKey(scanResult) + DeviceType.GALAXY_SMART_TAG -> SamsungDevice.getPublicKey(scanResult) + DeviceType.GALAXY_SMART_TAG_PLUS -> SamsungDevice.getPublicKey(scanResult) + else -> scanResult.device.address + } + } + fun getConnectionState(scanResult: ScanResult): ConnectionState { return when (DeviceManager.getDeviceType(scanResult)) { DeviceType.TILE -> Tile.getConnectionState(scanResult) @@ -125,5 +150,36 @@ data class BaseDevice( else -> ConnectionState.UNKNOWN } } + + fun getBatteryState(scanResult: ScanResult): BatteryState { + return when (DeviceManager.getDeviceType(scanResult)) { + DeviceType.GALAXY_SMART_TAG -> SamsungDevice.getBatteryState(scanResult) + DeviceType.GALAXY_SMART_TAG_PLUS -> SamsungDevice.getBatteryState(scanResult) + DeviceType.FIND_MY -> AirTag.getBatteryState(scanResult) + DeviceType.AIRTAG -> AirTag.getBatteryState(scanResult) + 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)) { + 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) + BatteryState.FULL -> ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.battery_full) + BatteryState.UNKNOWN -> ATTrackingDetectionApplication.getAppContext().resources.getString(R.string.battery_unknown) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/BatteryState.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/BatteryState.kt new file mode 100644 index 00000000..40a6a50e --- /dev/null +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/device/BatteryState.kt @@ -0,0 +1,5 @@ +package de.seemoo.at_tracking_detection.database.models.device + +enum class BatteryState { + FULL, MEDIUM, LOW, VERY_LOW, UNKNOWN +} \ No newline at end of file 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 7397e248..da1a6c05 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 @@ -19,4 +19,12 @@ interface DeviceContext { fun getConnectionState(scanResult: ScanResult): ConnectionState { return ConnectionState.UNKNOWN } + + fun getBatteryState(scanResult: ScanResult): BatteryState { + return BatteryState.UNKNOWN + } + + fun getPublicKey(scanResult: ScanResult): String{ + return scanResult.device.address + } } \ No newline at end of file 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 205c7e0b..7abc4279 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 @@ -1,6 +1,8 @@ package de.seemoo.at_tracking_detection.database.models.device +import de.seemoo.at_tracking_detection.R import de.seemoo.at_tracking_detection.database.models.device.types.* +import de.seemoo.at_tracking_detection.util.SharedPrefs enum class DeviceType { UNKNOWN, @@ -29,6 +31,45 @@ enum class DeviceType { GALAXY_SMART_TAG_PLUS -> SmartTagPlus.defaultDeviceName } } + + fun getImageDrawable(deviceType: DeviceType): Int { + return when (deviceType) { + UNKNOWN -> R.drawable.ic_baseline_device_unknown_24 + AIRPODS -> R.drawable.ic_airpods + AIRTAG -> R.drawable.ic_airtag + APPLE -> R.drawable.ic_baseline_device_unknown_24 + FIND_MY -> R.drawable.ic_chipolo + TILE -> R.drawable.ic_tile + CHIPOLO -> R.drawable.ic_chipolo + SAMSUNG -> R.drawable.ic_baseline_device_unknown_24 + GALAXY_SMART_TAG -> R.drawable.ic_smarttag_icon + GALAXY_SMART_TAG_PLUS -> R.drawable.ic_smarttag_icon + } + } + + fun getAllowedDeviceTypesFromSettings(): List { + val validDeviceTypes = SharedPrefs.devicesFilter.toList() + val allowedDeviceTypes = mutableListOf() + + for (validDeviceType in validDeviceTypes) { + when (validDeviceType) { + "airpods" -> allowedDeviceTypes.add(AIRPODS) + "airtags" -> allowedDeviceTypes.add(AIRTAG) + "apple_devices" -> allowedDeviceTypes.add(APPLE) + "chipolos" -> allowedDeviceTypes.add(CHIPOLO) + "find_my_devices" -> allowedDeviceTypes.add(FIND_MY) + "samsung_devices" -> allowedDeviceTypes.add(SAMSUNG) + "smart_tags" -> { + allowedDeviceTypes.add(GALAXY_SMART_TAG) + allowedDeviceTypes.add(GALAXY_SMART_TAG_PLUS) + } + "tiles" -> allowedDeviceTypes.add(TILE) + } + } + + return allowedDeviceTypes + } + } fun canBeIgnored(): Boolean { 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 31cf464d..c8a894bc 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 @@ -141,5 +141,26 @@ class AirTag(val id: Int) : Device(), Connectable { override val statusByteDeviceType: UInt get() = 1u + + override fun getBatteryState(scanResult: ScanResult): BatteryState { + val mfg: ByteArray? = scanResult.scanRecord?.getManufacturerSpecificData(0x4C) + + if (mfg != null && mfg.size >= 3) { + val status = mfg[2] // Extract the status byte + + // Bits 6-7: Battery level + val batteryLevel = (status.toInt() shr 6) and 0x03 + + // Full: 0, Medium 1, Low 2, Very Low 3 + when (batteryLevel) { + 0x00 -> return BatteryState.FULL + 0x01 -> return BatteryState.MEDIUM + 0x02 -> return BatteryState.LOW + 0x03 -> return BatteryState.VERY_LOW + } + } + + return BatteryState.UNKNOWN + } } } \ No newline at end of file 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 783dec01..afd7da41 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 @@ -52,7 +52,7 @@ class AppleDevice(val id: Int) : Device() { val mfg: ByteArray? = scanResult.scanRecord?.getManufacturerSpecificData(0x4C) if (mfg != null && mfg.size > 2) { - return if (mfg[1] == (0x19).toByte()){ + return if (mfg[1] == (0x19).toByte()) { ConnectionState.OVERMATURE_OFFLINE } else { ConnectionState.CONNECTED 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 4b4a8748..38435872 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 @@ -54,7 +54,7 @@ open class SamsungDevice(open val id: Int) : Device(){ override fun getConnectionState(scanResult: ScanResult): ConnectionState { val serviceData = scanResult.scanRecord?.getServiceData(offlineFindingServiceUUID) - if (serviceData != null) { + if (serviceData != null && serviceData.isNotEmpty()) { // Little Endian (5,6,7) --> (2,1,0) val bit5 = getBitsFromByte(serviceData[0], 2) val bit6 = getBitsFromByte(serviceData[0],1) @@ -77,6 +77,43 @@ open class SamsungDevice(open val id: Int) : Device(){ return ConnectionState.UNKNOWN } + override fun getBatteryState(scanResult: ScanResult): BatteryState { + val serviceData = scanResult.scanRecord?.getServiceData(offlineFindingServiceUUID) + + if (serviceData != null && serviceData.size >= 12) { + val bit6 = getBitsFromByte(serviceData[12],1) + val bit7 = getBitsFromByte(serviceData[12],0) + + return if (bit6 && bit7) { + Timber.d("Samsung Device Battery State: FULL") + BatteryState.FULL + } else if (bit6 && !bit7) { + Timber.d("Samsung Device Battery State: MEDIUM") + BatteryState.MEDIUM + } else if (!bit6 && bit7) { + Timber.d("Samsung Device Battery State: LOW") + BatteryState.LOW + } else { + Timber.d("Samsung Device Battery State: VERY_LOW") + BatteryState.VERY_LOW + } + } + + return BatteryState.UNKNOWN + } + + override fun getPublicKey(scanResult: ScanResult): String{ + val serviceData = scanResult.scanRecord?.getServiceData(SmartTag.offlineFindingServiceUUID) + + 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() + } + } + fun getSamsungDeviceType(scanResult: ScanResult): DeviceType{ val serviceData = scanResult.scanRecord?.getServiceData(SmartTag.offlineFindingServiceUUID) @@ -101,18 +138,6 @@ open class SamsungDevice(open val id: Int) : Device(){ SmartTag.deviceType } } - - fun getPublicKey(scanResult: ScanResult): String{ - val serviceData = scanResult.scanRecord?.getServiceData(SmartTag.offlineFindingServiceUUID) - - 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() - } - } } } \ No newline at end of file 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 9fd4f4e8..889e5331 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 @@ -5,6 +5,7 @@ import de.seemoo.at_tracking_detection.database.daos.DeviceDao import de.seemoo.at_tracking_detection.database.relations.DeviceBeaconNotification import de.seemoo.at_tracking_detection.database.models.device.BaseDevice import de.seemoo.at_tracking_detection.database.models.device.DeviceType +import de.seemoo.at_tracking_detection.util.risk.RiskLevel import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator import kotlinx.coroutines.flow.Flow import java.time.LocalDateTime @@ -20,6 +21,13 @@ class DeviceRepository @Inject constructor(private val deviceDao: DeviceDao) { fun trackingDevicesNotIgnoredSinceCount(since: LocalDateTime) = deviceDao.getAllTrackingDevicesNotIgnoredSinceCount(since) + fun getCachedRiskLevel(deviceAddress: String): Int = deviceDao.getCachedRiskLevel(deviceAddress) + + fun getLastCachedRiskLevelDate(deviceAddress: String): LocalDateTime? = deviceDao.getLastCachedRiskLevelDate(deviceAddress) + + fun updateRiskLevelCache(deviceAddress: String, riskLevel: Int, lastCalculatedRiskDate: LocalDateTime) + = deviceDao.updateRiskLevelCache(deviceAddress, riskLevel, lastCalculatedRiskDate) + fun trackingDevicesSinceCount(since: LocalDateTime) = deviceDao.trackingDevicesCount(since) val totalCount: Flow = deviceDao.getTotalCount() diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/NotificationRepository.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/NotificationRepository.kt index 1d0c27da..c8701b07 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/NotificationRepository.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/NotificationRepository.kt @@ -50,6 +50,8 @@ class NotificationRepository @Inject constructor( fun getFalseAlarmForDeviceSinceCount(deviceAddress: String, since: LocalDateTime): Int = notificationDao.getFalseAlarmForDeviceSinceCount(deviceAddress, since) + fun existsNotificationForDevice(deviceAddress: String): Boolean = notificationDao.existsNotificationForDevice(deviceAddress) + @WorkerThread suspend fun insert(notification: Notification): Long { return notificationDao.insert(notification) 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 97293da2..d10d5c03 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 @@ -5,6 +5,8 @@ import android.bluetooth.le.ScanResult 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 @@ -20,6 +22,7 @@ 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 -> { 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 23ced6d0..08c1d481 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 @@ -10,8 +10,6 @@ import android.os.* import androidx.core.content.ContextCompat import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication import de.seemoo.at_tracking_detection.util.BuildVersionProvider -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import timber.log.Timber import java.util.* import javax.inject.Inject @@ -149,7 +147,7 @@ open class LocationProvider @Inject constructor( // The fused location provider does not work reliably with Samsung + Android 12 // We just stay with the legacy location, because this just works - requestLegacyLocationUpdatesFromAnyProvider() + requestLocationUpdatesFromAnyProvider() if (timeoutMillis != null) { setTimeoutForLocationUpdate(requester = locationRequester, timeoutMillis= timeoutMillis) @@ -191,7 +189,8 @@ open class LocationProvider @Inject constructor( Timber.d("Location request timeout set to $timeoutMillis") } - private fun requestLegacyLocationUpdatesFromAnyProvider() { + + private fun requestLocationUpdatesFromAnyProvider() { if (ContextCompat.checkSelfPermission(ATTrackingDetectionApplication.getAppContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { return } @@ -201,27 +200,17 @@ open class LocationProvider @Inject constructor( val networkProviderEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) - if (gpsProviderEnabled) { - // Using GPS and Network provider, because the GPS provider does notwork indoors (it will never call the callback) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && locationManager.isProviderEnabled(LocationManager.FUSED_PROVIDER)) { locationManager.requestLocationUpdates( - LocationManager.GPS_PROVIDER, + LocationManager.FUSED_PROVIDER, MIN_UPDATE_TIME_MS, MIN_DISTANCE_METER, this, handler.looper ) - - if (networkProviderEnabled){ - locationManager.requestLocationUpdates( - LocationManager.NETWORK_PROVIDER, - MIN_UPDATE_TIME_MS, - MIN_DISTANCE_METER, - this, - handler.looper - ) - } - - } else if (networkProviderEnabled) { + } + + if (networkProviderEnabled) { locationManager.requestLocationUpdates( LocationManager.NETWORK_PROVIDER, MIN_UPDATE_TIME_MS, @@ -230,6 +219,31 @@ open class LocationProvider @Inject constructor( handler.looper ) } + + 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 + ) + } + + 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() + } + } } fun stopLocationUpdates() { @@ -275,7 +289,6 @@ open class LocationProvider @Inject constructor( const val MIN_DISTANCE_METER = 0.0F const val MAX_AGE_SECONDS = 120L const val MIN_ACCURACY_METER = 120L - const val MAX_LOCATION_DURATION = 60_000L /// Time until the location fetching will be stopped automatically } } 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 7ae4a763..873d6671 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 @@ -20,7 +20,7 @@ import de.seemoo.at_tracking_detection.BuildConfig import de.seemoo.at_tracking_detection.database.models.Beacon import de.seemoo.at_tracking_detection.database.models.Scan import de.seemoo.at_tracking_detection.database.models.device.* -import de.seemoo.at_tracking_detection.database.models.device.types.SamsungDevice.Companion.getPublicKey +import de.seemoo.at_tracking_detection.database.models.device.BaseDevice.Companion.getPublicKey import de.seemoo.at_tracking_detection.database.models.Location as LocationModel import de.seemoo.at_tracking_detection.database.repository.ScanRepository import de.seemoo.at_tracking_detection.notifications.NotificationService @@ -32,6 +32,7 @@ import de.seemoo.at_tracking_detection.detection.TrackingDetectorWorker.Companio 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 @@ -48,7 +49,7 @@ class ScanBluetoothWorker @AssistedInject constructor( private lateinit var bluetoothAdapter: BluetoothAdapter - private var scanResultDictionary: HashMap = HashMap() + private var scanResultDictionary: ConcurrentHashMap = ConcurrentHashMap() var location: Location? = null set(value) { @@ -60,6 +61,8 @@ class ScanBluetoothWorker @AssistedInject constructor( private var locationRetrievedCallback: (() -> Unit)? = null + private var locationFetchStarted: Long? = null + override suspend fun doWork(): Result { Timber.d("Bluetooth scanning worker started!") val scanMode = getScanMode() @@ -78,12 +81,13 @@ class ScanBluetoothWorker @AssistedInject constructor( return Result.retry() } - scanResultDictionary = HashMap() + scanResultDictionary = ConcurrentHashMap() val useLocation = SharedPrefs.useLocationInTrackingDetection if (useLocation) { // Returns the last known location if this matches our requirements or starts new location updates - location = locationProvider.lastKnownOrRequestLocationUpdates(locationRequester = locationRequester, timeoutMillis = 60_000L) + locationFetchStarted = System.currentTimeMillis() + location = locationProvider.lastKnownOrRequestLocationUpdates(locationRequester = locationRequester, timeoutMillis = LOCATION_UPDATE_MAX_TIME_MS - 2000L) } //Starting BLE Scan @@ -107,15 +111,21 @@ class ScanBluetoothWorker @AssistedInject constructor( location = locationProvider.getLastLocation(checkRequirements = false) } + val validDeviceTypes = DeviceType.getAllowedDeviceTypesFromSettings() + //Adding all scan results to the database after the scan has finished scanResultDictionary.forEach { (_, discoveredDevice) -> - insertScanResult( - discoveredDevice.scanResult, - location?.latitude, - location?.longitude, - location?.accuracy, - discoveredDevice.discoveryDate, - ) + val deviceType = DeviceManager.getDeviceType(discoveredDevice.scanResult) + + if (deviceType in validDeviceTypes) { + insertScanResult( + discoveredDevice.scanResult, + location?.latitude, + location?.longitude, + location?.accuracy, + discoveredDevice.discoveryDate, + ) + } } SharedPrefs.lastScanDate = LocalDateTime.now() @@ -163,6 +173,8 @@ class ScanBluetoothWorker @AssistedInject constructor( 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() } } @@ -180,9 +192,9 @@ class ScanBluetoothWorker @AssistedInject constructor( private fun getScanDuration(): Long { val useLowPower = SharedPrefs.useLowPowerBLEScan return if (useLowPower) { - 15000L + 30_000L } else { - 8000L + 20_000L } } @@ -214,7 +226,7 @@ class ScanBluetoothWorker @AssistedInject constructor( } // Fallback if no location is fetched in time - val maximumLocationDurationMillis = 60_000L + val maximumLocationDurationMillis = LOCATION_UPDATE_MAX_TIME_MS handler.postDelayed(runnable, maximumLocationDurationMillis) } } @@ -224,6 +236,7 @@ class ScanBluetoothWorker @AssistedInject constructor( 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, @@ -245,7 +258,7 @@ class ScanBluetoothWorker @AssistedInject constructor( discoveryDate: LocalDateTime, locId: Int? ): Beacon? { - val beaconRepository = ATTrackingDetectionApplication.getCurrentApp()?.beaconRepository!! + val beaconRepository = ATTrackingDetectionApplication.getCurrentApp()?.beaconRepository ?: return null val uuids = scanResult.scanRecord?.serviceUuids?.map { it.toString() }?.toList() val uniqueIdentifier = getPublicKey(scanResult) @@ -287,7 +300,7 @@ class ScanBluetoothWorker @AssistedInject constructor( scanResult: ScanResult, discoveryDate: LocalDateTime ): BaseDevice? { - val deviceRepository = ATTrackingDetectionApplication.getCurrentApp()?.deviceRepository!! + val deviceRepository = ATTrackingDetectionApplication.getCurrentApp()?.deviceRepository ?: return null val deviceAddress = getPublicKey(scanResult) @@ -325,7 +338,7 @@ class ScanBluetoothWorker @AssistedInject constructor( discoveryDate: LocalDateTime, accuracy: Float? ): LocationModel? { - val locationRepository = ATTrackingDetectionApplication.getCurrentApp()?.locationRepository!! + val locationRepository = ATTrackingDetectionApplication.getCurrentApp()?.locationRepository ?: return null // set location to null if gps location could not be retrieved var location: LocationModel? = null 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 b3f0c58b..929fa273 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 @@ -21,6 +21,7 @@ import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator import timber.log.Timber import java.time.LocalDateTime import java.time.temporal.ChronoUnit +import java.util.concurrent.ConcurrentHashMap @HiltWorker class TrackingDetectorWorker @AssistedInject constructor( @@ -47,10 +48,10 @@ class TrackingDetectorWorker @AssistedInject constructor( var notificationsSent = 0 cleanedBeaconsPerDevice.forEach { mapEntry -> - val device = deviceRepository.getDevice(mapEntry.key) + val device = deviceRepository.getDevice(mapEntry.key) ?: return@forEach val useLocation = SharedPrefs.useLocationInTrackingDetection - if (device != null && RiskLevelEvaluator.checkRiskLevelForDevice(device, useLocation) != RiskLevel.LOW && checkLastNotification(device)) { + if (RiskLevelEvaluator.checkRiskLevelForDevice(device, useLocation) != RiskLevel.LOW && checkLastNotification(device)) { // Send Notification Timber.d("Conditions for device ${device.address} being a tracking device are true... Sending Notification!") notificationService.sendTrackingNotification(device) @@ -75,8 +76,8 @@ class TrackingDetectorWorker @AssistedInject constructor( * Retrieves the devices detected during the last scan (last 15min) * @return a HashMap with the device address as key and the list of beacons as value (all beacons in the relevant interval) */ - private fun getLatestBeaconsPerDevice(): HashMap> { - val beaconsPerDevice: HashMap> = HashMap() + private fun getLatestBeaconsPerDevice(): ConcurrentHashMap> { + val beaconsPerDevice: ConcurrentHashMap> = ConcurrentHashMap() val since = SharedPrefs.lastScanDate?.minusMinutes(15) ?: LocalDateTime.now().minusMinutes(30) //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 { @@ -95,14 +96,17 @@ class TrackingDetectorWorker @AssistedInject constructor( return location } + /** + * Checks if the last notification was sent more than x hours ago + */ private fun checkLastNotification(device: BaseDevice): Boolean { - return if (device.lastNotificationSent != null) { - val hoursPassed = device.lastNotificationSent!!.until(LocalDateTime.now(), ChronoUnit.HOURS) - // Last Notification longer than 8 hours - hoursPassed >= RiskLevelEvaluator.HOURS_AT_LEAST_UNTIL_NEXT_NOTIFICATION - } else{ - true - } + 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 } } 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 25fe692e..e0379385 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 @@ -223,6 +223,53 @@ class NotificationBuilder @Inject constructor( return notification.build() } + fun buildObserveTrackerNotification( + deviceAddress: String, + notificationId: Int, + observationDuration: Long, + observationPositive: Boolean + ): Notification { + Timber.d("Notification with id $notificationId for device $deviceAddress has been build!") + 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 + ) + } + } else { + context.getString( + R.string.notification_observe_tracker_negative, + ) + } + + var notification = NotificationCompat.Builder(context, NotificationConstants.CHANNEL_ID) + .setContentTitle(context.getString(R.string.notification_observe_tracker_title_base)) + .setContentText(notifyText) + .setPriority(getNotificationPriority()) + .setContentIntent(pendingNotificationIntent(bundle, notificationId)) + .setCategory(getNotificationCategory()) + .setSmallIcon(R.drawable.ic_warning) + .setStyle(NotificationCompat.BigTextStyle().bigText(notifyText)) + + notification = notification.setDeleteIntent( + buildPendingIntent( + bundle, + NotificationConstants.DISMISSED_ACTION, + NotificationConstants.DISMISSED_CODE + ) + ).setAutoCancel(true) + + return notification.build() + + } + fun buildBluetoothErrorNotification(): Notification { val notificationId = -100 val bundle: Bundle = Bundle().apply { putInt("notificationId", notificationId) } @@ -241,14 +288,14 @@ class NotificationBuilder @Inject constructor( val deviceType = DeviceManager.getDeviceType(scanResult) - val milisecondsSinceEvent = (SystemClock.elapsedRealtimeNanos() - scanResult.timestampNanos) / 1000000L - val timeOfEvent = System.currentTimeMillis() - milisecondsSinceEvent + val millisecondsSinceEvent = (SystemClock.elapsedRealtimeNanos() - scanResult.timestampNanos) / 1000000L + val timeOfEvent = System.currentTimeMillis() - millisecondsSinceEvent val eventDate = Instant.ofEpochMilli(timeOfEvent).atZone(ZoneId.systemDefault()).toLocalDateTime() return NotificationCompat.Builder(context, NotificationConstants.INFO_CHANNEL_ID) .setContentTitle("Discovered ${deviceType.name} | ${scanResult.device.address}") - .setContentText("Received at ${eventDate.toString()}") + .setContentText("Received at $eventDate") .setPriority(getNotificationPriority()) .setCategory(Notification.CATEGORY_STATUS) .setSmallIcon(R.drawable.ic_scan_icon) @@ -256,6 +303,7 @@ class NotificationBuilder @Inject constructor( .build() } + /* fun buildSurveyInfoNotification(): Notification { val context = ATTrackingDetectionApplication.getAppContext() val text = context.getString(R.string.survey_info_1) + " " + context.getString(R.string.survey_info_2) + " " + context.getString(R.string.survey_info_3) @@ -273,6 +321,7 @@ class NotificationBuilder @Inject constructor( .setAutoCancel(true) .build() } + */ private fun getNotificationPriority(): Int { return if (SharedPrefs.notificationPriorityHigh){ 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 11a072f7..c34ebbe3 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 @@ -12,13 +12,12 @@ import androidx.core.app.NotificationManagerCompat import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication import de.seemoo.at_tracking_detection.BuildConfig import de.seemoo.at_tracking_detection.database.models.device.BaseDevice -import de.seemoo.at_tracking_detection.database.models.device.DeviceType import de.seemoo.at_tracking_detection.database.viewmodel.NotificationViewModel import de.seemoo.at_tracking_detection.util.SharedPrefs import timber.log.Timber import java.time.LocalDateTime import java.time.temporal.ChronoUnit -import java.time.temporal.TemporalUnit +import java.util.UUID import javax.inject.Inject import javax.inject.Singleton import kotlin.random.Random @@ -29,58 +28,101 @@ class NotificationService @Inject constructor( private val notificationBuilder: NotificationBuilder, private val notificationViewModel: NotificationViewModel ) { - + @SuppressLint("MissingPermission") suspend fun sendTrackingNotification(deviceAddress: String) { val notificationId = notificationViewModel.insert(deviceAddress) with(notificationManagerCompat) { - notify( - TRACKING_NOTIFICATION_TAG, - notificationId, - notificationBuilder.buildTrackingNotification(deviceAddress, notificationId) - ) + if (this.areNotificationsEnabled()) { + notify( + TRACKING_NOTIFICATION_TAG, + notificationId, + notificationBuilder.buildTrackingNotification(deviceAddress, notificationId) + ) + } } } + @SuppressLint("MissingPermission") suspend fun sendTrackingNotification(baseDevice: BaseDevice) { val notificationId = notificationViewModel.insert(deviceAddress = baseDevice.address) with(notificationManagerCompat) { - notify( - TRACKING_NOTIFICATION_TAG, - notificationId, - notificationBuilder.buildTrackingNotification(baseDevice, notificationId) - ) + if (this.areNotificationsEnabled()) { + notify( + TRACKING_NOTIFICATION_TAG, + notificationId, + notificationBuilder.buildTrackingNotification(baseDevice, notificationId) + ) + } } } - fun sendBLEErrorNotification() { + @SuppressLint("MissingPermission") + fun sendObserveTrackerNotification(deviceAddress: String, observationDuration: Long, observationPositive: Boolean) { + val notificationId = generateNotificationId() with(notificationManagerCompat) { - notify( - BLE_SCAN_ERROR_TAG, - -100, - notificationBuilder.buildBluetoothErrorNotification() - ) + if (this.areNotificationsEnabled()) { + notify( + OBSERVE_TRACKER_NOTIFICATION_TAG, + notificationId, + notificationBuilder.buildObserveTrackerNotification(deviceAddress, notificationId, observationDuration, observationPositive) + ) + } } } - fun sendSurveyInfoNotification() { + /* + @SuppressLint("MissingPermission") + suspend fun sendObserveTrackerNotification(baseDevice: BaseDevice) { + val notificationId = notificationViewModel.insert(deviceAddress = baseDevice.address) with(notificationManagerCompat) { + if (this.areNotificationsEnabled()) { + notify( + OBSERVE_TRACKER_NOTIFICATION_TAG, + notificationId, + notificationBuilder.buildTrackingNotification(baseDevice, notificationId) + ) + } + } + } - notify( - SURVEY_INFO_TAG, - -101, - notificationBuilder.buildSurveyInfoNotification() - ) + */ + + + @SuppressLint("MissingPermission") + fun sendBLEErrorNotification() { + with(notificationManagerCompat) { + if (this.areNotificationsEnabled()) { + notify( + BLE_SCAN_ERROR_TAG, + -100, + notificationBuilder.buildBluetoothErrorNotification() + ) + } } - SharedPrefs.surveyNotficationSent = true } +// fun sendSurveyInfoNotification() { +// with(notificationManagerCompat) { +// +// notify( +// SURVEY_INFO_TAG, +// -101, +// notificationBuilder.buildSurveyInfoNotification() +// ) +// } +// SharedPrefs.surveyNotficationSent = true +// } + + @SuppressLint("MissingPermission") fun sendDebugNotificationFoundDevice(scanResult: ScanResult) { with(notificationManagerCompat) { - notify( - BLE_SCAN_ERROR_TAG, - Random.nextInt(), - notificationBuilder.buildDebugFoundDeviceNotification(scanResult) + if (this.areNotificationsEnabled()) { + notify( + BLE_SCAN_ERROR_TAG, + Random.nextInt(), + notificationBuilder.buildDebugFoundDeviceNotification(scanResult) ) + } } } @@ -115,7 +157,7 @@ class NotificationService @Inject constructor( val alarmManager = ATTrackingDetectionApplication.getAppContext().getSystemService(Context.ALARM_SERVICE) as AlarmManager alarmManager.set(AlarmManager.RTC_WAKEUP, alarmTime, pendingIntent) - Timber.d("Scheduled a survey reminder notificaiton at $dateForNotification") + Timber.d("Scheduled a survey reminder notification at $dateForNotification") } } @@ -149,6 +191,12 @@ class NotificationService @Inject constructor( "de.seemoo.at_tracking_detection.tracking_notification" const val BLE_SCAN_ERROR_TAG = "de.seemoo.at_tracking_detection.ble_scan_error_notification" - const val SURVEY_INFO_TAG = "de.seemoo.at_tracking_detection.survey_info" + const val OBSERVE_TRACKER_NOTIFICATION_TAG = + "de.seemoo.at_tracking_detection.observe_tracker_notification" + // const val SURVEY_INFO_TAG = "de.seemoo.at_tracking_detection.survey_info" + + fun generateNotificationId(): Int { + return UUID.randomUUID().hashCode() + } } } \ No newline at end of file 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 fd07f6e6..c3c2cd36 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 @@ -12,7 +12,6 @@ class ScheduledNotificationReceiver: BroadcastReceiver() { Timber.d("Broadcast received ${intent?.action}") val notificationService = ATTrackingDetectionApplication.getCurrentApp()?.notificationService - SharedPrefs.dismissSurveyInformation = false - notificationService?.sendSurveyInfoNotification() + SharedPrefs.dismissSurveyInformation = true } } \ 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 new file mode 100644 index 00000000..244dd9de --- /dev/null +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/MarkdownViewerActivity.kt @@ -0,0 +1,45 @@ +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 d3cbc1ab..31af1aa7 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 @@ -45,6 +45,7 @@ class OnboardingActivity : AppIntro() { Manifest.permission.BLUETOOTH_SCAN -> scanSlide(1) Manifest.permission.ACCESS_BACKGROUND_LOCATION -> backgroundLocationSlide(1) Manifest.permission.BLUETOOTH_CONNECT -> connectSlide(1) + Manifest.permission.POST_NOTIFICATIONS -> notificationSlide(1) } } else { buildSlides() @@ -65,11 +66,7 @@ class OnboardingActivity : AppIntro() { Manifest.permission.ACCESS_BACKGROUND_LOCATION ) == PackageManager.PERMISSION_GRANTED - if (locationPermissionState && backgroundPermissionState) { - SharedPrefs.useLocationInTrackingDetection = true - } else { - SharedPrefs.useLocationInTrackingDetection = false - } + SharedPrefs.useLocationInTrackingDetection = locationPermissionState && backgroundPermissionState if (permission == null) { @@ -92,6 +89,25 @@ class OnboardingActivity : AppIntro() { handleRequiredPermission(permissionName) } + private fun notificationSlide(slideNumber: Int): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + addSlide( + AppIntroFragment.newInstance( + title = getString(R.string.onboarding_notification_title), + description = getString(R.string.onboarding_notification_description), + imageDrawable = R.drawable.ic_onboarding_notification + ) + ) + askForPermissions( + permissions = arrayOf(Manifest.permission.POST_NOTIFICATIONS), + slideNumber = slideNumber, + required = false + ) + return true + } + return false + } + private fun scanSlide(slideNumber: Int): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { addSlide( @@ -200,6 +216,8 @@ class OnboardingActivity : AppIntro() { backgroundLocationSlide(slideNumber + 2) + notificationSlide(slideNumber + 3) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { addSlide(IgnoreBatteryOptimizationFragment.newInstance()) } diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardFragment.kt index 83f31617..03ea3203 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardFragment.kt @@ -58,7 +58,6 @@ class DashboardFragment : Fragment() { } - companion object { private val dateTime = LocalDateTime.now(ZoneOffset.UTC) private const val HISTORY_LENGTH = 14L 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 44af2d62..e19c44be 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 @@ -59,7 +59,7 @@ class DashboardViewModel @Inject constructor( val isMapLoading = MutableLiveData(false) val isScanning: LiveData = - Transformations.map(backgroundWorkScheduler.getState(WorkerConstants.PERIODIC_SCAN_WORKER)) { + backgroundWorkScheduler.getState(WorkerConstants.PERIODIC_SCAN_WORKER).map { it == WorkInfo.State.RUNNING } 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 27ef6aac..1463b427 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 @@ -58,12 +58,11 @@ class DeviceMapFragment : Fragment() { Utility.enableMyLocationOverlay(map) val deviceAddress = this.deviceAddress - if (deviceAddress != null && !deviceAddress.isEmpty()) { + if (!deviceAddress.isNullOrEmpty()) { viewModel.markerLocations.observe(viewLifecycleOwner) { lifecycleScope.launch { val locationList = arrayListOf() - val locationRepository = - ATTrackingDetectionApplication.getCurrentApp()?.locationRepository!! + val locationRepository = ATTrackingDetectionApplication.getCurrentApp()?.locationRepository ?: return@launch it.filter { it.locationId != null && it.locationId != 0 } .map { @@ -83,7 +82,7 @@ class DeviceMapFragment : Fragment() { lifecycleScope.launch { val locationList = arrayListOf() val locationRepository = - ATTrackingDetectionApplication.getCurrentApp()?.locationRepository!! + ATTrackingDetectionApplication.getCurrentApp()?.locationRepository ?: return@launch it.filter { it.locationId != null && it.locationId != 0 } .map { 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 09ee22bb..1b5ef4d0 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 @@ -4,8 +4,6 @@ import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel import de.seemoo.at_tracking_detection.database.models.Beacon 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.NotificationRepository import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator import javax.inject.Inject @@ -15,7 +13,7 @@ class DeviceMapViewModel @Inject constructor( val deviceAddress = MutableLiveData() - val markerLocations: LiveData> = Transformations.map(deviceAddress) { + val markerLocations: LiveData> = deviceAddress.map { beaconRepository.getDeviceBeacons(it) } diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/DevicesViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/DevicesViewModel.kt index a5383820..bc40d7e4 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/DevicesViewModel.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/DevicesViewModel.kt @@ -32,7 +32,7 @@ class DevicesViewModel @Inject constructor( fun getDeviceBeaconsCount(deviceAddress: String): String = beaconRepository.getDeviceBeaconsCount(deviceAddress).toString() - fun getDevice(deviceAddress: String): BaseDevice = deviceRepository.getDevice(deviceAddress)!! + fun getDevice(deviceAddress: String): BaseDevice? = deviceRepository.getDevice(deviceAddress) fun getMarkerLocations(deviceAddress: String): List = beaconRepository.getDeviceBeacons(deviceAddress) @@ -108,7 +108,7 @@ class DevicesViewModel @Inject constructor( filterStringBuilder.append(DeviceType.userReadableName(device)) filterStringBuilder.append(", ") } - if (deviceTypeFilter.deviceTypes.count() > 0) { + if (deviceTypeFilter.deviceTypes.isNotEmpty()) { filterStringBuilder.delete( filterStringBuilder.length - 2, filterStringBuilder.length - 1 diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/filter/FilterDialogFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/filter/FilterDialogFragment.kt index c823f8c9..e96012fc 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/filter/FilterDialogFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/devices/filter/FilterDialogFragment.kt @@ -7,7 +7,6 @@ import android.view.ViewGroup import androidx.core.util.Pair import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.button.MaterialButton import com.google.android.material.datepicker.MaterialDatePicker import dagger.hilt.android.AndroidEntryPoint @@ -61,6 +60,9 @@ class FilterDialogFragment : } private fun filterAdaptions() { + val devicesViewModel = devicesViewModel // Assuming devicesViewModel is not frequently changing + + // Initialize filter chips binding.filterIgnoreChip.isChecked = devicesViewModel.activeFilter.containsKey(IgnoredFilter::class.toString()) binding.filterNotifiedChip.isChecked = @@ -75,17 +77,16 @@ class FilterDialogFragment : DeviceTypeFilter::class.toString(), defaultDeviceTypeFilter ) as DeviceTypeFilter - for (device in DeviceManager.devices) { - val chip = - IncludeFilterChipBinding.inflate(LayoutInflater.from(context)) + // Create and add device type filter chips + DeviceManager.devices.forEach { device -> + val chip = IncludeFilterChipBinding.inflate(LayoutInflater.from(context)) chip.text = device.defaultDeviceName - if (activeDeviceTypeFilter.contains(device.deviceType)) { - chip.filterDeviceTypeChip.isChecked = true - } + val isChecked = activeDeviceTypeFilter.contains(device.deviceType) + chip.filterDeviceTypeChip.isChecked = isChecked chip.filterDeviceTypeChip.id = (device.deviceType.toString() + ".chip").hashCode() - chip.filterDeviceTypeChip.setOnCheckedChangeListener { _, isChecked -> + chip.filterDeviceTypeChip.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { activeDeviceTypeFilter.add(device.deviceType) } else { @@ -96,6 +97,7 @@ class FilterDialogFragment : binding.filterDeviceTypes.addView(chip.root) } + // Set click listeners for filter chips binding.filterIgnoreChip.setOnClickListener { devicesViewModel.addOrRemoveFilter( IgnoredFilter.build(), !binding.filterIgnoreChip.isChecked @@ -106,6 +108,8 @@ class FilterDialogFragment : NotifiedFilter.build(), !binding.filterNotifiedChip.isChecked ) } + + // Date range picker click listener binding.filterDateRangeInput.setOnClickListener { var datePickerBuilder = MaterialDatePicker.Builder.dateRangePicker() .setTitleText(getString(R.string.filter_date_range_picker_title)) @@ -125,6 +129,8 @@ class FilterDialogFragment : ) } } + + // Clear date range filter binding.filterDateRange.setEndIconOnClickListener { binding.filterDateRangeInput.text?.clear() devicesViewModel.addOrRemoveFilter(DateRangeFilter.build(), true) diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/onboarding/BackgroundLocationFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/onboarding/BackgroundLocationFragment.kt index d126388c..d12fc6fb 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/onboarding/BackgroundLocationFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/onboarding/BackgroundLocationFragment.kt @@ -4,10 +4,12 @@ import android.Manifest import android.app.AlertDialog import android.content.DialogInterface import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.view.View import android.widget.Button import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import com.github.appintro.SlidePolicy @@ -17,13 +19,13 @@ import de.seemoo.at_tracking_detection.R @AndroidEntryPoint class BackgroundLocationFragment : Fragment(R.layout.fragment_background_location_permission_onboarding), SlidePolicy { - var canContinue = true + private var canContinue = true // Register the permissions callback, which handles the user's response to the // system permissions dialog. Save the return value, an instance of // ActivityResultLauncher. You can use either a val, as shown in this snippet, // or a lateinit var in your onAttach() or onCreate() method. - val requestPermissionLauncher = + private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> @@ -47,6 +49,7 @@ class BackgroundLocationFragment : Fragment(R.layout.fragment_background_locatio override val isPolicyRespected: Boolean get() = canContinue + @RequiresApi(Build.VERSION_CODES.Q) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -56,29 +59,32 @@ class BackgroundLocationFragment : Fragment(R.layout.fragment_background_locatio } } + @RequiresApi(Build.VERSION_CODES.Q) override fun onUserIllegallyRequestedNextPage() { showAlertDialogForLocationPermission() } + @RequiresApi(Build.VERSION_CODES.Q) private fun showAlertDialogForLocationPermission() { - val builder: AlertDialog.Builder? = context.let { AlertDialog.Builder(it) } + val builder: AlertDialog.Builder = context.let { AlertDialog.Builder(it) } - builder?.setMessage(R.string.onboarding_4_description) - builder?.setTitle(R.string.onboarding_4_title) - builder?.setIcon(R.drawable.ic_baseline_location_on_24) + builder.setMessage(R.string.onboarding_4_description) + builder.setTitle(R.string.onboarding_4_title) + builder.setIcon(R.drawable.ic_baseline_location_on_24) - builder?.setPositiveButton(R.string.ok_button) { _: DialogInterface, _: Int -> + builder.setPositiveButton(R.string.ok_button) { _: DialogInterface, _: Int -> this.requestPermissionLauncher.launch(Manifest.permission.ACCESS_BACKGROUND_LOCATION) } - builder?.setNegativeButton(getString(R.string.cancel)) { _: DialogInterface, _:Int -> + builder.setNegativeButton(getString(R.string.cancel)) { _: DialogInterface, _:Int -> } - val dialog = builder?.create() + val dialog = builder.create() dialog?.show() } + @RequiresApi(Build.VERSION_CODES.Q) fun requestLocationPermission() { when { ContextCompat.checkSelfPermission( diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/onboarding/ShareDataFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/onboarding/ShareDataFragment.kt index cb8367d7..34ef57cd 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/onboarding/ShareDataFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/onboarding/ShareDataFragment.kt @@ -69,8 +69,10 @@ class ShareDataFragment : Fragment(), SlidePolicy { get() = buttonPressed override fun onUserIllegallyRequestedNextPage() { - Timber.d("User illegally requested the next page!") - Snackbar.make(requireView(), R.string.onboarding_share_data_dialog, Snackbar.LENGTH_SHORT) - .show() + if (!buttonPressed) { + Timber.d("User illegally requested the next page!") + Snackbar.make(requireView(), R.string.onboarding_share_data_dialog, Snackbar.LENGTH_SHORT) + .show() + } } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/BluetoothDeviceAdapter.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/BluetoothDeviceAdapter.kt index b96e8b25..ac183bde 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/BluetoothDeviceAdapter.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/BluetoothDeviceAdapter.kt @@ -7,14 +7,16 @@ import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageView import androidx.fragment.app.FragmentManager +import androidx.navigation.findNavController import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.card.MaterialCardView import de.seemoo.at_tracking_detection.R import de.seemoo.at_tracking_detection.databinding.ItemScanResultBinding import de.seemoo.at_tracking_detection.ui.scan.dialog.PlaySoundDialogFragment import de.seemoo.at_tracking_detection.util.Utility -import de.seemoo.at_tracking_detection.database.models.device.types.SamsungDevice.Companion.getPublicKey +import de.seemoo.at_tracking_detection.database.models.device.BaseDevice.Companion.getPublicKey class BluetoothDeviceAdapter constructor(private val fragmentManager: FragmentManager) : ListAdapter(Companion) { @@ -39,15 +41,32 @@ class BluetoothDeviceAdapter constructor(private val fragmentManager: FragmentMa val scanResult: ScanResult = getItem(position) holder.bind(scanResult) - holder.itemView.findViewById(R.id.scan_result_play_sound).setOnClickListener { - val hasAllPermissions = - Build.VERSION.SDK_INT < Build.VERSION_CODES.S || Utility.checkAndRequestPermission( - Manifest.permission.BLUETOOTH_CONNECT - ) - if (hasAllPermissions) { - PlaySoundDialogFragment(scanResult).show(fragmentManager, null) + holder.itemView.findViewById(R.id.scan_result_item_card) + .setOnClickListener() { + val deviceAddress: String = getPublicKey(scanResult) + val directions = ScanFragmentDirections.actionScanToTrackingFragment(deviceAddress) + holder.itemView.findNavController() + .navigate(directions) + } + + holder.itemView.findViewById(R.id.scan_signal_strength) + .setOnClickListener() { + val deviceAddress: String = getPublicKey(scanResult) + val directions = ScanFragmentDirections.actionScanToScanDistance(deviceAddress) + holder.itemView.findNavController() + .navigate(directions) + } + + holder.itemView.findViewById(R.id.scan_result_play_sound) + .setOnClickListener() { + val hasAllPermissions = + Build.VERSION.SDK_INT < Build.VERSION_CODES.S || Utility.checkAndRequestPermission( + Manifest.permission.BLUETOOTH_CONNECT + ) + if (hasAllPermissions) { + PlaySoundDialogFragment(scanResult).show(fragmentManager, null) + } } - } } companion object : DiffUtil.ItemCallback() { diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanDistanceFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanDistanceFragment.kt new file mode 100644 index 00000000..8ae0e374 --- /dev/null +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanDistanceFragment.kt @@ -0,0 +1,268 @@ +package de.seemoo.at_tracking_detection.ui.scan + +import android.animation.ObjectAnimator +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.animation.addListener +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.navArgs +import com.google.android.material.snackbar.Snackbar +import de.seemoo.at_tracking_detection.R +import de.seemoo.at_tracking_detection.database.models.device.BaseDevice.Companion.getBatteryState +import de.seemoo.at_tracking_detection.database.models.device.BaseDevice.Companion.getBatteryStateAsString +import de.seemoo.at_tracking_detection.database.models.device.BaseDevice.Companion.getConnectionState +import de.seemoo.at_tracking_detection.database.models.device.BaseDevice.Companion.getConnectionStateAsString +import de.seemoo.at_tracking_detection.util.ble.BLEScanner +import de.seemoo.at_tracking_detection.database.models.device.BaseDevice.Companion.getPublicKey +import de.seemoo.at_tracking_detection.database.models.device.BatteryState +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.databinding.FragmentScanDistanceBinding +import de.seemoo.at_tracking_detection.util.Utility +import timber.log.Timber + +class ScanDistanceFragment : Fragment() { + private val viewModel: ScanDistanceViewModel by viewModels() + private val safeArgs: ScanDistanceFragmentArgs by navArgs() + + private var deviceAddress: String? = null + + private var oldAnimationValue = 0f + private val animationDuration = 1000L + + private lateinit var binding: FragmentScanDistanceBinding + + private val scanCallback: ScanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult?) { + super.onScanResult(callbackType, result) + result?.let { + val publicKey = safeArgs.deviceAddress + + if (publicKey == null) { + showSearchMessage() + } + + if (getPublicKey(it) == publicKey){ + // viewModel.bluetoothRssi.postValue(it.rssi) + val connectionState = getConnectionState(it) + val connectionStateString = getConnectionStateAsString(it) + viewModel.connectionStateString.postValue(connectionStateString) + viewModel.connectionState.postValue(connectionState) + val batteryState = getBatteryState(it) + val batteryStateString = getBatteryStateAsString(it) + viewModel.batteryStateString.postValue(batteryStateString) + viewModel.batteryState.postValue(batteryState) + val connectionQuality = Utility.dbmToPercent(it.rssi).toFloat() + val displayedConnectionQuality = (connectionQuality * 100).toInt() + viewModel.connectionQuality.postValue(displayedConnectionQuality) + + val deviceType = DeviceManager.getDeviceType(it) + setDeviceType(deviceType) + setBattery(batteryState) + setHeight(connectionQuality) + + if (viewModel.isFirstScanCallback.value as Boolean) { + viewModel.isFirstScanCallback.value = false + removeSearchMessage() + } + } + + } + } + + override fun onScanFailed(errorCode: Int) { + super.onScanFailed(errorCode) + Timber.e("BLE Scan failed. $errorCode") + stopBluetoothScan() + view?.let { + Snackbar.make( + it, + R.string.ble_service_connection_error, + Snackbar.LENGTH_LONG + ) + } + } + } + + private fun removeSearchMessage() { + binding.scanResultLoadingBar.visibility = View.GONE + binding.searchingForDevice.visibility = View.GONE + binding.connectionQuality.visibility = View.VISIBLE + binding.batteryLayout.visibility = View.VISIBLE + binding.deviceTypeLayout.visibility = View.VISIBLE + binding.connectionStateLayout.visibility = View.VISIBLE + binding.deviceNotFound.visibility = View.GONE + } + + private fun showSearchMessage() { + binding.scanResultLoadingBar.visibility = View.VISIBLE + binding.searchingForDevice.visibility = View.VISIBLE + binding.connectionQuality.visibility = View.GONE + binding.batteryLayout.visibility = View.GONE + binding.deviceTypeLayout.visibility = View.GONE + binding.connectionStateLayout.visibility = View.GONE + binding.deviceNotFound.visibility = View.GONE + } + + private fun deviceNotFound() { + binding.scanResultLoadingBar.visibility = View.GONE + binding.searchingForDevice.visibility = View.GONE + binding.connectionQuality.visibility = View.GONE + binding.batteryLayout.visibility = View.GONE + binding.deviceTypeLayout.visibility = View.GONE + binding.connectionStateLayout.visibility = View.GONE + binding.deviceNotFound.visibility = View.VISIBLE + + setHeight(1f, 100L) + } + + private fun setHeight(connectionQuality: Float, speed: Long = animationDuration) { + val viewHeight = binding.backgroundBar.height + val targetHeight: Float = connectionQuality * viewHeight * (-1) + viewHeight + + ObjectAnimator.ofFloat( + binding.backgroundBar, + "translationY", + oldAnimationValue, + targetHeight + ).apply { + cancel() // cancels any old animation + duration = speed + addListener(onEnd = { + // only changes the value after the animation is done + oldAnimationValue = targetHeight + }) + start() + } + } + + private fun setBattery(batteryState: BatteryState) { + when(batteryState) { + BatteryState.FULL -> binding.batterySymbol.setImageDrawable(resources.getDrawable(R.drawable.ic_battery_full_24)) + BatteryState.MEDIUM -> binding.batterySymbol.setImageDrawable(resources.getDrawable(R.drawable.ic_battery_medium_24)) + BatteryState.LOW -> binding.batterySymbol.setImageDrawable(resources.getDrawable(R.drawable.ic_battery_low_24)) + BatteryState.VERY_LOW -> binding.batterySymbol.setImageDrawable(resources.getDrawable(R.drawable.ic_battery_very_low_24)) + else -> binding.batterySymbol.setImageDrawable(resources.getDrawable(R.drawable.ic_battery_unknown_24)) + } + } + + private fun setDeviceType(deviceType: DeviceType) { + val drawable = resources.getDrawable(DeviceType.getImageDrawable(deviceType)) + binding.deviceTypeSymbol.setImageDrawable(drawable) + binding.deviceTypeText.text = DeviceType.userReadableName(deviceType) + } + + private fun startBluetoothScan() { + // Start a scan if the BLEScanner is not already running + if (!BLEScanner.isScanning) { + BLEScanner.startBluetoothScan(this.requireContext()) + } + + // Register the current fragment as a callback + BLEScanner.registerCallback(this.scanCallback) + + // Show to the user that no devices have been found + Handler(Looper.getMainLooper()).postDelayed({ + // Stop scanning if no device was detected + if(viewModel.isFirstScanCallback.value as Boolean) { + stopBluetoothScan() + deviceNotFound() + } + }, SCAN_DURATION) + } + + private fun stopBluetoothScan() { + // We just unregister the callback, but keep the scanner running + // until the app is closed / moved to background + BLEScanner.unregisterCallback(this.scanCallback) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = DataBindingUtil.inflate( + inflater, + R.layout.fragment_scan_distance, + container, + false + ) + binding.lifecycleOwner = viewLifecycleOwner + binding.vm = viewModel + + // This is called deviceAddress but contains the ID + deviceAddress = safeArgs.deviceAddress + viewModel.deviceAddress.postValue(deviceAddress) + + viewModel.isFirstScanCallback.postValue(true) + showSearchMessage() + + startBluetoothScan() + + val infoButton = binding.infoButton + infoButton.setOnClickListener { + val text = when (viewModel.connectionState.value as ConnectionState){ + ConnectionState.OVERMATURE_OFFLINE -> R.string.connection_state_overmature_offline_explanation + ConnectionState.CONNECTED -> R.string.connection_state_connected_explanation + ConnectionState.OFFLINE -> R.string.connection_state_offline_explanation + ConnectionState.PREMATURE_OFFLINE -> R.string.connection_state_premature_offline_explanation + ConnectionState.UNKNOWN -> R.string.connection_state_unknown_explanation + } + val duration = Toast.LENGTH_SHORT + + val toast = Toast.makeText(requireContext(), text, duration) // in Activity + toast.show() + } + + val batterySymbol = binding.batterySymbol + batterySymbol.setOnClickListener { + val text = when (viewModel.batteryState.value as BatteryState){ + BatteryState.FULL -> R.string.battery_full + BatteryState.MEDIUM -> R.string.battery_medium + BatteryState.VERY_LOW -> R.string.battery_very_low + BatteryState.LOW -> R.string.battery_low + else -> R.string.battery_unknown + } + val duration = Toast.LENGTH_SHORT + + val toast = Toast.makeText(requireContext(), text, duration) // in Activity + toast.show() + } + + return binding.root + } + + override fun onResume() { + super.onResume() + viewModel.isFirstScanCallback.postValue(true) + showSearchMessage() + startBluetoothScan() + } + + override fun onPause() { + super.onPause() + showSearchMessage() + stopBluetoothScan() + } + + override fun onDestroyView() { + super.onDestroyView() + stopBluetoothScan() + } + + companion object { + private const val SCAN_DURATION = 30_000L + } + +} \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanDistanceViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanDistanceViewModel.kt new file mode 100644 index 00000000..2cd6845b --- /dev/null +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanDistanceViewModel.kt @@ -0,0 +1,23 @@ +package de.seemoo.at_tracking_detection.ui.scan + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import de.seemoo.at_tracking_detection.database.models.device.BatteryState +import de.seemoo.at_tracking_detection.database.models.device.ConnectionState +import de.seemoo.at_tracking_detection.util.ble.BLEScanner + +class ScanDistanceViewModel: ViewModel() { + // var bluetoothRssi = MutableLiveData() + var deviceAddress = MutableLiveData() + var connectionStateString = MutableLiveData() + var connectionState = MutableLiveData() + var batteryStateString = MutableLiveData() + var batteryState = MutableLiveData() + var connectionQuality = MutableLiveData() + var isFirstScanCallback = MutableLiveData(true) + + var bluetoothEnabled = MutableLiveData(true) + init { + bluetoothEnabled.value = BLEScanner.isBluetoothOn() + } +} \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanFragment.kt index 86ae45d6..424d65e1 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanFragment.kt @@ -9,9 +9,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.TextView import androidx.databinding.DataBindingUtil import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import de.seemoo.at_tracking_detection.R @@ -25,7 +27,8 @@ class ScanFragment : Fragment() { private val scanViewModel: ScanViewModel by viewModels() override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, + inflater: LayoutInflater, + container: ViewGroup?, savedInstanceState: Bundle? ): View { val binding: FragmentScanBinding = @@ -41,6 +44,37 @@ class ScanFragment : Fragment() { // Ugly workaround because i don't know why this adapter only displays items after a screen wake up... bluetoothDeviceAdapter.notifyDataSetChanged() } + + scanViewModel.scanFinished.observe(viewLifecycleOwner) { + if (it) { + binding.buttonStartStopScan.setImageResource(R.drawable.ic_baseline_play_arrow_24) + } else { + binding.buttonStartStopScan.setImageResource(R.drawable.ic_baseline_stop_24) + } + } + + scanViewModel.sortingOrder.observe(viewLifecycleOwner) { + val bluetoothDeviceListValue = scanViewModel.bluetoothDeviceList.value ?: return@observe + scanViewModel.sortResults(bluetoothDeviceListValue) + scanViewModel.bluetoothDeviceList.postValue(bluetoothDeviceListValue) + + if (view != null) { + val sortBySignalStrength = requireView().findViewById(R.id.sort_option_signal_strength) + val sortByDetectionOrder = requireView().findViewById(R.id.sort_option_order_detection) + val sortByAddress = requireView().findViewById(R.id.sort_option_address) + + val sortOptions = listOf(sortBySignalStrength, sortByDetectionOrder, sortByAddress) + + when(it) { + SortingOrder.SIGNAL_STRENGTH -> scanViewModel.changeColorOf(sortOptions, sortBySignalStrength) + SortingOrder.DETECTION_ORDER -> scanViewModel.changeColorOf(sortOptions, sortByDetectionOrder) + SortingOrder.ADDRESS -> scanViewModel.changeColorOf(sortOptions, sortByAddress) + else -> scanViewModel.changeColorOf(sortOptions, sortBySignalStrength) + } + } + + } + return binding.root } @@ -53,6 +87,33 @@ class ScanFragment : Fragment() { context?.let { BLEScanner.openBluetoothSettings(it) } } + + val startStopButton = view.findViewById(R.id.button_start_stop_scan) + startStopButton.setOnClickListener { + if (scanViewModel.scanFinished.value == true) { + startBluetoothScan() + } else { + stopBluetoothScan() + } + } + + val sortBySignalStrength = view.findViewById(R.id.sort_option_signal_strength) + val sortByDetectionOrder = view.findViewById(R.id.sort_option_order_detection) + val sortByAddress = view.findViewById(R.id.sort_option_address) + + val sortOptions = listOf(sortBySignalStrength, sortByDetectionOrder, sortByAddress) + + scanViewModel.changeColorOf(sortOptions, sortBySignalStrength) + + sortBySignalStrength.setOnClickListener { + scanViewModel.sortingOrder.postValue(SortingOrder.SIGNAL_STRENGTH) + } + sortByDetectionOrder.setOnClickListener { + scanViewModel.sortingOrder.postValue(SortingOrder.DETECTION_ORDER) + } + sortByAddress.setOnClickListener { + scanViewModel.sortingOrder.postValue(SortingOrder.ADDRESS) + } } override fun onStart() { @@ -90,6 +151,7 @@ class ScanFragment : Fragment() { // Register the current fragment as a callback BLEScanner.registerCallback(this.scanCallback) + scanViewModel.scanFinished.postValue(false) // Show to the user that no devices have been found Handler(Looper.getMainLooper()).postDelayed({ @@ -105,11 +167,14 @@ class ScanFragment : Fragment() { // We just unregister the callback, but keep the scanner running // until the app is closed / moved to background BLEScanner.unregisterCallback(this.scanCallback) + scanViewModel.scanFinished.postValue(true) } override fun onResume() { super.onResume() - startBluetoothScan() + if (scanViewModel.scanFinished.value == false) { + startBluetoothScan() + } } override fun onPause() { diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanViewModel.kt index b7bb59e2..c676fbc9 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanViewModel.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/ScanViewModel.kt @@ -1,28 +1,31 @@ package de.seemoo.at_tracking_detection.ui.scan import android.bluetooth.le.ScanResult -import android.bluetooth.le.ScanSettings +import android.widget.TextView +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.map import dagger.hilt.android.lifecycle.HiltViewModel -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.DeviceManager -import de.seemoo.at_tracking_detection.database.models.device.types.SamsungDevice.Companion.getPublicKey +import de.seemoo.at_tracking_detection.database.models.device.BaseDevice.Companion.getPublicKey +import de.seemoo.at_tracking_detection.database.models.device.DeviceManager.getDeviceType +import de.seemoo.at_tracking_detection.database.models.device.DeviceType.Companion.getAllowedDeviceTypesFromSettings import de.seemoo.at_tracking_detection.database.repository.BeaconRepository import de.seemoo.at_tracking_detection.database.repository.ScanRepository import de.seemoo.at_tracking_detection.detection.LocationProvider import de.seemoo.at_tracking_detection.detection.ScanBluetoothWorker import de.seemoo.at_tracking_detection.detection.ScanBluetoothWorker.Companion.TIME_BETWEEN_BEACONS import de.seemoo.at_tracking_detection.util.SharedPrefs +import de.seemoo.at_tracking_detection.util.Utility import de.seemoo.at_tracking_detection.util.ble.BLEScanner import kotlinx.coroutines.MainScope import kotlinx.coroutines.async import timber.log.Timber import java.time.LocalDateTime -import java.time.temporal.ChronoUnit import javax.inject.Inject @HiltViewModel @@ -36,9 +39,11 @@ class ScanViewModel @Inject constructor( val scanFinished = MutableLiveData(false) + val sortingOrder = MutableLiveData(SortingOrder.SIGNAL_STRENGTH) + val scanStart = MutableLiveData(LocalDateTime.MIN) - var bluetoothEnabled = MutableLiveData(true) + var bluetoothEnabled = MutableLiveData(true) init { bluetoothDeviceList.value = ArrayList() bluetoothEnabled.value = BLEScanner.isBluetoothOn() @@ -49,6 +54,18 @@ class ScanViewModel @Inject constructor( } fun addScanResult(scanResult: ScanResult) { + if (scanFinished.value == true) { + return + } + + val deviceType = getDeviceType(scanResult) + val validDeviceTypes = getAllowedDeviceTypesFromSettings() + + if (deviceType !in validDeviceTypes) { + // If device not selected in settings then do not add ScanResult to list or database + return + } + val currentDate = LocalDateTime.now() val uniqueIdentifier = getPublicKey(scanResult) // either public key or MAC-Address if (beaconRepository.getNumberOfBeaconsAddress( @@ -88,7 +105,8 @@ class ScanViewModel @Inject constructor( } } - bluetoothDeviceListValue.sortByDescending { it.rssi } + sortResults(bluetoothDeviceListValue) + bluetoothDeviceList.postValue(bluetoothDeviceListValue) Timber.d("Adding scan result ${scanResult.device.address} with unique identifier $uniqueIdentifier") Timber.d( @@ -99,15 +117,32 @@ class ScanViewModel @Inject constructor( Timber.d("Device list: ${bluetoothDeviceList.value?.count()}") } - val isListEmpty: LiveData = bluetoothDeviceList.map { it.isEmpty() } + fun sortResults(bluetoothDeviceListValue: MutableList) { + when(sortingOrder.value) { + SortingOrder.SIGNAL_STRENGTH -> bluetoothDeviceListValue.sortByDescending { it.rssi } + SortingOrder.DETECTION_ORDER -> bluetoothDeviceListValue.sortByDescending { it.timestampNanos } + SortingOrder.ADDRESS -> bluetoothDeviceListValue.sortBy { it.device.address } + else -> bluetoothDeviceListValue.sortByDescending { it.rssi } + } + } - val listSize: LiveData = bluetoothDeviceList.map { it.size } + fun changeColorOf(sortOptions: List, sortOption: TextView) { + val theme = Utility.getSelectedTheme() + var color = Color.Gray + if (theme){ + color = Color.LightGray + } - suspend fun saveScanToRepository(){ - // Not used anymore, because manual scan is always when the app is open - if (scanStart.value == LocalDateTime.MIN) { return } - val duration: Int = ChronoUnit.SECONDS.between(scanStart.value, LocalDateTime.now()).toInt() - val scan = Scan(endDate = LocalDateTime.now(), bluetoothDeviceList.value?.size ?: 0, duration, isManual = true, scanMode = ScanSettings.SCAN_MODE_LOW_LATENCY, startDate = scanStart.value ?: LocalDateTime.now()) - scanRepository.insert(scan) + sortOptions.forEach { + if(it == sortOption) { + it.setBackgroundColor(color.toArgb()) + } else { + it.setBackgroundColor(Color.Transparent.toArgb()) + } + } } + + val isListEmpty: LiveData = bluetoothDeviceList.map { it.isEmpty() } + + val listSize: LiveData = bluetoothDeviceList.map { it.size } } \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/SortingOrder.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/SortingOrder.kt new file mode 100644 index 00000000..6b6bb82f --- /dev/null +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/SortingOrder.kt @@ -0,0 +1,9 @@ +package de.seemoo.at_tracking_detection.ui.scan + +enum class SortingOrder { + SIGNAL_STRENGTH, + DETECTION_ORDER, + NAME, + TYPE, + ADDRESS +} \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/dialog/PlaySoundDialogFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/dialog/PlaySoundDialogFragment.kt index 0035a763..54c5cc9f 100644 --- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/dialog/PlaySoundDialogFragment.kt +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/scan/dialog/PlaySoundDialogFragment.kt @@ -42,8 +42,7 @@ class PlaySoundDialogFragment constructor(scanResult: ScanResult) : BottomSheetD override fun onResume() { super.onResume() val gattServiceIntent = Intent(context, BluetoothLeService::class.java) - val activity = ATTrackingDetectionApplication.getCurrentActivity() - if (activity == null) {return} + val activity = ATTrackingDetectionApplication.getCurrentActivity() ?: return LocalBroadcastManager.getInstance(activity) .registerReceiver(gattUpdateReceiver, DeviceManager.gattIntentFilter) activity.bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE) diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/AttributionAdapter.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/AttributionAdapter.kt new file mode 100644 index 00000000..51cd5b08 --- /dev/null +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/AttributionAdapter.kt @@ -0,0 +1,35 @@ +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import de.seemoo.at_tracking_detection.R +import de.seemoo.at_tracking_detection.ui.settings.AttributionItem + +class AttributionAdapter( + private val attributions: List, + private val onItemClick: (AttributionItem) -> Unit +) : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AttributionViewHolder { + val view = + LayoutInflater.from(parent.context).inflate(R.layout.item_attribution, parent, false) + return AttributionViewHolder(view) + } + + override fun onBindViewHolder(holder: AttributionViewHolder, position: Int) { + val attribution = attributions[position] + holder.bind(attribution, onItemClick) + } + + override fun getItemCount(): Int = attributions.size + + class AttributionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + private val nameTextView: TextView = itemView.findViewById(R.id.textViewAttributionName) + + fun bind(attribution: AttributionItem, onItemClick: (AttributionItem) -> Unit) { + nameTextView.text = attribution.name + itemView.setOnClickListener { onItemClick(attribution) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/DataDeletionFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/DataDeletionFragment.kt new file mode 100644 index 00000000..d0f61a77 --- /dev/null +++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/DataDeletionFragment.kt @@ -0,0 +1,38 @@ +package de.seemoo.at_tracking_detection.ui.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.fragment.app.Fragment +import de.seemoo.at_tracking_detection.R + +class DataDeletionFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val rootView = inflater.inflate(R.layout.fragment_data_deletion, container, false) + + return rootView + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val sharedPreferences = requireActivity().getSharedPreferences("shared_preferences", 0) + val deletionButton = view.findViewById