diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 4179ac3c..63443d36 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -10,16 +10,34 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- - name: Set up JDK
- uses: actions/setup-java@v2
+ - name: Set up JDK 17 and enable Gradle cache
+ uses: actions/setup-java@v3
with:
- java-version: '11'
- distribution: 'adopt'
- cache: gradle
+ distribution: 'temurin'
+ java-version: '17'
+ cache: 'gradle'
+ cache-dependency-path: |
+ **/gradle.lockfile
+ **/gradle-wrapper.properties
+ **/build.gradle
+ **/build.gradle.kts
+ **/settings.gradle
+ **/settings.gradle.kts
- - name: Build app
- uses: gradle/gradle-command-action@v2
+ - name: Cache Gradle directories
+ uses: actions/cache@v4
with:
- arguments: build
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle-wrapper.properties', '**/build.gradle', '**/build.gradle.kts', '**/settings.gradle', '**/settings.gradle.kts') }}
+ restore-keys: |
+ ${{ runner.os }}-gradle-
+
+ - name: Grant execute permission for Gradle wrapper
+ run: chmod +x ./gradlew
+
+ - name: Build app
+ run: ./gradlew build --no-daemon
diff --git a/api.properties b/api.properties
index bb91fe29..400e273b 100644
--- a/api.properties
+++ b/api.properties
@@ -1,2 +1,2 @@
-API_KEY="3acPl2TP.lAyeGkWWlqPrgfWI9WbzqKKHejOmahJ3"
+API_KEY="VVZZfRjk.Ijo01HvEAbi7XpcNIrlQLyhnj0S3OLwh"
API_BASE_ADDRESS="https://tpe.seemoo.tu-darmstadt.de/api/"
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index 9f804f0a..d91ec9e3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -28,8 +28,8 @@ android {
applicationId "de.seemoo.at_tracking_detection"
minSdkVersion 28
targetSdk = 36
- versionCode 55
- versionName "2.6.1"
+ versionCode 56
+ versionName "2.6.2"
buildConfigField "String", "API_KEY", apiProperties["API_KEY"]
buildConfigField "String", "API_BASE_ADDRESS", apiProperties["API_BASE_ADDRESS"]
@@ -92,8 +92,8 @@ dependencies {
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.vectordrawable:vectordrawable:1.2.0'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.9.4'
- implementation 'androidx.navigation:navigation-ui-ktx:2.9.4'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.9.5'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.9.5'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.4'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
@@ -159,23 +159,23 @@ dependencies {
// Integration with activities
implementation 'androidx.activity:activity-compose:1.11.0'
// Compose Material3 Design
- implementation 'androidx.compose.material3:material3:1.3.2'
- implementation 'androidx.compose.material3:material3-window-size-class:1.3.2'
+ implementation 'androidx.compose.material3:material3:1.4.0'
+ implementation 'androidx.compose.material3:material3-window-size-class:1.4.0'
// Animations
- implementation 'androidx.compose.animation:animation:1.9.1'
+ implementation 'androidx.compose.animation:animation:1.9.2'
// Tooling support (Previews, etc.)
- implementation 'androidx.compose.ui:ui-tooling:1.9.1'
+ implementation 'androidx.compose.ui:ui-tooling:1.9.2'
// Integration with ViewModels
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4'
// UI Tests
- androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.9.1'
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.9.2'
// When using a MDC theme
implementation "com.google.android.material:compose-theme-adapter:1.2.1"
- // Google Review Handling
- implementation("com.google.android.play:review:2.0.2")
- implementation("com.google.android.play:review-ktx:2.0.2")
- implementation("com.google.android.gms:play-services-base:18.8.0")
+ // Google Review Handling (only active in Google Play Builds done from the googlePlyStore Branch)
+ // implementation("com.google.android.play:review:2.0.2")
+ // implementation("com.google.android.play:review-ktx:2.0.2")
+ // implementation("com.google.android.gms:play-services-base:18.9.0")
}
ksp {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3d4eaa83..4361d9f8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -11,6 +11,8 @@
+
+
Timber.e(throwable, "Uncaught exception on thread ${thread.name}")
}
// Initiate the permanent background scan
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && SharedPrefs.usePermanentBluetoothScanner) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && SharedPrefs.usePermanentBluetoothScanner) {
PermanentBluetoothScanner.scan()
}
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/BeaconDao.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/BeaconDao.kt
index 3e240275..aa630107 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/BeaconDao.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/daos/BeaconDao.kt
@@ -35,6 +35,9 @@ interface BeaconDao {
@Query("SELECT * FROM beacon WHERE deviceAddress LIKE :deviceAddress ORDER BY receivedAt DESC")
fun getDeviceBeacons(deviceAddress: String): List
+ @Query("SELECT * FROM beacon WHERE deviceAddress LIKE :deviceAddress ORDER BY receivedAt DESC")
+ fun observeDeviceBeacons(deviceAddress: String): Flow>
+
@Query("SELECT * FROM beacon WHERE deviceAddress LIKE :deviceAddress AND receivedAt >= :since ORDER BY receivedAt DESC")
fun getDeviceBeaconsSince(deviceAddress: String, since: LocalDateTime): List
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 eb019aa4..6e0b8cec 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
@@ -129,4 +129,7 @@ interface DeviceDao {
@Query("SELECT * FROM device WHERE deviceType = :deviceType AND lastSeen >= :since AND connectable = :connectableState LIMIT 1")
fun getDeviceWithConnectableStateSince(deviceType: String, since: LocalDateTime, connectableState: Boolean): BaseDevice?
+
+ @Query("SELECT * FROM device WHERE address LIKE :address LIMIT 1")
+ fun observeByAddress(address: String): Flow
}
\ 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 b778a14e..fa970f80 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
@@ -23,6 +23,7 @@ import de.seemoo.at_tracking_detection.database.models.device.types.SamsungTrack
import de.seemoo.at_tracking_detection.database.models.device.types.SamsungTrackerType
import de.seemoo.at_tracking_detection.database.models.device.types.Tile
import de.seemoo.at_tracking_detection.database.models.device.types.Unknown
+import de.seemoo.at_tracking_detection.ui.scan.ScanFragment
import de.seemoo.at_tracking_detection.ui.scan.ScanResultWrapper
import de.seemoo.at_tracking_detection.util.Utility
import de.seemoo.at_tracking_detection.util.converter.DateTimeConverter
@@ -129,8 +130,14 @@ data class BaseDevice(
} else if (deviceType == DeviceType.GOOGLE_FIND_MY_NETWORK && subDeviceType != "UNKNOWN") {
val subType = GoogleFindMyNetworkType.stringToSubType(subDeviceType)
AppCompatResources.getDrawable(ATTrackingDetectionApplication.getAppContext(), GoogleFindMyNetworkType.drawableForSubType(subType, name))
- }
- else {
+ } else if (ScanFragment.samsungSubDeviceTypeMap.containsKey(uniqueId)) {
+ val subType = ScanFragment.samsungSubDeviceTypeMap[uniqueId]!!
+ AppCompatResources.getDrawable(ATTrackingDetectionApplication.getAppContext(), SamsungTrackerType.drawableForSubType(subType))
+ } else if (ScanFragment.googleSubDeviceTypeMap.containsKey(uniqueId)) {
+ val subType = ScanFragment.googleSubDeviceTypeMap[uniqueId]!!
+ val deviceNameFromCache = ScanFragment.deviceNameMap[uniqueId]
+ AppCompatResources.getDrawable(ATTrackingDetectionApplication.getAppContext(), GoogleFindMyNetworkType.drawableForSubType(subType, deviceNameFromCache))
+ } else {
device.getDrawable()
}
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 d6f28205..6ddd7b3b 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
@@ -2,6 +2,7 @@ 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.ui.scan.ScanFragment
import de.seemoo.at_tracking_detection.ui.scan.ScanResultWrapper
import de.seemoo.at_tracking_detection.util.SharedPrefs
@@ -35,23 +36,6 @@ enum class DeviceType {
}
}
- fun userReadableName(wrappedScanResult: ScanResultWrapper): String {
- val deviceType: DeviceType = wrappedScanResult.deviceType
- return when (deviceType) {
- UNKNOWN -> Unknown.defaultDeviceName
- AIRPODS -> AirPods.defaultDeviceName
- AIRTAG -> AirTag.defaultDeviceName
- APPLE -> AppleDevice.defaultDeviceName
- FIND_MY -> AppleFindMy.defaultDeviceName
- TILE -> Tile.defaultDeviceName
- CHIPOLO -> Chipolo.defaultDeviceName
- PEBBLEBEE -> PebbleBee.defaultDeviceName
- SAMSUNG_TRACKER -> SamsungTracker.defaultDeviceName
- SAMSUNG_FIND_MY_MOBILE -> SamsungFindMyMobile.defaultDeviceName
- GOOGLE_FIND_MY_NETWORK -> GoogleFindMyNetwork.defaultDeviceName
- }
- }
-
fun getImageDrawable(wrappedScanResult: ScanResultWrapper): Int {
val deviceType: DeviceType = wrappedScanResult.deviceType
return when (deviceType) {
@@ -63,9 +47,28 @@ enum class DeviceType {
TILE -> R.drawable.ic_tile
CHIPOLO -> R.drawable.ic_chipolo
PEBBLEBEE -> R.drawable.ic_pebblebee_clip
- SAMSUNG_TRACKER -> R.drawable.ic_smarttag_icon
+ SAMSUNG_TRACKER -> getSamsungDrawable(wrappedScanResult)
SAMSUNG_FIND_MY_MOBILE -> R.drawable.ic_baseline_device_unknown_24
- GOOGLE_FIND_MY_NETWORK -> R.drawable.ic_chipolo
+ GOOGLE_FIND_MY_NETWORK -> getGoogleDrawable(wrappedScanResult)
+ }
+ }
+
+ private fun getSamsungDrawable(wrappedScanResult: ScanResultWrapper): Int {
+ return if (ScanFragment.samsungSubDeviceTypeMap.containsKey(wrappedScanResult.uniqueIdentifier)) {
+ val subType = ScanFragment.samsungSubDeviceTypeMap[wrappedScanResult.uniqueIdentifier]!!
+ SamsungTrackerType.drawableForSubType(subType)
+ } else {
+ R.drawable.ic_smarttag_icon
+ }
+ }
+
+ private fun getGoogleDrawable(wrappedScanResult: ScanResultWrapper): Int {
+ return if (ScanFragment.googleSubDeviceTypeMap.containsKey(wrappedScanResult.uniqueIdentifier)) {
+ val subType = ScanFragment.googleSubDeviceTypeMap[wrappedScanResult.uniqueIdentifier]!!
+ val deviceNameFromCache = ScanFragment.deviceNameMap[wrappedScanResult.uniqueIdentifier]
+ GoogleFindMyNetworkType.drawableForSubType(subType, deviceNameFromCache)
+ } else {
+ R.drawable.ic_chipolo
}
}
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/BeaconRepository.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/BeaconRepository.kt
index ebc6efeb..e6d570c8 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/BeaconRepository.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/BeaconRepository.kt
@@ -37,6 +37,9 @@ class BeaconRepository @Inject constructor(
fun getDeviceBeacons(deviceAddress: String): List =
beaconDao.getDeviceBeacons(deviceAddress)
+ fun getDeviceBeaconsFlow(deviceAddress: String): Flow> =
+ beaconDao.observeDeviceBeacons(deviceAddress)
+
fun getDeviceBeaconsSince(deviceAddress: String, since: LocalDateTime): List =
beaconDao.getDeviceBeaconsSince(deviceAddress, since)
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 b616c4ba..a418b75d 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
@@ -47,6 +47,8 @@ class DeviceRepository @Inject constructor(private val deviceDao: DeviceDao) {
fun getDevice(deviceAddress: String): BaseDevice? = deviceDao.getByAddress(deviceAddress)
+ fun observeDevice(deviceAddress: String): Flow = deviceDao.observeByAddress(deviceAddress)
+
val countNotTracking = deviceDao.getCountNotTracking(RiskLevelEvaluator.relevantTrackingDateForRiskCalculation)
val countIgnored = deviceDao.getCountIgnored()
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 56da9818..ac6aab14 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
@@ -9,6 +9,8 @@ import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import com.github.appintro.AppIntro
import com.github.appintro.AppIntroFragment
@@ -37,6 +39,15 @@ class OnboardingActivity : AppIntro() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
+ MainActivity.configureSystemBars(this, edgeToEdge = true, applyRootPadding = true)
+
+ try {
+ WindowCompat.getInsetsController(window, window.decorView).show(WindowInsetsCompat.Type.systemBars())
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to disable immersive mode or show system bars")
+ }
+
permission = intent.getStringExtra("permission")
Timber.d("Onboarding started with: $permission")
if (permission != null) {
@@ -52,6 +63,15 @@ class OnboardingActivity : AppIntro() {
}
}
+ override fun onResume() {
+ super.onResume()
+ try {
+ WindowCompat.getInsetsController(window, window.decorView).show(WindowInsetsCompat.Type.systemBars())
+ } catch (e: Exception) {
+ Timber.w(e, "Failed to disable immersive mode or show system bars in onResume")
+ }
+ }
+
override fun onDonePressed(currentFragment: Fragment?) {
//Checks which permissions have given to store the default value for location access
val locationPermissionState =
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt
index 740d9caf..251c43a9 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt
@@ -38,8 +38,9 @@ class DashboardRiskFragment : Fragment() {
private var _binding: FragmentDashboardRiskBinding? = null
private val binding get() = _binding!!
- @Inject
- lateinit var reviewController: ReviewController
+ // Google Play Review Controller: Only active in Google Play builds
+ // @Inject
+ // lateinit var reviewController: ReviewController
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
@@ -62,8 +63,9 @@ class DashboardRiskFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ // Google Play Review Controller: Only active in Google Play builds
// Increment app open count
- reviewController.incrementAppOpenCount()
+ // reviewController.incrementAppOpenCount()
val riskCard: MaterialCardView = view.findViewById(R.id.risk_card)
riskCard.setOnClickListener {
@@ -204,19 +206,21 @@ class DashboardRiskFragment : Fragment() {
}
}
+ // Google Play Review Controller: Only active in Google Play builds
// Check if we should show review after data is loaded
- checkAndShowReview()
+ // checkAndShowReview()
}
- private fun checkAndShowReview() {
- Timber.d("Checking if review should be shown")
- if (BuildConfig.DEBUG) {
- reviewController.debugReviewStatus()
- }
- reviewController.requestReviewDialog(requireActivity()) {
- Timber.d("Review dialog request completed")
- }
- }
+ // Google Play Review Controller: Only active in Google Play builds
+// private fun checkAndShowReview() {
+// Timber.d("Checking if review should be shown")
+// if (BuildConfig.DEBUG) {
+// reviewController.debugReviewStatus()
+// }
+// reviewController.requestReviewDialog(requireActivity()) {
+// Timber.d("Review dialog request completed")
+// }
+// }
override fun onStart() {
super.onStart()
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ReviewController.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ReviewController.kt
index b37c9c0e..7b0508ac 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ReviewController.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ReviewController.kt
@@ -1,159 +1,160 @@
package de.seemoo.at_tracking_detection.ui.dashboard
-import android.content.Context
-import androidx.fragment.app.FragmentActivity
-import com.google.android.gms.common.ConnectionResult
-import com.google.android.gms.common.GoogleApiAvailability
-import com.google.android.play.core.review.ReviewInfo
-import com.google.android.play.core.review.ReviewManager
-import com.google.android.play.core.review.ReviewManagerFactory
-import de.seemoo.at_tracking_detection.util.SharedPrefs
-import timber.log.Timber
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class ReviewController @Inject constructor(
- private val context: Context
-) {
- private var reviewManager: ReviewManager? = null
- private var reviewInfo: ReviewInfo? = null
- private var isReviewFlowReady = false
- private var pendingReviewRequest: (() -> Unit)? = null
-
- companion object {
- private const val REVIEW_THRESHOLD = 20
- }
-
- init {
- if (isGooglePlayServicesAvailable()) {
- reviewManager = ReviewManagerFactory.create(context)
- prepareReviewFlow()
- }
- }
-
- private fun isGooglePlayServicesAvailable(): Boolean {
- val googleApiAvailability = GoogleApiAvailability.getInstance()
- val connectionResult = googleApiAvailability.isGooglePlayServicesAvailable(context)
- return connectionResult == ConnectionResult.SUCCESS
- }
-
- fun incrementAppOpenCount() {
- val currentCount: Int = SharedPrefs.appOpenCount
- SharedPrefs.appOpenCount = currentCount + 1
- Timber.d("App opened ${currentCount + 1} times")
- }
-
- private fun prepareReviewFlow() {
- reviewManager?.let { manager ->
- val request = manager.requestReviewFlow()
- request.addOnCompleteListener { task ->
- if (task.isSuccessful) {
- reviewInfo = task.result
- isReviewFlowReady = true
- Timber.d("Review flow prepared successfully")
-
- // Execute pending review request if any
- pendingReviewRequest?.invoke()
- pendingReviewRequest = null
- } else {
- Timber.w("Failed to prepare review flow: ${task.exception}")
- isReviewFlowReady = false
- }
- }
- }
- }
-
- fun shouldShowReview(): Boolean {
- if (!isGooglePlayServicesAvailable()) {
- Timber.d("Google Play Services not available")
- return false
- }
-
- val appOpenCount: Int = SharedPrefs.appOpenCount
- val reviewShown: Boolean = SharedPrefs.reviewShown
-
- Timber.d("Review check - appOpenCount: $appOpenCount, reviewShown: $reviewShown, reviewFlowReady: $isReviewFlowReady, reviewInfo: ${reviewInfo != null}")
-
- return appOpenCount >= REVIEW_THRESHOLD && !reviewShown && isReviewFlowReady && reviewInfo != null
- }
-
- fun requestReviewDialog(activity: FragmentActivity, onComplete: () -> Unit = {}) {
- if (!isGooglePlayServicesAvailable()) {
- Timber.d("Google Play Services not available, skipping review")
- onComplete()
- return
- }
-
- val appOpenCount: Int = SharedPrefs.appOpenCount
- val reviewShown: Boolean = SharedPrefs.reviewShown
-
- if (appOpenCount < REVIEW_THRESHOLD || reviewShown) {
- Timber.d("Review conditions not met - count: $appOpenCount, shown: $reviewShown")
- onComplete()
- return
- }
-
- if (isReviewFlowReady && reviewInfo != null) {
- showReviewDialog(activity, onComplete)
- } else {
- Timber.d("Review flow not ready yet, queuing request")
- pendingReviewRequest = {
- if (shouldShowReview()) {
- showReviewDialog(activity, onComplete)
- } else {
- onComplete()
- }
- }
- }
- }
-
- private fun showReviewDialog(activity: FragmentActivity, onComplete: () -> Unit = {}) {
- reviewManager?.let { manager ->
- reviewInfo?.let { info ->
- Timber.d("Launching review flow")
- val flow = manager.launchReviewFlow(activity, info)
- flow.addOnCompleteListener { task ->
- if (task.isSuccessful) {
- Timber.d("Review flow completed successfully")
- markReviewAsShown()
- } else {
- Timber.w("Review flow failed: ${task.exception}")
- }
- onComplete()
- }
- } ?: run {
- Timber.w("ReviewInfo is null, cannot show review")
- onComplete()
- }
- } ?: run {
- Timber.w("ReviewManager is null, cannot show review")
- onComplete()
- }
- }
-
- private fun markReviewAsShown() {
- SharedPrefs.reviewShown = true
- Timber.d("Review marked as shown")
- }
-
- fun getAppOpenCount(): Int {
- return SharedPrefs.appOpenCount
- }
-
- fun resetReviewStatus() {
- SharedPrefs.reviewShown = false
- Timber.d("Review status reset")
- }
-
- fun debugReviewStatus() {
- Timber.d("=== Review Debug Info ===")
- Timber.d("Google Play Services available: ${isGooglePlayServicesAvailable()}")
- Timber.d("App open count: ${SharedPrefs.appOpenCount}")
- Timber.d("Review shown: ${SharedPrefs.reviewShown}")
- Timber.d("Review flow ready: $isReviewFlowReady")
- Timber.d("ReviewInfo available: ${reviewInfo != null}")
- Timber.d("Should show review: ${shouldShowReview()}")
- Timber.d("========================")
- }
-}
\ No newline at end of file
+// Google Play Review Controller: Only active in Google Play builds
+//import android.content.Context
+//import androidx.fragment.app.FragmentActivity
+//import com.google.android.gms.common.ConnectionResult
+//import com.google.android.gms.common.GoogleApiAvailability
+//import com.google.android.play.core.review.ReviewInfo
+//import com.google.android.play.core.review.ReviewManager
+//import com.google.android.play.core.review.ReviewManagerFactory
+//import de.seemoo.at_tracking_detection.util.SharedPrefs
+//import timber.log.Timber
+//import javax.inject.Inject
+//import javax.inject.Singleton
+//
+//@Singleton
+//class ReviewController @Inject constructor(
+// private val context: Context
+//) {
+// private var reviewManager: ReviewManager? = null
+// private var reviewInfo: ReviewInfo? = null
+// private var isReviewFlowReady = false
+// private var pendingReviewRequest: (() -> Unit)? = null
+//
+// companion object {
+// private const val REVIEW_THRESHOLD = 20
+// }
+//
+// init {
+// if (isGooglePlayServicesAvailable()) {
+// reviewManager = ReviewManagerFactory.create(context)
+// prepareReviewFlow()
+// }
+// }
+//
+// private fun isGooglePlayServicesAvailable(): Boolean {
+// val googleApiAvailability = GoogleApiAvailability.getInstance()
+// val connectionResult = googleApiAvailability.isGooglePlayServicesAvailable(context)
+// return connectionResult == ConnectionResult.SUCCESS
+// }
+//
+// fun incrementAppOpenCount() {
+// val currentCount: Int = SharedPrefs.appOpenCount
+// SharedPrefs.appOpenCount = currentCount + 1
+// Timber.d("App opened ${currentCount + 1} times")
+// }
+//
+// private fun prepareReviewFlow() {
+// reviewManager?.let { manager ->
+// val request = manager.requestReviewFlow()
+// request.addOnCompleteListener { task ->
+// if (task.isSuccessful) {
+// reviewInfo = task.result
+// isReviewFlowReady = true
+// Timber.d("Review flow prepared successfully")
+//
+// // Execute pending review request if any
+// pendingReviewRequest?.invoke()
+// pendingReviewRequest = null
+// } else {
+// Timber.w("Failed to prepare review flow: ${task.exception}")
+// isReviewFlowReady = false
+// }
+// }
+// }
+// }
+//
+// fun shouldShowReview(): Boolean {
+// if (!isGooglePlayServicesAvailable()) {
+// Timber.d("Google Play Services not available")
+// return false
+// }
+//
+// val appOpenCount: Int = SharedPrefs.appOpenCount
+// val reviewShown: Boolean = SharedPrefs.reviewShown
+//
+// Timber.d("Review check - appOpenCount: $appOpenCount, reviewShown: $reviewShown, reviewFlowReady: $isReviewFlowReady, reviewInfo: ${reviewInfo != null}")
+//
+// return appOpenCount >= REVIEW_THRESHOLD && !reviewShown && isReviewFlowReady && reviewInfo != null
+// }
+//
+// fun requestReviewDialog(activity: FragmentActivity, onComplete: () -> Unit = {}) {
+// if (!isGooglePlayServicesAvailable()) {
+// Timber.d("Google Play Services not available, skipping review")
+// onComplete()
+// return
+// }
+//
+// val appOpenCount: Int = SharedPrefs.appOpenCount
+// val reviewShown: Boolean = SharedPrefs.reviewShown
+//
+// if (appOpenCount < REVIEW_THRESHOLD || reviewShown) {
+// Timber.d("Review conditions not met - count: $appOpenCount, shown: $reviewShown")
+// onComplete()
+// return
+// }
+//
+// if (isReviewFlowReady && reviewInfo != null) {
+// showReviewDialog(activity, onComplete)
+// } else {
+// Timber.d("Review flow not ready yet, queuing request")
+// pendingReviewRequest = {
+// if (shouldShowReview()) {
+// showReviewDialog(activity, onComplete)
+// } else {
+// onComplete()
+// }
+// }
+// }
+// }
+//
+// private fun showReviewDialog(activity: FragmentActivity, onComplete: () -> Unit = {}) {
+// reviewManager?.let { manager ->
+// reviewInfo?.let { info ->
+// Timber.d("Launching review flow")
+// val flow = manager.launchReviewFlow(activity, info)
+// flow.addOnCompleteListener { task ->
+// if (task.isSuccessful) {
+// Timber.d("Review flow completed successfully")
+// markReviewAsShown()
+// } else {
+// Timber.w("Review flow failed: ${task.exception}")
+// }
+// onComplete()
+// }
+// } ?: run {
+// Timber.w("ReviewInfo is null, cannot show review")
+// onComplete()
+// }
+// } ?: run {
+// Timber.w("ReviewManager is null, cannot show review")
+// onComplete()
+// }
+// }
+//
+// private fun markReviewAsShown() {
+// SharedPrefs.reviewShown = true
+// Timber.d("Review marked as shown")
+// }
+//
+// fun getAppOpenCount(): Int {
+// return SharedPrefs.appOpenCount
+// }
+//
+// fun resetReviewStatus() {
+// SharedPrefs.reviewShown = false
+// Timber.d("Review status reset")
+// }
+//
+// fun debugReviewStatus() {
+// Timber.d("=== Review Debug Info ===")
+// Timber.d("Google Play Services available: ${isGooglePlayServicesAvailable()}")
+// Timber.d("App open count: ${SharedPrefs.appOpenCount}")
+// Timber.d("Review shown: ${SharedPrefs.reviewShown}")
+// Timber.d("Review flow ready: $isReviewFlowReady")
+// Timber.d("ReviewInfo available: ${reviewInfo != null}")
+// Timber.d("Should show review: ${shouldShowReview()}")
+// Timber.d("========================")
+// }
+//}
\ No newline at end of file
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ReviewModule.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ReviewModule.kt
index 88732e56..9ccffae4 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ReviewModule.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ReviewModule.kt
@@ -1,20 +1,21 @@
package de.seemoo.at_tracking_detection.ui.dashboard
-import android.content.Context
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
-
-@Module
-@InstallIn(SingletonComponent::class)
-object ReviewModule {
-
- @Provides
- @Singleton
- fun provideReviewController(@ApplicationContext context: Context): ReviewController {
- return ReviewController(context)
- }
-}
\ No newline at end of file
+// Google Play Review Controller: Only active in Google Play builds
+//import android.content.Context
+//import dagger.Module
+//import dagger.Provides
+//import dagger.hilt.InstallIn
+//import dagger.hilt.android.qualifiers.ApplicationContext
+//import dagger.hilt.components.SingletonComponent
+//import javax.inject.Singleton
+//
+//@Module
+//@InstallIn(SingletonComponent::class)
+//object ReviewModule {
+//
+// @Provides
+// @Singleton
+// fun provideReviewController(@ApplicationContext context: Context): ReviewController {
+// return ReviewController(context)
+// }
+//}
\ No newline at end of file
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
index 90cd2b06..96f1186d 100644
--- 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
@@ -123,8 +123,8 @@ class ScanDistanceFragment : Fragment() {
viewModel.displayName.postValue(GoogleFindMyNetworkType.visibleStringFromSubtype(googleSubType))
} else {
Timber.d("Display Name - Default")
- binding.deviceTypeText.text = DeviceType.userReadableName(
- latestWrappedScanResult!!
+ binding.deviceTypeText.text = DeviceType.userReadableNameDefault(
+ latestWrappedScanResult!!.deviceType
)
}
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 e1929bf0..018e147b 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
@@ -115,8 +115,42 @@ class ScanFragment : Fragment() {
}
private fun toggleInfoLayoutVisibility(view: View) {
- view.findViewById(R.id.info_layout).apply {
- visibility = if (isVisible) View.GONE else View.VISIBLE
+ val infoLayout = view.findViewById(R.id.info_layout)
+
+ val duration = 200L
+ val density = view.context.resources.displayMetrics.density
+ val slidePx = (-10 * density)
+
+ if (infoLayout.isVisible) {
+ infoLayout.animate()
+ .alpha(0f)
+ .scaleX(0.95f)
+ .scaleY(0.95f)
+ .translationY(slidePx)
+ .setDuration(duration)
+ .withEndAction {
+ infoLayout.visibility = View.GONE
+ // reset properties for next show
+ infoLayout.alpha = 1f
+ infoLayout.scaleX = 1f
+ infoLayout.scaleY = 1f
+ infoLayout.translationY = 0f
+ }
+ .start()
+ } else {
+ infoLayout.alpha = 0f
+ infoLayout.scaleX = 0.95f
+ infoLayout.scaleY = 0.95f
+ infoLayout.translationY = slidePx
+ infoLayout.visibility = View.VISIBLE
+
+ infoLayout.animate()
+ .alpha(1f)
+ .scaleX(1f)
+ .scaleY(1f)
+ .translationY(0f)
+ .setDuration(duration)
+ .start()
}
}
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/SettingsFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/SettingsFragment.kt
index fc69ea7a..f0a3f7d4 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/SettingsFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/SettingsFragment.kt
@@ -56,12 +56,24 @@ class SettingsFragment : PreferenceFragmentCompat() {
entries.add(getString(R.string.samsung_find_my_mobile_name))
entryValues.add("samsung_find_my_mobile")
}
+
+ if (!entryValues.contains("apple_devices")) {
+ entries.add(getString(R.string.apple_device))
+ entryValues.add("apple_devices")
+ }
} else {
// Remove samsung_find_my_mobile if present
- val index = entryValues.indexOf("samsung_find_my_mobile")
- if (index != -1) {
- entries.removeAt(index)
- entryValues.removeAt(index)
+ val samsungIndex = entryValues.indexOf("samsung_find_my_mobile")
+ if (samsungIndex != -1) {
+ entries.removeAt(samsungIndex)
+ entryValues.removeAt(samsungIndex)
+ }
+
+ // Remove apple_devices if present
+ val appleIndex = entryValues.indexOf("apple_devices")
+ if (appleIndex != -1) {
+ entries.removeAt(appleIndex)
+ entryValues.removeAt(appleIndex)
}
}
@@ -197,7 +209,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
"use_permanent_bluetooth_scanner" -> {
if (SharedPrefs.usePermanentBluetoothScanner) {
Timber.d("Enabled permanent bluetooth scanner!")
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
try {
GlobalScope.launch(kotlinx.coroutines.Dispatchers.IO) {
PermanentBluetoothScanner.scan()
@@ -224,7 +236,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
if (SharedPrefs.advancedMode) {
Timber.d("Enabled advanced mode!")
findPreference("use_location")?.isVisible = true
- findPreference("use_permanent_bluetooth_scanner")?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+ findPreference("use_permanent_bluetooth_scanner")?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
findPreference("use_low_power_ble")?.isVisible = true
findPreference("notification_priority_high")?.isVisible = true
findPreference("show_onboarding")?.isVisible = true
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingViewModel.kt
index 3ac69fa5..4566424d 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingViewModel.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingViewModel.kt
@@ -13,12 +13,14 @@ 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.SharedPrefs
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import javax.inject.Inject
+import androidx.core.net.toUri
class TrackingViewModel @Inject constructor(
private val notificationRepository: NotificationRepository,
@@ -54,8 +56,10 @@ class TrackingViewModel @Inject constructor(
val isMapLoading = MutableLiveData(false)
- val markerLocations: LiveData> = deviceAddress.map {
- beaconRepository.getDeviceBeacons(it)
+ // Reactively update markers when beacons are written
+ // This is relevant for the case when a user very quickly opens the map from the manual scan while the device and beacons are still beeing written
+ val markerLocations: LiveData> = deviceAddress.switchMap { address ->
+ beaconRepository.getDeviceBeaconsFlow(address).asLiveData()
}
val amountBeacons: LiveData = markerLocations.map {
@@ -71,52 +75,45 @@ class TrackingViewModel @Inject constructor(
val deviceComment = MutableLiveData("")
- fun loadDevice(address: String, deviceTypeOverride: DeviceType) =
- deviceRepository.getDevice(address).also { device ->
- this.device.postValue(device)
- deviceType.value = deviceTypeOverride
-
- Timber.d("Set Device type: ${deviceType.value}")
-
- if (device != null) {
- deviceType.value = device.device.deviceContext.deviceType // This line is still necessary for the Device List in Expert Mode
- val deviceObserved = device.nextObservationNotification != null && device.nextObservationNotification!!.isAfter(
- LocalDateTime.now())
- trackerObserved.postValue(deviceObserved)
- deviceIgnored.postValue(device.ignore)
- noLocationsYet.postValue(false)
- connectable.postValue(device.device is Connectable)
- canBeIgnored.postValue(deviceType.value!!.canBeIgnored(ConnectionState.OVERMATURE_OFFLINE))
- val notification = notificationRepository.notificationForDevice(device).firstOrNull()
- notification?.let { notificationId.postValue(it.notificationId) }
- falseAlarm.postValue(notification?.falseAlarm ?: false)
-
- // Load last seen times
- viewModelScope.launch {
- loadLastSeenTimes(device)
+ fun loadDevice(address: String, deviceTypeOverride: DeviceType) {
+ deviceAddress.postValue(address)
+ deviceType.postValue(deviceTypeOverride)
+
+ viewModelScope.launch {
+ deviceRepository.observeDevice(address).collectLatest { dev ->
+ this@TrackingViewModel.device.postValue(dev)
+
+ if (dev != null) {
+ deviceType.value = dev.device.deviceContext.deviceType
+ val deviceObserved = dev.nextObservationNotification != null && dev.nextObservationNotification!!.isAfter(
+ LocalDateTime.now())
+ trackerObserved.postValue(deviceObserved)
+ deviceIgnored.postValue(dev.ignore)
+ noLocationsYet.postValue(false)
+ connectable.postValue(dev.device is Connectable)
+ canBeIgnored.postValue(deviceType.value!!.canBeIgnored(ConnectionState.OVERMATURE_OFFLINE))
+ val notification = notificationRepository.notificationForDevice(dev).firstOrNull()
+ notification?.let { notificationId.postValue(it.notificationId) }
+ falseAlarm.postValue(notification?.falseAlarm ?: false)
+
+ // Update last seen times based on current beacons
+ val beacons = beaconRepository.getDeviceBeacons(dev.address)
+ val lastSeenList = beacons.sortedByDescending { it.receivedAt }.take(5).map { beacon ->
+ DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).format(beacon.receivedAt)
+ }
+ lastSeenTimes.postValue(lastSeenList)
+
expertMode.postValue(SharedPrefs.advancedMode)
+ deviceComment.postValue(dev.comment ?: "")
+ } else {
+ noLocationsYet.postValue(true)
+ deviceComment.postValue("")
}
- deviceComment.postValue(device.comment ?: "")
- } else {
- noLocationsYet.postValue(true)
- deviceComment.postValue("")
- }
- showNfcHint.postValue(deviceType.value == DeviceType.AIRTAG)
- if (deviceType.value != null) {
- val websiteURL = DeviceManager.getWebsiteURL(deviceType.value!!)
- manufacturerWebsiteUrl.postValue(websiteURL)
- } else {
- manufacturerWebsiteUrl.postValue("")
+ showNfcHint.postValue(deviceType.value == DeviceType.AIRTAG)
+ manufacturerWebsiteUrl.postValue(DeviceManager.getWebsiteURL(deviceType.value!!))
}
}
-
- private fun loadLastSeenTimes(baseDevice: BaseDevice) {
- val beacons = beaconRepository.getDeviceBeacons(baseDevice.address)
- val lastSeenList = beacons.sortedByDescending { it.receivedAt }.take(5).map { beacon ->
- DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).format(beacon.receivedAt)
- }
- lastSeenTimes.postValue(lastSeenList)
}
fun toggleIgnoreDevice() {
@@ -143,13 +140,12 @@ class TrackingViewModel @Inject constructor(
fun clickOnWebsite(context: android.content.Context) {
if (manufacturerWebsiteUrl.value != null) {
Timber.d("Click on website: ${manufacturerWebsiteUrl.value}")
- val webpage: Uri = Uri.parse(manufacturerWebsiteUrl.value)
+ val webpage: Uri = manufacturerWebsiteUrl.value!!.toUri()
val intent = Intent(Intent.ACTION_VIEW, webpage)
context.startActivity(intent)
}
}
- // Add function to update comment and save to DB
fun updateDeviceComment(newComment: String) {
device.value?.let { baseDevice ->
if (baseDevice.comment != newComment) {
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/BindingAdapter.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/BindingAdapter.kt
index b00d2b4b..856ba64b 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/BindingAdapter.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/BindingAdapter.kt
@@ -9,7 +9,6 @@ import androidx.recyclerview.widget.RecyclerView
import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
import de.seemoo.at_tracking_detection.R
import de.seemoo.at_tracking_detection.database.models.device.DeviceType
-import de.seemoo.at_tracking_detection.database.models.device.types.GoogleFindMyNetwork
import de.seemoo.at_tracking_detection.database.models.device.types.GoogleFindMyNetworkType
import de.seemoo.at_tracking_detection.database.models.device.types.SamsungTrackerType
import de.seemoo.at_tracking_detection.ui.scan.ScanFragment
@@ -51,26 +50,11 @@ fun setDeviceDrawable(imageView: ImageView, wrappedScanResult: ScanResultWrapper
val deviceRepository = ATTrackingDetectionApplication.getCurrentApp().deviceRepository
val deviceFromDb = deviceRepository.getDevice(wrappedScanResult.uniqueIdentifier)
- val drawableResId = if (deviceFromDb != null && deviceFromDb.subDeviceType != "UNKNOWN" && deviceFromDb.deviceType == DeviceType.SAMSUNG_TRACKER) {
- val subTypeString = deviceFromDb.subDeviceType
- val subType = SamsungTrackerType.stringToSubType(subTypeString)
- SamsungTrackerType.drawableForSubType(subType)
- } else if (deviceFromDb != null && deviceFromDb.deviceType == DeviceType.GOOGLE_FIND_MY_NETWORK) {
- val subTypeString = deviceFromDb.subDeviceType
- val subType = GoogleFindMyNetworkType.stringToSubType(subTypeString)
- GoogleFindMyNetworkType.drawableForSubType(subType, deviceFromDb.name)
- } else if (ScanFragment.samsungSubDeviceTypeMap.containsKey(wrappedScanResult.uniqueIdentifier)) {
- val subType = ScanFragment.samsungSubDeviceTypeMap[wrappedScanResult.uniqueIdentifier]!!
- SamsungTrackerType.drawableForSubType(subType)
- } else if (ScanFragment.googleSubDeviceTypeMap.containsKey(wrappedScanResult.uniqueIdentifier)) {
- val subType = ScanFragment.googleSubDeviceTypeMap[wrappedScanResult.uniqueIdentifier]!!
- val deviceNameFromCache = ScanFragment.deviceNameMap[wrappedScanResult.uniqueIdentifier]
- GoogleFindMyNetworkType.drawableForSubType(subType, deviceNameFromCache)
+ val drawable = if (deviceFromDb != null) {
+ deviceFromDb.getDrawable()
} else {
- DeviceType.getImageDrawable(wrappedScanResult)
+ DeviceType.getImageDrawable(wrappedScanResult).let { ContextCompat.getDrawable(imageView.context, it) }
}
-
- val drawable = ContextCompat.getDrawable(imageView.context, drawableResId)
imageView.setImageDrawable(drawable)
}
@@ -79,28 +63,41 @@ fun setDeviceName(textView: TextView, wrappedScanResult: ScanResultWrapper) {
val deviceRepository = ATTrackingDetectionApplication.getCurrentApp().deviceRepository
val deviceFromDb = deviceRepository.getDevice(wrappedScanResult.uniqueIdentifier)
- if (deviceFromDb?.name != null) {
- textView.text = deviceFromDb.getDeviceNameWithID()
- } else if (deviceFromDb != null && deviceFromDb.subDeviceType != "UNKNOWN" && deviceFromDb.deviceType == DeviceType.SAMSUNG_TRACKER) {
- val subTypeString = deviceFromDb.subDeviceType
- val subType = SamsungTrackerType.stringToSubType(subTypeString)
- ScanFragment.samsungSubDeviceTypeMap[wrappedScanResult.uniqueIdentifier] = subType
- textView.text = SamsungTrackerType.visibleStringFromSubtype(subType)
- } else if (deviceFromDb != null && deviceFromDb.deviceType == DeviceType.GOOGLE_FIND_MY_NETWORK) {
- val subTypeString = deviceFromDb.subDeviceType
- val subType = GoogleFindMyNetworkType.stringToSubType(subTypeString)
- ScanFragment.googleSubDeviceTypeMap[wrappedScanResult.uniqueIdentifier] = subType
- textView.text = GoogleFindMyNetworkType.visibleStringFromSubtype(subType)
- } else if (ScanFragment.samsungSubDeviceTypeMap.containsKey(wrappedScanResult.uniqueIdentifier)) {
- val subType = ScanFragment.samsungSubDeviceTypeMap[wrappedScanResult.uniqueIdentifier]!!
- textView.text = SamsungTrackerType.visibleStringFromSubtype(subType)
- } else if (ScanFragment.googleSubDeviceTypeMap.containsKey(wrappedScanResult.uniqueIdentifier)) {
- val subType = ScanFragment.googleSubDeviceTypeMap[wrappedScanResult.uniqueIdentifier]!!
- textView.text = GoogleFindMyNetworkType.visibleStringFromSubtype(subType)
- } else if (ScanFragment.deviceNameMap.containsKey(wrappedScanResult.uniqueIdentifier)) {
- textView.text = ScanFragment.deviceNameMap[wrappedScanResult.uniqueIdentifier]
+ textView.text = if (deviceFromDb != null) {
+ // Case: device is in DB
+
+ if (deviceFromDb.name != null) {
+ deviceFromDb.getDeviceNameWithID()
+ } else if (deviceFromDb.deviceType == DeviceType.SAMSUNG_TRACKER && deviceFromDb.subDeviceType != "UNKNOWN") {
+ val subTypeString = deviceFromDb.subDeviceType
+ val subType = SamsungTrackerType.stringToSubType(subTypeString)
+ ScanFragment.samsungSubDeviceTypeMap[wrappedScanResult.uniqueIdentifier] = subType
+ SamsungTrackerType.visibleStringFromSubtype(subType)
+ } else if (deviceFromDb.deviceType == DeviceType.GOOGLE_FIND_MY_NETWORK) {
+ val subTypeString = deviceFromDb.subDeviceType
+ val subType = GoogleFindMyNetworkType.stringToSubType(subTypeString)
+ ScanFragment.googleSubDeviceTypeMap[wrappedScanResult.uniqueIdentifier] = subType
+ GoogleFindMyNetworkType.visibleStringFromSubtype(subType)
+ } else {
+ // Fallback
+ DeviceType.userReadableNameDefault(wrappedScanResult.deviceType)
+ }
} else {
- textView.text = DeviceType.userReadableName(wrappedScanResult)
+ // Case: device ist not in DB
+ // There is a possibility that the device has been determined. In that case this is only saved in the temporary map
+
+ if (ScanFragment.samsungSubDeviceTypeMap.containsKey(wrappedScanResult.uniqueIdentifier)) {
+ val subType = ScanFragment.samsungSubDeviceTypeMap[wrappedScanResult.uniqueIdentifier]!!
+ SamsungTrackerType.visibleStringFromSubtype(subType)
+ } else if (ScanFragment.googleSubDeviceTypeMap.containsKey(wrappedScanResult.uniqueIdentifier)) {
+ val subType = ScanFragment.googleSubDeviceTypeMap[wrappedScanResult.uniqueIdentifier]!!
+ GoogleFindMyNetworkType.visibleStringFromSubtype(subType)
+ } else if (ScanFragment.deviceNameMap.containsKey(wrappedScanResult.uniqueIdentifier)) {
+ ScanFragment.deviceNameMap[wrappedScanResult.uniqueIdentifier]
+ } else {
+ // Fallback
+ DeviceType.userReadableNameDefault(wrappedScanResult.deviceType)
+ }
}
}
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/Utility.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/Utility.kt
index bf760e2b..bd8bd690 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/Utility.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/Utility.kt
@@ -497,8 +497,11 @@ object Utility {
val deviceType = wrappedScanResult.deviceType
val securityLevel = SharedPrefs.riskSensitivity
+ // Skip Samsung Find My Mobile and Apple devices if security level is set to high as this causes a lot of false positives
if (deviceType == DeviceType.SAMSUNG_FIND_MY_MOBILE) {
return securityLevel != "high"
+ } else if (deviceType == DeviceType.APPLE) {
+ return securityLevel != "high"
}
return false
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/worker/ScheduleWorkersReceiver.kt b/app/src/main/java/de/seemoo/at_tracking_detection/worker/ScheduleWorkersReceiver.kt
index 3128c952..027f4d53 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/worker/ScheduleWorkersReceiver.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/worker/ScheduleWorkersReceiver.kt
@@ -8,7 +8,6 @@ import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
-import de.seemoo.at_tracking_detection.detection.BackgroundBluetoothScanner
import de.seemoo.at_tracking_detection.detection.PermanentBluetoothScanner
import de.seemoo.at_tracking_detection.detection.ScanBluetoothWorker
import de.seemoo.at_tracking_detection.util.SharedPrefs
@@ -16,6 +15,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
+import kotlinx.coroutines.Dispatchers
import timber.log.Timber
import java.util.concurrent.TimeUnit
import kotlin.coroutines.CoroutineContext
@@ -25,37 +25,67 @@ class ScheduleWorkersReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Timber.d("Broadcast received ${intent?.action}")
- if (intent?.action == "AlarmManagerWakeUp_Schedule_BackgroundScan") {
- // The app has been launched because no scan was performed since two hours
- val backgroundWorkScheduler = ATTrackingDetectionApplication.getCurrentApp().backgroundWorkScheduler
- //Schedule the periodic scan worker which runs every 15min
- backgroundWorkScheduler.launch()
- if (SharedPrefs.shareData) {
- backgroundWorkScheduler.scheduleShareData()
- }
- BackgroundWorkScheduler.scheduleAlarmWakeupIfScansFail()
- }else {
- // action = AlarmManagerWakeUp_Perform_BackgroundScan
- // The app has been launched to perform another scan
- BackgroundWorkScheduler.scheduleScanWithAlarm()
- @OptIn(DelicateCoroutinesApi::class)
- goAsync {
- Timber.d("Running scan launched from Alert")
- BackgroundBluetoothScanner.scanInBackground(startedFrom = "ScheduleWorkersReceiver")
+ val action = intent?.action
+ val backgroundWorkScheduler = ATTrackingDetectionApplication.getCurrentApp().backgroundWorkScheduler
+
+ // Keep the broadcast short: schedule alarms inline; offload WorkManager to a separate thread and finish quickly.
+ goAsync(Dispatchers.Default) {
+ when (action) {
+ // App woke up to make sure background schedule exists. Keep it light.
+ "AlarmManagerWakeUp_Schedule_BackgroundScan" -> {
+ // Offload WorkManager work
+ Thread {
+ try {
+ backgroundWorkScheduler.launch()
+ if (SharedPrefs.shareData) {
+ backgroundWorkScheduler.scheduleShareData()
+ }
+ } catch (t: Throwable) {
+ Timber.w(t, "Failed scheduling periodic work from receiver")
+ } finally {
+ BackgroundWorkScheduler.scheduleAlarmWakeupIfScansFail()
+ }
+ }.start()
+ }
+
+ // Our exact/alarm fired to perform a scan: enqueue work, don't run the scan inline.
+ "AlarmManagerWakeUp_Perform_BackgroundScan" -> {
+ // Reschedule next alarm now (cheap)
+ BackgroundWorkScheduler.scheduleScanWithAlarm()
+ // Offload WorkManager enqueue to separate thread so we return fast
+ Thread {
+ try {
+ backgroundWorkScheduler.scheduleImmediateBackgroundScan()
+ } catch (t: Throwable) {
+ Timber.w(t, "Failed to enqueue immediate scan from receiver")
+ }
+ }.start()
+ }
+
+ // System broadcasts: keep receiver fast; just (re)establish lightweight schedules.
+ Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
+ // Only (re)schedule alarms on boot/package replace. Avoid WorkManager initialization here.
+ BackgroundWorkScheduler.scheduleScanWithAlarm()
+ BackgroundWorkScheduler.scheduleAlarmWakeupIfScansFail()
+ }
+
+ else -> Timber.w("Unhandled broadcast action: $action")
}
}
- // Initiate the permanent background scanner.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && SharedPrefs.usePermanentBluetoothScanner) {
- goAsync {
- Timber.d("Attempting to start PermanentBluetoothScanner from ScheduleWorkersReceiver")
- PermanentBluetoothScanner.scan()
+ // Start the permanent scanner in a detached way
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && Build.VERSION.SDK_INT <= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && SharedPrefs.usePermanentBluetoothScanner) {
+ goAsync(Dispatchers.Default) {
+ try {
+ Timber.d("Attempting to start PermanentBluetoothScanner from ScheduleWorkersReceiver")
+ PermanentBluetoothScanner.scan()
+ } catch (t: Throwable) {
+ Timber.w(t, "Failed starting PermanentBluetoothScanner from receiver")
+ }
}
}
}
-
-
companion object {
const val OBSERVATION_DURATION = 1L // in hours
const val OBSERVATION_DELTA = 30L // in minutes
diff --git a/app/src/main/res/layout/fragment_scan.xml b/app/src/main/res/layout/fragment_scan.xml
index 53ef3186..e2e32cd4 100644
--- a/app/src/main/res/layout/fragment_scan.xml
+++ b/app/src/main/res/layout/fragment_scan.xml
@@ -67,6 +67,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
+ android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:setAdapter="@{adapter_high_risk}" />
@@ -74,6 +75,7 @@
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 9df18a39..e28a003e 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -2,7 +2,7 @@
AirGuard
Ignorierte Geräte
- Übersicht
+ Startseite
Einstellungen
Debug
Feedback
@@ -31,7 +31,7 @@
Hintergrundscan deaktivieren
Diese Option deaktiviert den Hintergrundscan. Achtung: Wenn diese Option aktiviert ist, bekommst du keine Benachrichtigungen mehr, wenn dir ein Tracker folgt.
Permanenter Scanner
- Nutze einen permanenten Hintergrund-Scan. Diese Feature sorgt für akkuratere Scans, könnte aber in Ausnahmefällen die Stabilität der App beeinflussen.
+ Nutze einen permanenten Hintergrund-Scan. Diese Feature sorgt für akkuratere Scans, könnte aber die Stabilität der App beeinflussen. Im Zweifel: Deaktiviert lassen
Aktivieren, um detailliertere Standortinformationen bei einer Warnung zu erhalten
Standort nutzen
Verbundene Geräte anzeigen
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index e0af794c..edfd53ef 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2,7 +2,7 @@
AirGuard
Ignored
- Dashboard
+ Home
Settings
Debug
Feedback
@@ -35,7 +35,7 @@
Deactivate background scan
This deactivates AirGuard running in the background. Please be aware: You will not be warned if a tracker is following you anymore.
Permanent scanning
- Use a permanent background scanner. Enabling this feature results in more accurate results, but in rare cases might affect the stability of the app.
+ Use a permanent background scanner. Enabling this feature results in more accurate results, but it might affect the stability of the app. When in doubt: leave this option disabled.
Discovered:
Beacons
Times seen
diff --git a/build.gradle b/build.gradle
index d891b82a..6fa59a03 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,12 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '2.2.10'
- ext.hilt_compiler_version = '2.57.1'
+ ext.hilt_compiler_version = '2.57.2'
ext.hilt_version = '1.3.0'
- ext.room_version = '2.8.0'
+ ext.room_version = '2.8.1'
ext.compose_version = '1.7.3'
ext.about_libraries_version = '12.2.4'
- ext.work_version = '2.10.4'
+ ext.work_version = '2.10.5'
ext.ksp_version = '2.2.10-2.0.2'
repositories {
@@ -22,7 +22,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:8.13.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_compiler_version"
- classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.9.4"
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.9.5"
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries_version"
classpath "com.google.devtools.ksp:symbol-processing-gradle-plugin:$ksp_version"
classpath "org.jetbrains.compose:compose-gradle-plugin:$compose_version"
diff --git a/fastlane/metadata/android/de-DE/changelogs/56.txt b/fastlane/metadata/android/de-DE/changelogs/56.txt
new file mode 100644
index 00000000..b8fdd870
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/56.txt
@@ -0,0 +1,4 @@
+Änderung: Apple-Geräte (MacBooks, iPhones) sind standardmäßig deaktiviert, wenn die Sensitivität nicht auf "hoch" eingestellt ist
+Verbessert: Absturz behoben, der auf neueren Android-Geräten auftreten konnte
+Verbessert: Leistungsverbesserungen im Hintergrund
+Verbessert: Fehlerbehebungen
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/56.txt b/fastlane/metadata/android/en-US/changelogs/56.txt
new file mode 100644
index 00000000..4b0977ae
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/56.txt
@@ -0,0 +1,4 @@
+CHANGE: Apple Devices (MacBooks, iPhones) are disabled by default when risk sensitivity is not set to high
+IMPROVED: Fix crash that could occur on newer Android devices
+IMPROVED: Background Performance Improvements
+IMPROVED: Bugfixes
\ No newline at end of file