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