From 5f9c644953eccd0a5879f3270b072c29b4c15d40 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 21 Apr 2025 13:17:00 +0100
Subject: [PATCH 01/71] create empty UI and navigation for tracker export
---
.../ui/settings/FoundTrackersExport.kt | 9 +++++++++
.../ui/settings/InformationFragment.kt | 2 ++
.../ui/settings/SettingsFragment.kt | 13 +++++++++++++
.../res/drawable/ic_baseline_attach_file_24.xml | 5 +++++
.../res/layout/fragment_found_trackers_export.xml | 8 ++++++++
app/src/main/res/navigation/main_navigation.xml | 12 ++++++++++++
app/src/main/res/values-de/strings.xml | 4 ++++
app/src/main/res/values/strings.xml | 5 +++++
app/src/main/res/xml/fragment_settings.xml | 6 ++++++
9 files changed, 64 insertions(+)
create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/FoundTrackersExport.kt
create mode 100644 app/src/main/res/drawable/ic_baseline_attach_file_24.xml
create mode 100644 app/src/main/res/layout/fragment_found_trackers_export.xml
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/FoundTrackersExport.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/FoundTrackersExport.kt
new file mode 100644
index 00000000..d09b68e9
--- /dev/null
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/FoundTrackersExport.kt
@@ -0,0 +1,9 @@
+package de.seemoo.at_tracking_detection.ui.settings
+
+import androidx.fragment.app.Fragment
+import dagger.hilt.android.AndroidEntryPoint
+
+@AndroidEntryPoint
+class FoundTrackersExport : Fragment() {
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/InformationFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/InformationFragment.kt
index 99f1eb67..a6dba083 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/InformationFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/InformationFragment.kt
@@ -14,8 +14,10 @@ import androidx.navigation.NavDirections
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import dagger.hilt.android.AndroidEntryPoint
import de.seemoo.at_tracking_detection.R
+@AndroidEntryPoint
class InformationFragment : Fragment() {
private lateinit var attributionList: RecyclerView
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 2e1f2809..719b68fe 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
@@ -77,6 +77,13 @@ class SettingsFragment : PreferenceFragmentCompat() {
setAdvancedModeButtonVisibility()
+ // TODO
+// if (ATTrackingDetectionApplication.getCurrentApp().beaconRepository.totalCount > 0) {
+// findPreference("export_found_trackers")?.isVisible = true
+// } else {
+// findPreference("export_found_trackers")?.isVisible = false
+// }
+
val deactivateBackgroundScanningPref = findPreference("deactivate_background_scanning")
deactivateBackgroundScanningPref?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (newValue as Boolean) {
@@ -114,6 +121,12 @@ class SettingsFragment : PreferenceFragmentCompat() {
true
}
+ findPreference("export_found_trackers")?.onPreferenceClickListener =
+ Preference.OnPreferenceClickListener {
+ view?.findNavController()?.navigate(R.id.action_settings_to_export_found_trackers)
+ true
+ }
+
findPreference("delete_study_data")?.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
view?.findNavController()?.navigate(R.id.action_settings_to_data_deletion)
diff --git a/app/src/main/res/drawable/ic_baseline_attach_file_24.xml b/app/src/main/res/drawable/ic_baseline_attach_file_24.xml
new file mode 100644
index 00000000..dc6c07a4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_attach_file_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_found_trackers_export.xml b/app/src/main/res/layout/fragment_found_trackers_export.xml
new file mode 100644
index 00000000..a0993e98
--- /dev/null
+++ b/app/src/main/res/layout/fragment_found_trackers_export.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/main_navigation.xml b/app/src/main/res/navigation/main_navigation.xml
index 253d1372..201256e1 100644
--- a/app/src/main/res/navigation/main_navigation.xml
+++ b/app/src/main/res/navigation/main_navigation.xml
@@ -77,6 +77,10 @@
android:id="@+id/action_settings_to_data_deletion"
app:destination="@id/data_deletion"
/>
+
@@ -99,6 +103,14 @@
/>
+
+
+
+
Hoch
Information & KontaktBekomme Informationen über uns
+ Exportiere gefundene Tracker
+ Erstelle ein Dokument mit allen gefunden TrackernStudiendaten löschenAnfrage der Löschung der StudiendatenGerätetypen herausfiltern
@@ -396,4 +398,6 @@
LinksRechts
+
+ Tracker exportieren
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8577b918..6a7241f5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -120,6 +120,9 @@
Information & ContactGet information about us
+ Export Found Trackers
+ Create a document with all identified trackers
+
Delete study dataRequest deletion of study data
@@ -438,4 +441,6 @@
LeftRight
+ Export Trackers
+
diff --git a/app/src/main/res/xml/fragment_settings.xml b/app/src/main/res/xml/fragment_settings.xml
index b3b1f35b..383d7e37 100644
--- a/app/src/main/res/xml/fragment_settings.xml
+++ b/app/src/main/res/xml/fragment_settings.xml
@@ -10,6 +10,12 @@
android:summary="@string/settings_advanced_mode_summary"
app:title="@string/settings_advanced_mode_title" />
+
+
Date: Mon, 21 Apr 2025 16:19:34 +0100
Subject: [PATCH 02/71] initial implementation of the PDF creation
---
app/src/main/AndroidManifest.xml | 3 +
.../database/daos/NotificationDao.kt | 3 +
.../database/repository/LocationRepository.kt | 1 -
.../repository/NotificationRepository.kt | 4 +-
.../ui/settings/FoundTrackersExport.kt | 358 ++++++++++++++++++
.../layout/fragment_found_trackers_export.xml | 62 ++-
app/src/main/res/values-de/strings.xml | 2 +
app/src/main/res/values/strings.xml | 2 +
8 files changed, 428 insertions(+), 7 deletions(-)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cb8732f2..3d4eaa83 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -45,6 +45,9 @@
+
+
>
+ @Query("SELECT * FROM notification")
+ fun getAllNotifications(): List
+
@Query("UPDATE notification SET falseAlarm = :state WHERE :id Like notificationId")
suspend fun setFalseAlarm(id: Int, state: Boolean)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/LocationRepository.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/LocationRepository.kt
index 07d066c0..b6b88a8b 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/LocationRepository.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/repository/LocationRepository.kt
@@ -28,7 +28,6 @@ class LocationRepository @Inject constructor(
fun getLocationsForBeaconSince(deviceAddress: String, since: LocalDateTime): List = locationDao.getLocationsForDeviceSince(deviceAddress, since)
-
fun getLocationsWithNoBeacons(): List = locationDao.getLocationsWithNoBeacons()
@WorkerThread
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 c8701b07..0aeee895 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
@@ -44,7 +44,7 @@ class NotificationRepository @Inject constructor(
*/
val last_notification: List = notificationDao.getLastNotification()
- fun notificationForDevice(device: BaseDevice) = notificationDao.getNotificationForDevice(device.address)
+ fun notificationForDevice(device: BaseDevice): List = notificationDao.getNotificationForDevice(device.address)
fun getNotificationForDeviceSinceCount(deviceAddress: String, since: LocalDateTime): Int = notificationDao.getNotificationForDeviceSinceCount(deviceAddress, since)
@@ -52,6 +52,8 @@ class NotificationRepository @Inject constructor(
fun existsNotificationForDevice(deviceAddress: String): Boolean = notificationDao.existsNotificationForDevice(deviceAddress)
+ fun getAllNotifications(): List = notificationDao.getAllNotifications()
+
@WorkerThread
suspend fun insert(notification: Notification): Long {
return notificationDao.insert(notification)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/FoundTrackersExport.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/FoundTrackersExport.kt
index d09b68e9..718afd32 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/FoundTrackersExport.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/FoundTrackersExport.kt
@@ -1,9 +1,367 @@
package de.seemoo.at_tracking_detection.ui.settings
+import android.Manifest
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Typeface
+import android.graphics.pdf.PdfDocument
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ProgressBar
+import android.widget.Toast
+import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
+import de.seemoo.at_tracking_detection.R
+import de.seemoo.at_tracking_detection.database.models.device.BaseDevice
+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.LocationRepository
+import de.seemoo.at_tracking_detection.database.repository.NotificationRepository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import timber.log.Timber
+import java.io.ByteArrayOutputStream
+import java.time.format.DateTimeFormatter
+import javax.inject.Inject
@AndroidEntryPoint
class FoundTrackersExport : Fragment() {
+ @Inject lateinit var deviceRepository: DeviceRepository
+ @Inject lateinit var beaconRepository: BeaconRepository
+ @Inject lateinit var locationRepository: LocationRepository
+ @Inject lateinit var notificationRepository: NotificationRepository
+
+ private lateinit var exportDocumentButton: Button
+ private lateinit var progressBar: ProgressBar
+
+ data class PageContext(
+ val page: PdfDocument.Page,
+ val canvas: Canvas,
+ val yPos: Float
+ )
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_found_trackers_export, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ exportDocumentButton = view.findViewById(R.id.create_document_button)
+ progressBar = view.findViewById(R.id.progress_bar)
+
+ val exportDocumentButton = view.findViewById
@@ -322,6 +330,17 @@
app:nullable="true" />
+
+
+
+
Apple \"FindMy\"
Apple Gerät
+ Exportiere Geräte-Infos als PDF
+ Erstelle eine Zusammenfassung über dieses Gerät als PDF
+
Tracker beobachtenBeobachtung stoppenDieser Tracker wird zur Zeit beobachtet.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4809b9e0..1536627f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -11,6 +11,7 @@
ScanLocate: {deviceAddress}Observe tracker: {deviceAddress}
+ Export device data: {deviceAddress}All DevicesMap: %1$sDevices
@@ -364,6 +365,9 @@
Preview Image for this articleShow explanation text
+ Export Device to PDF
+ Create a PDF file summary about this tracker
+
Observe TrackerImage of a clockStart Observation
diff --git a/app/src/main/res/xml/fragment_settings.xml b/app/src/main/res/xml/fragment_settings.xml
index 383d7e37..38de9a74 100644
--- a/app/src/main/res/xml/fragment_settings.xml
+++ b/app/src/main/res/xml/fragment_settings.xml
@@ -10,11 +10,12 @@
android:summary="@string/settings_advanced_mode_summary"
app:title="@string/settings_advanced_mode_title" />
-
+ -->
Date: Fri, 25 Apr 2025 14:46:59 +0100
Subject: [PATCH 04/71] basic pdf creation
---
.../ui/settings/FoundTrackersExport.kt | 1 -
.../ui/tracking/ExportDeviceFragment.kt | 335 ++++++++++++++++++
.../res/layout/fragment_export_device.xml | 24 +-
app/src/main/res/values-de/strings.xml | 14 +
app/src/main/res/values/strings.xml | 15 +-
5 files changed, 376 insertions(+), 13 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/FoundTrackersExport.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/FoundTrackersExport.kt
index 718afd32..03640f87 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/FoundTrackersExport.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/FoundTrackersExport.kt
@@ -65,7 +65,6 @@ class FoundTrackersExport : Fragment() {
exportDocumentButton = view.findViewById(R.id.create_document_button)
progressBar = view.findViewById(R.id.progress_bar)
- val exportDocumentButton = view.findViewById(R.id.create_document_button)
exportDocumentButton.setOnClickListener {
checkPermissionsAndGeneratePdf()
}
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
index 7b9480b0..bf7ad68f 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
@@ -1,22 +1,74 @@
package de.seemoo.at_tracking_detection.ui.tracking
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.AlertDialog
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.pdf.PdfDocument
+import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.widget.Button
+import android.widget.ProgressBar
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
+import dagger.hilt.android.AndroidEntryPoint
import de.seemoo.at_tracking_detection.R
+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.DeviceType
+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.LocationRepository
+import de.seemoo.at_tracking_detection.database.repository.NotificationRepository
import de.seemoo.at_tracking_detection.databinding.FragmentExportDeviceBinding
+import de.seemoo.at_tracking_detection.ui.settings.FoundTrackersExport
+import de.seemoo.at_tracking_detection.ui.settings.FoundTrackersExport.PageContext
+import de.seemoo.at_tracking_detection.util.SharedPrefs
+import de.seemoo.at_tracking_detection.util.risk.RiskLevel
+import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator.Companion.checkRiskLevelForDevice
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.osmdroid.tileprovider.tilesource.TileSourceFactory
+import org.osmdroid.util.GeoPoint
+import org.osmdroid.views.MapView
+import org.osmdroid.views.overlay.Marker
+import timber.log.Timber
+import java.io.ByteArrayOutputStream
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+import javax.inject.Inject
+@AndroidEntryPoint
class ExportDeviceFragment: Fragment() {
+ @Inject lateinit var deviceRepository: DeviceRepository
+ @Inject lateinit var beaconRepository: BeaconRepository
+ @Inject lateinit var locationRepository: LocationRepository
+ @Inject lateinit var notificationRepository: NotificationRepository
+
private val viewModel: ExportDeviceViewModel by viewModels()
private val safeArgs: ExportDeviceFragmentArgs by navArgs()
private var deviceAddress: String? = null
+ private lateinit var exportDocumentButton: Button
+ private lateinit var progressBar: ProgressBar
+
private lateinit var binding: FragmentExportDeviceBinding
override fun onCreateView(
@@ -37,4 +89,287 @@ class ExportDeviceFragment: Fragment() {
return binding.root
}
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ exportDocumentButton = view.findViewById(R.id.create_document_button)
+ progressBar = view.findViewById(R.id.progress_bar)
+
+ exportDocumentButton.setOnClickListener {
+ checkPermissionsAndGeneratePdf()
+ }
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ when (requestCode) {
+ STORAGE_PERMISSION_CODE -> {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ startPdfGeneration()
+ } else {
+ Toast.makeText(context, "Permission required to save PDF", Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+
+ private fun checkPermissionsAndGeneratePdf() {
+ Timber.d("Checking permissions for PDF generation")
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+ when {
+ ContextCompat.checkSelfPermission(
+ requireContext(),
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ ) == PackageManager.PERMISSION_GRANTED -> {
+ startPdfGeneration()
+ }
+ shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE) -> {
+ showPermissionRationale()
+ }
+ else -> {
+ requestPermissions(
+ arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
+ STORAGE_PERMISSION_CODE
+ )
+ }
+ }
+ } else {
+ startPdfGeneration()
+ }
+ }
+
+ private fun startPdfGeneration() {
+ showLoading(true)
+ Timber.d("Starting PDF generation")
+
+ lifecycleScope.launch(Dispatchers.IO) {
+ try {
+ val pdfBytes = generatePdfContent()
+
+ withContext(Dispatchers.Main) {
+ showSaveDialog(pdfBytes)
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(context, "Error generating PDF: ${e.message}", Toast.LENGTH_LONG).show()
+ }
+ } finally {
+ withContext(Dispatchers.Main) {
+ showLoading(false)
+ }
+ }
+ }
+ }
+
+ private fun showSaveDialog(pdfBytes: ByteArray) {
+ val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
+ addCategory(Intent.CATEGORY_OPENABLE)
+ type = "application/pdf"
+ putExtra(Intent.EXTRA_TITLE, "TrackingReport_${System.currentTimeMillis()}.pdf")
+ }
+ startActivityForResult(intent, CREATE_FILE_REQUEST_CODE)
+
+ // Store PDF bytes temporarily
+ Companion.pdfBytes = pdfBytes
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == CREATE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
+ data?.data?.let { uri ->
+ lifecycleScope.launch(Dispatchers.IO) {
+ try {
+ requireContext().contentResolver.openOutputStream(uri)?.use {
+ it.write(pdfBytes)
+ }
+ withContext(Dispatchers.Main) {
+ Toast.makeText(context, "PDF saved successfully", Toast.LENGTH_SHORT).show()
+ }
+ } catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ Toast.makeText(context, "Error saving PDF: ${e.message}", Toast.LENGTH_LONG).show()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun showLoading(show: Boolean) {
+ exportDocumentButton.visibility = if (show) View.GONE else View.VISIBLE
+ progressBar.visibility = if (show) View.VISIBLE else View.GONE
+ }
+
+ private fun showPermissionRationale() {
+ AlertDialog.Builder(requireContext())
+ .setTitle("Storage Permission Needed")
+ .setMessage("This permission is required to save PDF reports")
+ .setPositiveButton("OK") { _, _ ->
+ requestPermissions(
+ arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
+ STORAGE_PERMISSION_CODE
+ )
+ }
+ .setNegativeButton("Cancel", null)
+ .show()
+ }
+
+ private suspend fun generatePdfContent(): ByteArray = withContext(Dispatchers.IO) {
+ Timber.d("Generating PDF content")
+ val outputStream = ByteArrayOutputStream()
+ val pdfDocument = PdfDocument()
+ val device = deviceRepository.getDevice(deviceAddress!!) ?: throw IllegalStateException("Device not found")
+ try {
+ // val initialPage = createNewPage(pdfDocument)
+ val pageInfo = PdfDocument.PageInfo.Builder(1200, 2000, 1).create()
+ val page = pdfDocument.startPage(pageInfo)
+ val canvas = page.canvas
+ var pageContext = PageContext(page, canvas, 50f)
+ var yPos = 50f
+
+ // Determine if the tracker is following
+ val useLocation = SharedPrefs.useLocationInTrackingDetection
+ val deviceRiskLevel = checkRiskLevelForDevice(device!!, useLocation) //TODO: Handle null cases
+ val trackerFollowing = deviceRiskLevel != RiskLevel.LOW
+
+ val headerPaint = Paint().apply {
+ color = if (trackerFollowing) Color.RED else Color.BLUE
+ textSize = 36f
+ }
+ canvas.drawText(
+ if (trackerFollowing) getString(R.string.export_trackers_following) else getString(R.string.export_trackers_not_following),
+ 50f, 60f, headerPaint
+ )
+ yPos += 80f
+
+ // --- Basic Device Information ---
+ val trackerType = DeviceManager.deviceTypeToString(device.deviceType!!) // TODO: Handle potential null deviceType
+ val lastSeenDate = device.lastSeen.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM))
+ val firstSeenDate = device.firstDiscovery.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM))
+ val detectionCount = beaconRepository.getDeviceBeaconsCount(device.address)
+ val locations = locationRepository.getLocationsForBeacon(device.address)
+ val uniqueLocations = locations.size
+
+ val textPaint = Paint().apply {
+ color = Color.BLACK
+ textSize = 24f
+ }
+
+ // Basic Info
+ canvas.drawText(getString(R.string.export_trackers_tracker_type, trackerType), 50f, yPos, textPaint)
+ yPos += 40
+ if (device.deviceType != DeviceType.SAMSUNG_TRACKER && device.deviceType != DeviceType.SAMSUNG_FIND_MY_MOBILE) {
+ // Identification does not work via MAC address on Samsung devices, therefore hide it
+ canvas.drawText(getString(R.string.export_trackers_mac, device.address), 50f, yPos, textPaint)
+ yPos += 40
+ }
+ canvas.drawText(getString(R.string.export_trackers_last_seen, lastSeenDate), 50f, yPos, textPaint)
+ yPos += 40
+ canvas.drawText(getString(R.string.export_trackers_first_seen, firstSeenDate), 50f, yPos, textPaint)
+ yPos += 40
+ canvas.drawText(getString(R.string.export_trackers_detections, detectionCount), 50f, yPos, textPaint)
+ yPos += 40
+ canvas.drawText(getString(R.string.export_trackers_unique_locations, uniqueLocations), 50f, yPos, textPaint)
+ yPos += 60 // Add extra space before the next section
+
+ // --- Map Placeholder ---
+ // TODO: Map
+ canvas.drawText("[Map Placeholder]", 50f, yPos, textPaint) // Placeholder for the map
+ yPos += 60 // Add extra space before the next section
+
+ // --- Beacons ---
+ val beacons = beaconRepository.getDeviceBeacons(device.address)
+
+ for (beacon in beacons) {
+ val beaconDate = beacon.receivedAt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM))
+ val beaconLocation = locations.find { it.locationId == beacon.locationId }
+
+ // 1. Time
+ canvas.drawText(getString(R.string.export_trackers_time, beaconDate), 50f, yPos, textPaint)
+ yPos += 30
+
+ // 2. Location
+ val locationText = if (beaconLocation != null) {
+ getString(R.string.export_trackers_location, beaconLocation.latitude.toString(), beaconLocation.longitude.toString())
+ } else {
+ getString(R.string.export_trackers_location_unknown)
+ }
+ canvas.drawText(locationText, 50f, yPos, textPaint)
+ yPos += 30
+
+ // 3. Google Maps Link
+ if (beaconLocation != null && beaconLocation.locationId != 0) {
+ // TODO: make Link clickable
+ val googleMapsLink = "https://maps.google.com/?q=${beaconLocation.latitude},${beaconLocation.longitude}"
+ canvas.drawText(getString(R.string.export_trackers_map, googleMapsLink), 50f, yPos, textPaint)
+ yPos += 30
+ }
+
+ // 4. Signal Strength
+ val rssi = beacon.rssi
+ canvas.drawText(getString(R.string.export_trackers_signal_strength, rssi), 50f, yPos, textPaint)
+ yPos += 40
+ }
+
+ // --- Footer ---
+ // Draw footer at a fixed position near the bottom
+ canvas.drawText(getString(R.string.export_trackers_created_by), 50f, 1900f, textPaint)
+
+ pdfDocument.finishPage(pageContext.page)
+ pdfDocument.writeTo(outputStream)
+ outputStream.toByteArray()
+ } catch (e: Exception) {
+ Timber.e(e, "Error generating PDF content")
+ throw e
+ } finally {
+ pdfDocument.close()
+ outputStream.close()
+ }
+ }
+
+// private fun generatePdfToUri(device: BaseDevice, uri: Uri) {
+// val context = context ?: return // Handle detached state
+//
+//// val mapView = MapView(context).apply {
+//// setTileSource(TileSourceFactory.MAPNIK)
+//// minZoomLevel = 4.0
+//// maxZoomLevel = 19.0
+//// controller.setZoom(12.0)
+////
+//// // TODO: replace with existing logic
+////
+//// // Add markers for each location
+//// locations.forEach { location ->
+//// val marker = Marker(this).apply {
+//// position = GeoPoint(location.latitude, location.longitude)
+//// setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
+//// }
+//// overlays.add(marker)
+//// }
+//// }
+////
+//// // Measure and layout map view
+//// mapView.measure(
+//// View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY),
+//// View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
+//// )
+//// mapView.layout(0, 0, 1000, 600)
+////
+//// // Draw map to canvas
+//// mapView.draw(canvas.apply { translate(50f, yPos + 40f) })
+//// yPos += 640f
+//
+// }
+
+ companion object {
+ private const val STORAGE_PERMISSION_CODE = 1001
+ private const val CREATE_FILE_REQUEST_CODE = 1002
+ private var pdfBytes: ByteArray? = null
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_export_device.xml b/app/src/main/res/layout/fragment_export_device.xml
index 1b020f8d..49413f9c 100644
--- a/app/src/main/res/layout/fragment_export_device.xml
+++ b/app/src/main/res/layout/fragment_export_device.xml
@@ -14,25 +14,27 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
-
+ android:layout_marginEnd="16dp"
+ android:padding="16dp"
+ android:text="@string/export_trackers_create_document" />
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index ec5662f9..0ee7b9cf 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -405,4 +405,18 @@
Tracker exportierenDer Button unten ermöglicht es ein PDF Dokument zu erstellen, welche alle Geräte enthält, die als Tracker identifiziert wurden.Dokument erstellen
+ Dieser Tracker folgte dir
+ Dieser Tracker folgte dir nicht
+ "Unbekannter Standort"
+ "Tracker Typ: %s"
+ "MAC-Adresse: %s"
+ "Zuerst gesehen am: %s"
+ "Zuletzt gesehen am: %s"
+ "Wie oft erkannt: %d"
+ "Erkannt an Standorten: %d"
+ Uhrzeit: %s"
+ Standort: Latitude: %s, Longitude: %s"
+ Karte: %s"
+ Signalstärke: %d"
+ Erstellt von AirGuard auf Android
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1536627f..bd56105c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -448,5 +448,18 @@
Export TrackersBy clicking the button below, you will be able to create a pdf document that contains all trackers that have been identified as following you for a longer time.Create document
-
+ The tracker has been following you
+ The tracker has not been following you
+ "Location: Unknown"
+ "Tracker Type: %s"
+ "MAC Address: %s"
+ "Last Seen: %s"
+ "First Seen: %s"
+ "Detections: %d"
+ "Unique Locations: %d"
+ Time: %s"
+ Location: Latitude: %s, Longitude: %s"
+ Map: %s"
+ Signal Strength: %d"
+ Created by AirGuard for Android
From f6015228793bcc77c2b95ea6514eee8377d3cb6e Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sat, 26 Apr 2025 13:18:08 +0100
Subject: [PATCH 05/71] implement working map and pagination
---
.../ui/tracking/ExportDeviceFragment.kt | 404 +++++++++++++-----
.../ui/tracking/TrackingFragment.kt | 9 +-
.../at_tracking_detection/util/Utility.kt | 15 +-
.../res/layout/fragment_export_device.xml | 49 ++-
app/src/main/res/values-de/strings.xml | 28 +-
app/src/main/res/values/strings.xml | 22 +-
6 files changed, 369 insertions(+), 158 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
index bf7ad68f..179db2c4 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
@@ -6,10 +6,13 @@ import android.app.Activity
import android.app.AlertDialog
import android.content.Intent
import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.Typeface
import android.graphics.pdf.PdfDocument
-import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
@@ -18,9 +21,8 @@ import android.view.ViewGroup
import android.widget.Button
import android.widget.ProgressBar
import android.widget.Toast
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
+import androidx.core.graphics.createBitmap
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@@ -28,6 +30,8 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
import dagger.hilt.android.AndroidEntryPoint
import de.seemoo.at_tracking_detection.R
+import de.seemoo.at_tracking_detection.database.models.Beacon
+import de.seemoo.at_tracking_detection.database.models.Location
import de.seemoo.at_tracking_detection.database.models.device.BaseDevice
import de.seemoo.at_tracking_detection.database.models.device.DeviceManager
import de.seemoo.at_tracking_detection.database.models.device.DeviceType
@@ -36,15 +40,14 @@ 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.databinding.FragmentExportDeviceBinding
-import de.seemoo.at_tracking_detection.ui.settings.FoundTrackersExport
-import de.seemoo.at_tracking_detection.ui.settings.FoundTrackersExport.PageContext
import de.seemoo.at_tracking_detection.util.SharedPrefs
+import de.seemoo.at_tracking_detection.util.Utility
import de.seemoo.at_tracking_detection.util.risk.RiskLevel
import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator.Companion.checkRiskLevelForDevice
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import org.osmdroid.tileprovider.tilesource.TileSourceFactory
+import org.osmdroid.util.BoundingBox
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
@@ -69,8 +72,22 @@ class ExportDeviceFragment: Fragment() {
private lateinit var exportDocumentButton: Button
private lateinit var progressBar: ProgressBar
+ private lateinit var mapView: MapView
+ private var isMapReady = false
+
private lateinit var binding: FragmentExportDeviceBinding
+ private val PAGE_WIDTH = 1200
+ private val PAGE_HEIGHT = 2000
+ private val MARGIN = 50f
+ private val FOOTER_HEIGHT = 50f // Space reserved for footer
+ private val TEXT_SIZE_NORMAL = 24f
+ private val TEXT_SIZE_HEADER = 36f
+ private val LINE_SPACING = 10f // Extra space between lines
+ private val NORMAL_LINE_HEIGHT = TEXT_SIZE_NORMAL + LINE_SPACING
+ private val MAP_BOTTOM_MARGIN = 20f
+ private val SECTION_SPACING = 40f // Space between logical sections
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -90,14 +107,74 @@ class ExportDeviceFragment: Fragment() {
return binding.root
}
+ @SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ mapView = view.findViewById(R.id.map_preview)
+ Utility.basicMapSetup(mapView)
+
+ mapView.setMultiTouchControls(false)
+ mapView.setBuiltInZoomControls(false)
+ mapView.setOnTouchListener { _, _ -> true }
+
+ view.post {
+ setupMapContent()
+ }
+
exportDocumentButton = view.findViewById(R.id.create_document_button)
progressBar = view.findViewById(R.id.progress_bar)
exportDocumentButton.setOnClickListener {
- checkPermissionsAndGeneratePdf()
+ if (isMapReady) {
+ checkPermissionsAndGeneratePdf()
+ } else {
+ Toast.makeText(context, getString(R.string.export_trackers_map_still_loading), Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mapView.onResume()
+ if (!isMapReady) setupMapContent()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ mapView.onPause()
+ }
+
+ private fun setupMapContent() {
+ lifecycleScope.launch {
+ val locations = locationRepository.getLocationsForBeacon(deviceAddress!!)
+ val geoPoints = locations.map { GeoPoint(it.latitude, it.longitude) }
+
+ // Add markers
+ val iconDrawable = ContextCompat.getDrawable(requireContext(), R.drawable.ic_baseline_location_on_45_black)
+ iconDrawable?.setTint(Color.BLACK)
+ geoPoints.forEach { point ->
+ Marker(mapView).apply {
+ position = point
+ icon = iconDrawable
+ setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
+ setInfoWindow(null)
+ mapView.overlays.add(this)
+ }
+ }
+
+ // Set zoom and center
+ if (geoPoints.isNotEmpty()) {
+ val boundingBox = BoundingBox.fromGeoPointsSafe(geoPoints)
+ mapView.post {
+ mapView.zoomToBoundingBox(boundingBox, false, 100)
+ mapView.invalidate()
+ isMapReady = true
+ }
+ } else {
+ mapView.controller.setZoom(2.0)
+ isMapReady = true
+ }
}
}
@@ -222,107 +299,149 @@ class ExportDeviceFragment: Fragment() {
private suspend fun generatePdfContent(): ByteArray = withContext(Dispatchers.IO) {
Timber.d("Generating PDF content")
+ val device = deviceRepository.getDevice(deviceAddress!!) ?: run {
+ Timber.e("Device not found in database: $deviceAddress")
+ throw IllegalStateException("Device $deviceAddress not found for PDF export.")
+ }
+ val beacons = beaconRepository.getDeviceBeacons(device.address)
+ val locations = locationRepository.getLocationsForBeacon(device.address)
+
val outputStream = ByteArrayOutputStream()
val pdfDocument = PdfDocument()
- val device = deviceRepository.getDevice(deviceAddress!!) ?: throw IllegalStateException("Device not found")
+
try {
- // val initialPage = createNewPage(pdfDocument)
- val pageInfo = PdfDocument.PageInfo.Builder(1200, 2000, 1).create()
- val page = pdfDocument.startPage(pageInfo)
- val canvas = page.canvas
- var pageContext = PageContext(page, canvas, 50f)
- var yPos = 50f
-
- // Determine if the tracker is following
+ Timber.d("Starting PDF layout calculation (Pass 1)...")
+ val totalPages = calculateTotalPages(device, beacons, locations)
+ Timber.d("Calculated total pages: $totalPages")
+ if (totalPages <= 0) {
+ Timber.w("Calculation resulted in zero or negative pages. Aborting.")
+ return@withContext ByteArray(0) // Return empty if no content leads to pages
+ }
+
+ Timber.d("Starting PDF rendering (Pass 2)...")
+ var currentPageNumber = 1
+ var pageInfo = PdfDocument.PageInfo.Builder(PAGE_WIDTH, PAGE_HEIGHT, currentPageNumber).create()
+ var currentPage = pdfDocument.startPage(pageInfo)
+ var canvas = currentPage.canvas
+ var yPos = MARGIN
+
+ // Common Paints
+ val textPaint = Paint().apply { color = Color.BLACK; textSize = TEXT_SIZE_NORMAL; isAntiAlias = true }
+ val boldTextPaint = Paint(textPaint).apply { typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) }
+ val linkPaint = Paint(textPaint).apply { color = Color.BLUE; isUnderlineText = true }
+ val headerPaint = Paint().apply { textSize = TEXT_SIZE_HEADER; isAntiAlias = true; typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) }
+
+ val drawablePageHeight = PAGE_HEIGHT - MARGIN - FOOTER_HEIGHT - LINE_SPACING // Usable height for content
+
val useLocation = SharedPrefs.useLocationInTrackingDetection
- val deviceRiskLevel = checkRiskLevelForDevice(device!!, useLocation) //TODO: Handle null cases
+ val deviceRiskLevel = checkRiskLevelForDevice(device, useLocation)
val trackerFollowing = deviceRiskLevel != RiskLevel.LOW
-
- val headerPaint = Paint().apply {
- color = if (trackerFollowing) Color.RED else Color.BLUE
- textSize = 36f
- }
- canvas.drawText(
- if (trackerFollowing) getString(R.string.export_trackers_following) else getString(R.string.export_trackers_not_following),
- 50f, 60f, headerPaint
- )
- yPos += 80f
+ headerPaint.color = if (trackerFollowing) Color.RED else Color.BLUE
+ val headerText = if (trackerFollowing) getString(R.string.export_trackers_following) else getString(R.string.export_trackers_not_following)
+ canvas.drawText(headerText, MARGIN, yPos + TEXT_SIZE_HEADER, headerPaint)
+ yPos += TEXT_SIZE_HEADER + SECTION_SPACING
// --- Basic Device Information ---
- val trackerType = DeviceManager.deviceTypeToString(device.deviceType!!) // TODO: Handle potential null deviceType
+ val deviceTypeStr = DeviceManager.deviceTypeToString(device.deviceType ?: DeviceType.UNKNOWN)
val lastSeenDate = device.lastSeen.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM))
val firstSeenDate = device.firstDiscovery.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM))
- val detectionCount = beaconRepository.getDeviceBeaconsCount(device.address)
- val locations = locationRepository.getLocationsForBeacon(device.address)
- val uniqueLocations = locations.size
+ val detectionCount = beacons.size
+ val uniqueLocationCount = locations.size
- val textPaint = Paint().apply {
- color = Color.BLACK
- textSize = 24f
- }
+ canvas.drawText(getString(R.string.export_trackers_basic_info_title), MARGIN, yPos, boldTextPaint)
+ yPos += NORMAL_LINE_HEIGHT
- // Basic Info
- canvas.drawText(getString(R.string.export_trackers_tracker_type, trackerType), 50f, yPos, textPaint)
- yPos += 40
+ canvas.drawText(getString(R.string.export_trackers_tracker_type, deviceTypeStr), MARGIN, yPos, textPaint)
+ yPos += NORMAL_LINE_HEIGHT
if (device.deviceType != DeviceType.SAMSUNG_TRACKER && device.deviceType != DeviceType.SAMSUNG_FIND_MY_MOBILE) {
- // Identification does not work via MAC address on Samsung devices, therefore hide it
- canvas.drawText(getString(R.string.export_trackers_mac, device.address), 50f, yPos, textPaint)
- yPos += 40
+ canvas.drawText(getString(R.string.export_trackers_mac, device.address), MARGIN, yPos, textPaint)
+ yPos += NORMAL_LINE_HEIGHT
+ }
+ canvas.drawText(getString(R.string.export_trackers_first_seen, firstSeenDate), MARGIN, yPos, textPaint)
+ yPos += NORMAL_LINE_HEIGHT
+ canvas.drawText(getString(R.string.export_trackers_last_seen, lastSeenDate), MARGIN, yPos, textPaint)
+ yPos += NORMAL_LINE_HEIGHT
+ canvas.drawText(getString(R.string.export_trackers_detections, detectionCount), MARGIN, yPos, textPaint)
+ yPos += NORMAL_LINE_HEIGHT
+ canvas.drawText(getString(R.string.export_trackers_unique_locations, uniqueLocationCount), MARGIN, yPos, textPaint)
+ yPos += SECTION_SPACING
+
+ // --- Map Section ---
+ val mapBitmap = createMapBitmap()
+ val mapHeight = (mapBitmap.height * (PAGE_WIDTH - 2 * MARGIN) / mapBitmap.width)
+ val mapRect = Rect(MARGIN.toInt(), yPos.toInt(), (PAGE_WIDTH - MARGIN).toInt(), (yPos + mapHeight).toInt())
+ if (yPos + mapHeight > drawablePageHeight) {
+ Timber.d("Map doesn't fit on page $currentPageNumber, creating new page.")
+ drawPageFooter(canvas, currentPageNumber, totalPages, textPaint) // Add footer to current page
+ pdfDocument.finishPage(currentPage) // Finish current page
+ currentPageNumber++
+ pageInfo = PdfDocument.PageInfo.Builder(PAGE_WIDTH, PAGE_HEIGHT, currentPageNumber).create()
+ currentPage = pdfDocument.startPage(pageInfo)
+ canvas = currentPage.canvas
+ yPos = MARGIN // Reset yPos for new page
}
- canvas.drawText(getString(R.string.export_trackers_last_seen, lastSeenDate), 50f, yPos, textPaint)
- yPos += 40
- canvas.drawText(getString(R.string.export_trackers_first_seen, firstSeenDate), 50f, yPos, textPaint)
- yPos += 40
- canvas.drawText(getString(R.string.export_trackers_detections, detectionCount), 50f, yPos, textPaint)
- yPos += 40
- canvas.drawText(getString(R.string.export_trackers_unique_locations, uniqueLocations), 50f, yPos, textPaint)
- yPos += 60 // Add extra space before the next section
-
- // --- Map Placeholder ---
- // TODO: Map
- canvas.drawText("[Map Placeholder]", 50f, yPos, textPaint) // Placeholder for the map
- yPos += 60 // Add extra space before the next section
-
- // --- Beacons ---
- val beacons = beaconRepository.getDeviceBeacons(device.address)
+
+ canvas.drawText(getString(R.string.export_trackers_map_title), MARGIN, yPos, boldTextPaint)
+ yPos += NORMAL_LINE_HEIGHT
+ canvas.drawBitmap(mapBitmap, null, mapRect, null) // Draw map scaled into rect
+ yPos += mapHeight + SECTION_SPACING
+ mapBitmap.recycle()
+
+ // --- Beacon Details Section ---
+ canvas.drawText(getString(R.string.export_trackers_detections_title), MARGIN, yPos, boldTextPaint)
+ yPos += NORMAL_LINE_HEIGHT
for (beacon in beacons) {
- val beaconDate = beacon.receivedAt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM))
val beaconLocation = locations.find { it.locationId == beacon.locationId }
- // 1. Time
- canvas.drawText(getString(R.string.export_trackers_time, beaconDate), 50f, yPos, textPaint)
- yPos += 30
+ val beaconEntryHeight = getBeaconEntryHeight(beaconLocation)
+
+ // Check if the *entire* beacon entry fits on the current page
+ if (yPos + beaconEntryHeight > drawablePageHeight) {
+ Timber.d("Beacon entry doesn't fit on page $currentPageNumber, creating new page.")
+ drawPageFooter(canvas, currentPageNumber, totalPages, textPaint)
+ pdfDocument.finishPage(currentPage)
+ currentPageNumber++
+ pageInfo = PdfDocument.PageInfo.Builder(PAGE_WIDTH, PAGE_HEIGHT, currentPageNumber).create()
+ currentPage = pdfDocument.startPage(pageInfo)
+ canvas = currentPage.canvas
+ yPos = MARGIN // Reset yPos
+ canvas.drawText(getString(R.string.export_trackers_detections_title_continued), MARGIN, yPos, boldTextPaint)
+ yPos += NORMAL_LINE_HEIGHT
+ }
+
+ // Draw beacon details
+ val beaconDate = beacon.receivedAt.format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM))
+ canvas.drawText(getString(R.string.export_trackers_time, beaconDate), MARGIN, yPos, textPaint)
+ yPos += NORMAL_LINE_HEIGHT
- // 2. Location
val locationText = if (beaconLocation != null) {
getString(R.string.export_trackers_location, beaconLocation.latitude.toString(), beaconLocation.longitude.toString())
} else {
getString(R.string.export_trackers_location_unknown)
}
- canvas.drawText(locationText, 50f, yPos, textPaint)
- yPos += 30
+ canvas.drawText(locationText, MARGIN, yPos, textPaint)
+ yPos += NORMAL_LINE_HEIGHT
- // 3. Google Maps Link
- if (beaconLocation != null && beaconLocation.locationId != 0) {
- // TODO: make Link clickable
+ if (beaconLocation != null) {
val googleMapsLink = "https://maps.google.com/?q=${beaconLocation.latitude},${beaconLocation.longitude}"
- canvas.drawText(getString(R.string.export_trackers_map, googleMapsLink), 50f, yPos, textPaint)
- yPos += 30
+ // TODO: This is currently just blue, figure out a way to make this clickabel
+ canvas.drawText(getString(R.string.export_trackers_map, googleMapsLink), MARGIN, yPos, linkPaint)
+ yPos += NORMAL_LINE_HEIGHT
}
- // 4. Signal Strength
- val rssi = beacon.rssi
- canvas.drawText(getString(R.string.export_trackers_signal_strength, rssi), 50f, yPos, textPaint)
- yPos += 40
+ canvas.drawText(getString(R.string.export_trackers_signal_strength, beacon.rssi), MARGIN, yPos, textPaint)
+ yPos += NORMAL_LINE_HEIGHT + LINE_SPACING // Add extra spacing between beacon entries
}
- // --- Footer ---
- // Draw footer at a fixed position near the bottom
- canvas.drawText(getString(R.string.export_trackers_created_by), 50f, 1900f, textPaint)
+ // --- Final Footer ---
+ drawPageFooter(canvas, currentPageNumber, totalPages, textPaint) // Draw footer on the last page
+
+ pdfDocument.finishPage(currentPage) // Finish the last page
- pdfDocument.finishPage(pageContext.page)
+ // --- Write to Output Stream ---
pdfDocument.writeTo(outputStream)
+ Timber.d("PDF rendering finished successfully.")
outputStream.toByteArray()
} catch (e: Exception) {
Timber.e(e, "Error generating PDF content")
@@ -330,42 +449,107 @@ class ExportDeviceFragment: Fragment() {
} finally {
pdfDocument.close()
outputStream.close()
+ Timber.d("PDF Document closed.")
+ }
+ }
+
+ private suspend fun createMapBitmap(): Bitmap = withContext(Dispatchers.Main) {
+ // Ensure map view has valid dimensions
+ if (mapView.width <= 0 || mapView.height <= 0) {
+ Timber.w("MapView dimensions are invalid, returning placeholder bitmap")
+ // Return a small placeholder or throw an error
+ return@withContext createBitmap(100, 100, Bitmap.Config.ARGB_8888).apply { eraseColor(Color.LTGRAY) }
+ }
+
+ Timber.d("Creating map bitmap with dimensions: ${mapView.width}x${mapView.height}")
+ val bitmap = createBitmap(mapView.width, mapView.height, Bitmap.Config.ARGB_8888)
+ val canvas = Canvas(bitmap)
+ mapView.draw(canvas)
+ Timber.d("Map drawing onto bitmap canvas complete.")
+ bitmap
+ }
+
+ private fun drawPageFooter(canvas: Canvas, pageNumber: Int, totalPages: Int, paint: Paint) {
+ val footerText = getString(R.string.export_trackers_created_by)
+ val pageNumberText = getString(R.string.export_page_number, pageNumber, totalPages)
+
+ val footerY = PAGE_HEIGHT - MARGIN / 2 // Position slightly above the bottom margin
+ val pageNumberY = footerY
+
+ canvas.drawText(footerText, MARGIN, footerY, paint)
+
+ // Draw "Page X / Y" on the right
+ val pageNumWidth = paint.measureText(pageNumberText)
+ val pageNumberX = PAGE_WIDTH - MARGIN - pageNumWidth
+ canvas.drawText(pageNumberText, pageNumberX, pageNumberY, paint)
+ Timber.v("Drew footer for page $pageNumber / $totalPages")
+ }
+
+ private fun getBeaconEntryHeight(beaconLocation: de.seemoo.at_tracking_detection.database.models.Location?): Float {
+ var lines = 3 // Time, Location Text, Signal Strength
+ if (beaconLocation != null) {
+ lines += 1 // Add line for Google Maps link
}
+ return lines * NORMAL_LINE_HEIGHT
}
-// private fun generatePdfToUri(device: BaseDevice, uri: Uri) {
-// val context = context ?: return // Handle detached state
-//
-//// val mapView = MapView(context).apply {
-//// setTileSource(TileSourceFactory.MAPNIK)
-//// minZoomLevel = 4.0
-//// maxZoomLevel = 19.0
-//// controller.setZoom(12.0)
-////
-//// // TODO: replace with existing logic
-////
-//// // Add markers for each location
-//// locations.forEach { location ->
-//// val marker = Marker(this).apply {
-//// position = GeoPoint(location.latitude, location.longitude)
-//// setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
-//// }
-//// overlays.add(marker)
-//// }
-//// }
-////
-//// // Measure and layout map view
-//// mapView.measure(
-//// View.MeasureSpec.makeMeasureSpec(1000, View.MeasureSpec.EXACTLY),
-//// View.MeasureSpec.makeMeasureSpec(600, View.MeasureSpec.EXACTLY)
-//// )
-//// mapView.layout(0, 0, 1000, 600)
-////
-//// // Draw map to canvas
-//// mapView.draw(canvas.apply { translate(50f, yPos + 40f) })
-//// yPos += 640f
-//
-// }
+ private suspend fun calculateTotalPages(
+ device: BaseDevice,
+ beacons: List,
+ locations: List
+ ): Int = withContext(Dispatchers.Default) { // Use Default dispatcher for calculations
+ var pageCount = 1
+ var yPos = MARGIN
+ val drawablePageHeight = PAGE_HEIGHT - MARGIN - FOOTER_HEIGHT - LINE_SPACING
+
+ // --- Simulate Header ---
+ yPos += TEXT_SIZE_HEADER + SECTION_SPACING
+
+ // --- Simulate Basic Info ---
+ // Approximate height: Title line + 5 lines of info + section spacing
+ var basicInfoHeight = NORMAL_LINE_HEIGHT // Title
+ basicInfoHeight += if (device.deviceType != DeviceType.SAMSUNG_TRACKER && device.deviceType != DeviceType.SAMSUNG_FIND_MY_MOBILE) 5 * NORMAL_LINE_HEIGHT else 4 * NORMAL_LINE_HEIGHT
+ basicInfoHeight += SECTION_SPACING
+ yPos += basicInfoHeight
+
+
+ // --- Simulate Map ---
+ val mapBitmap = createMapBitmap() // Need to create it to get dimensions
+ val mapHeight = (mapBitmap.height * (PAGE_WIDTH - 2 * MARGIN) / mapBitmap.width).toFloat()
+ mapBitmap.recycle()
+
+ val mapSectionHeight = NORMAL_LINE_HEIGHT + mapHeight + SECTION_SPACING // Title + Map + Spacing
+
+ if (yPos + mapSectionHeight > drawablePageHeight) {
+ pageCount++
+ yPos = MARGIN // Reset for new page
+ }
+ yPos += mapSectionHeight
+
+
+ // --- Simulate Beacon Details ---
+ val beaconListHeaderHeight = NORMAL_LINE_HEIGHT + LINE_SPACING // "Detections:" title + spacing
+ if (yPos + beaconListHeaderHeight > drawablePageHeight) { // Check if header itself needs new page
+ pageCount++
+ yPos = MARGIN
+ }
+ yPos += beaconListHeaderHeight
+
+ for (beacon in beacons) {
+ val beaconLocation = locations.find { it.locationId == beacon.locationId }
+ val entryHeight = getBeaconEntryHeight(beaconLocation) + LINE_SPACING // Add spacing between entries
+
+ if (yPos + entryHeight > drawablePageHeight) {
+ pageCount++
+ yPos = MARGIN // Reset for new page
+ // Add height for potential "Detections (Continued)" header on new page
+ yPos += NORMAL_LINE_HEIGHT
+ }
+ yPos += entryHeight
+ }
+
+ pageCount
+ }
companion object {
private const val STORAGE_PERMISSION_CODE = 1001
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingFragment.kt
index 5b1acdad..70f371e9 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingFragment.kt
@@ -156,14 +156,13 @@ class TrackingFragment : Fragment() {
mapView.addMapListener(object : MapListener {
override fun onZoom(event: ZoomEvent?): Boolean {
- if (mapView.zoomLevelDouble >= 0 && mapView.zoomLevelDouble <= mapView.maxZoomLevel) {
- zoomToMarkers()
- }
- return true
+ Timber.d("MapListener: Zoom event received (zoom level: ${event?.zoomLevel})")
+ return false
}
override fun onScroll(event: ScrollEvent?): Boolean {
- return true
+ Timber.d("MapListener: Scroll event received")
+ return false
}
})
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 83856727..505cdf6b 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
@@ -130,6 +130,7 @@ object Utility {
map.setTileSource(TileSourceFactory.MAPNIK)
map.setUseDataConnection(true)
map.setMultiTouchControls(true)
+ map.maxZoomLevel = MAX_ZOOM_LEVEL
map.overlays.add(copyrightOverlay)
}
@@ -143,10 +144,14 @@ object Utility {
val mapController = map.controller
val geoPointList = ArrayList()
- val iconDrawable = R.drawable.ic_baseline_location_on_45_black
+ val icon = R.drawable.ic_baseline_location_on_45_black
+ val iconDrawable = ContextCompat.getDrawable(
+ context, icon
+
+ )
val clusterer = RadiusMarkerClusterer(context)
- val clusterIcon = BonusPackHelper.getBitmapFromVectorDrawable(context, iconDrawable)
+ val clusterIcon = BonusPackHelper.getBitmapFromVectorDrawable(context, icon)
clusterer.setIcon(clusterIcon)
clusterer.setRadius(100)
clusterer.mAnchorU = Marker.ANCHOR_CENTER
@@ -160,10 +165,7 @@ object Utility {
val marker = Marker(map)
val geoPoint = GeoPoint(location.latitude, location.longitude)
marker.position = geoPoint
- marker.icon = ContextCompat.getDrawable(
- context,
- iconDrawable
- )
+ marker.icon = iconDrawable
marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
geoPointList.add(geoPoint)
@@ -181,6 +183,7 @@ object Utility {
if (geoPointList.isEmpty()) {
mapController.setZoom(MAX_ZOOM_LEVEL)
+ map.post { map.invalidate() }
return false
}
diff --git a/app/src/main/res/layout/fragment_export_device.xml b/app/src/main/res/layout/fragment_export_device.xml
index 49413f9c..6afd6a3b 100644
--- a/app/src/main/res/layout/fragment_export_device.xml
+++ b/app/src/main/res/layout/fragment_export_device.xml
@@ -14,27 +14,40 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
-
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
-
-
-
-
+ android:layout_height="300dp"
+ android:layout_margin="16dp"/>
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 0ee7b9cf..bdb88ab4 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -407,16 +407,22 @@
Dokument erstellenDieser Tracker folgte dirDieser Tracker folgte dir nicht
- "Unbekannter Standort"
- "Tracker Typ: %s"
- "MAC-Adresse: %s"
- "Zuerst gesehen am: %s"
- "Zuletzt gesehen am: %s"
- "Wie oft erkannt: %d"
- "Erkannt an Standorten: %d"
- Uhrzeit: %s"
- Standort: Latitude: %s, Longitude: %s"
- Karte: %s"
- Signalstärke: %d"
+ Unbekannter Standort
+ Tracker Typ: %s
+ MAC-Adresse: %s
+ Zuerst gesehen am: %s
+ Zuletzt gesehen am: %s
+ Wie oft erkannt: %d
+ Erkannt an Standorten: %d
+ Uhrzeit: %s
+ Standort: Latitude: %s, Longitude: %s
+ Karte: %s
+ Karte ist noch nicht geladen...
+ Signalstärke: %dErstellt von AirGuard auf Android
+ Informationen:
+ Karte:
+ Dieser Tracker wurde gefunden am:
+ Dieser Tracker wurde gefunden am (Fortsetzung):
+ Seite %d/%d
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index bd56105c..cce40c63 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -450,16 +450,22 @@
Create documentThe tracker has been following youThe tracker has not been following you
- "Location: Unknown"
- "Tracker Type: %s"
- "MAC Address: %s"
- "Last Seen: %s"
- "First Seen: %s"
- "Detections: %d"
- "Unique Locations: %d"
+ Location: Unknown
+ Tracker Type: %s
+ MAC Address: %s
+ Last Seen: %s
+ First Seen: %s
+ Detections: %d
+ Unique Locations: %dTime: %s"Location: Latitude: %s, Longitude: %s"
- Map: %s"
+ Map: %s
+ Map still loading...Signal Strength: %d"Created by AirGuard for Android
+ Information:
+ Map:
+ This tracker was found at:
+ This tracker was found at (continuation):
+ Page %d/%d
From b8a3b66540445bfc09db98096eafb5fa334ea614 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sat, 26 Apr 2025 15:01:30 +0100
Subject: [PATCH 06/71] add warning for 15 minute algorithm matching in pdf
export
---
.../ui/tracking/ExportDeviceFragment.kt | 21 +++++++++++++++----
.../at_tracking_detection/util/Utility.kt | 11 ++++++++++
app/src/main/res/values-de/strings.xml | 1 +
app/src/main/res/values/strings.xml | 1 +
4 files changed, 30 insertions(+), 4 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
index 179db2c4..0ecf65ce 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
@@ -33,6 +33,7 @@ import de.seemoo.at_tracking_detection.R
import de.seemoo.at_tracking_detection.database.models.Beacon
import de.seemoo.at_tracking_detection.database.models.Location
import de.seemoo.at_tracking_detection.database.models.device.BaseDevice
+import de.seemoo.at_tracking_detection.database.models.device.ConnectionState
import de.seemoo.at_tracking_detection.database.models.device.DeviceManager
import de.seemoo.at_tracking_detection.database.models.device.DeviceType
import de.seemoo.at_tracking_detection.database.repository.BeaconRepository
@@ -394,7 +395,10 @@ class ExportDeviceFragment: Fragment() {
for (beacon in beacons) {
val beaconLocation = locations.find { it.locationId == beacon.locationId }
- val beaconEntryHeight = getBeaconEntryHeight(beaconLocation)
+ val beaconConnectionState: ConnectionState = Utility.getConnectionStateFromString(beacon.connectionState)
+ val show15MinuteWarning = beaconConnectionState !in DeviceManager.unsafeConnectionState
+
+ val beaconEntryHeight = getBeaconEntryHeight(beaconLocation, show15MinuteWarning)
// Check if the *entire* beacon entry fits on the current page
if (yPos + beaconEntryHeight > drawablePageHeight) {
@@ -425,11 +429,15 @@ class ExportDeviceFragment: Fragment() {
if (beaconLocation != null) {
val googleMapsLink = "https://maps.google.com/?q=${beaconLocation.latitude},${beaconLocation.longitude}"
- // TODO: This is currently just blue, figure out a way to make this clickabel
canvas.drawText(getString(R.string.export_trackers_map, googleMapsLink), MARGIN, yPos, linkPaint)
yPos += NORMAL_LINE_HEIGHT
}
+ if (show15MinuteWarning) {
+ canvas.drawText(getString(R.string.export_trackers_15_minute_warning), MARGIN, yPos, textPaint)
+ yPos += NORMAL_LINE_HEIGHT
+ }
+
canvas.drawText(getString(R.string.export_trackers_signal_strength, beacon.rssi), MARGIN, yPos, textPaint)
yPos += NORMAL_LINE_HEIGHT + LINE_SPACING // Add extra spacing between beacon entries
}
@@ -485,11 +493,14 @@ class ExportDeviceFragment: Fragment() {
Timber.v("Drew footer for page $pageNumber / $totalPages")
}
- private fun getBeaconEntryHeight(beaconLocation: de.seemoo.at_tracking_detection.database.models.Location?): Float {
+ private fun getBeaconEntryHeight(beaconLocation: Location?, show15MinuteWarning: Boolean): Float {
var lines = 3 // Time, Location Text, Signal Strength
if (beaconLocation != null) {
lines += 1 // Add line for Google Maps link
}
+ if (show15MinuteWarning) {
+ lines += 1 // Add line for 15-minute warning
+ }
return lines * NORMAL_LINE_HEIGHT
}
@@ -537,7 +548,9 @@ class ExportDeviceFragment: Fragment() {
for (beacon in beacons) {
val beaconLocation = locations.find { it.locationId == beacon.locationId }
- val entryHeight = getBeaconEntryHeight(beaconLocation) + LINE_SPACING // Add spacing between entries
+ val beaconConnectionState: ConnectionState = Utility.getConnectionStateFromString(beacon.connectionState)
+ val show15MinuteWarning = beaconConnectionState !in DeviceManager.unsafeConnectionState
+ val entryHeight = getBeaconEntryHeight(beaconLocation, show15MinuteWarning) + LINE_SPACING // Add spacing between entries
if (yPos + entryHeight > drawablePageHeight) {
pageCount++
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 505cdf6b..16d92cd0 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
@@ -265,6 +265,17 @@ object Utility {
}
}
+ fun getConnectionStateFromString(connectionState: String): ConnectionState {
+ return when (connectionState) {
+ "CONNECTED" -> ConnectionState.CONNECTED
+ "OFFLINE" -> ConnectionState.OFFLINE
+ "OVERMATURE_OFFLINE" -> ConnectionState.OVERMATURE_OFFLINE
+ "PREMATURE_OFFLINE" -> ConnectionState.PREMATURE_OFFLINE
+ "UNKNOWN" -> ConnectionState.UNKNOWN
+ else -> ConnectionState.UNKNOWN
+ }
+ }
+
fun getExplanationTextForDeviceType(deviceType: DeviceType?): String {
Timber.d("get Explanation for DeviceType: $deviceType")
return when (deviceType) {
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index bdb88ab4..2d2362f9 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -425,4 +425,5 @@
Dieser Tracker wurde gefunden am:Dieser Tracker wurde gefunden am (Fortsetzung):Seite %d/%d
+ Hinweis: Dieser Eintrag ist eine Schätzung. Es könnte sich um eine falsch positive Zuordnung handeln.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index cce40c63..07381a7c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -468,4 +468,5 @@
This tracker was found at:This tracker was found at (continuation):Page %d/%d
+ Warning: This Entry was matched based on likelihoods. This might be false positive.
From 219fa4e9e61293f54a7f412d8cad03df0e7f9f5d Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sat, 26 Apr 2025 15:55:58 +0100
Subject: [PATCH 07/71] add preview before creating pdf document
---
.../ui/tracking/BeaconPreviewAdapter.kt | 51 +++++++
.../ui/tracking/ExportDeviceFragment.kt | 34 ++++-
.../ui/tracking/ExportDeviceViewModel.kt | 90 +++++++++++-
.../at_tracking_detection/util/Utility.kt | 4 +-
.../res/layout/fragment_export_device.xml | 132 ++++++++++++++----
.../main/res/layout/item_beacon_preview.xml | 27 ++++
6 files changed, 303 insertions(+), 35 deletions(-)
create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/BeaconPreviewAdapter.kt
create mode 100644 app/src/main/res/layout/item_beacon_preview.xml
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/BeaconPreviewAdapter.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/BeaconPreviewAdapter.kt
new file mode 100644
index 00000000..eadaf749
--- /dev/null
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/BeaconPreviewAdapter.kt
@@ -0,0 +1,51 @@
+package de.seemoo.at_tracking_detection.ui.tracking // Or your adapters package
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import de.seemoo.at_tracking_detection.R
+
+// Data class to hold the simplified info for one beacon row
+data class BeaconPreviewItem(
+ val id: Int, // Unique ID for DiffUtil (can use beacon ID or hash)
+ val timeText: String,
+ val locationText: String
+)
+
+class BeaconPreviewAdapter : ListAdapter(BeaconDiffCallback()) {
+
+ class BeaconViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val timeTextView: TextView = itemView.findViewById(R.id.beacon_time_textview)
+ private val locationTextView: TextView = itemView.findViewById(R.id.beacon_location_textview)
+
+ fun bind(item: BeaconPreviewItem) {
+ timeTextView.text = item.timeText
+ locationTextView.text = item.locationText
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BeaconViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_beacon_preview, parent, false)
+ return BeaconViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: BeaconViewHolder, position: Int) {
+ val item = getItem(position)
+ holder.bind(item)
+ }
+}
+
+class BeaconDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: BeaconPreviewItem, newItem: BeaconPreviewItem): Boolean {
+ return oldItem.id == newItem.id // Use a unique ID per beacon
+ }
+
+ override fun areContentsTheSame(oldItem: BeaconPreviewItem, newItem: BeaconPreviewItem): Boolean {
+ return oldItem == newItem // Rely on data class equals()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
index 0ecf65ce..b90b46d6 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
@@ -28,6 +28,8 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import de.seemoo.at_tracking_detection.R
import de.seemoo.at_tracking_detection.database.models.Beacon
@@ -41,10 +43,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.databinding.FragmentExportDeviceBinding
-import de.seemoo.at_tracking_detection.util.SharedPrefs
import de.seemoo.at_tracking_detection.util.Utility
-import de.seemoo.at_tracking_detection.util.risk.RiskLevel
-import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator.Companion.checkRiskLevelForDevice
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -78,6 +77,9 @@ class ExportDeviceFragment: Fragment() {
private lateinit var binding: FragmentExportDeviceBinding
+ private lateinit var beaconPreviewAdapter: BeaconPreviewAdapter
+ private lateinit var beaconsRecyclerView: RecyclerView
+
private val PAGE_WIDTH = 1200
private val PAGE_HEIGHT = 2000
private val MARGIN = 50f
@@ -105,6 +107,8 @@ class ExportDeviceFragment: Fragment() {
deviceAddress = safeArgs.deviceAddress
+ beaconPreviewAdapter = BeaconPreviewAdapter()
+
return binding.root
}
@@ -112,6 +116,12 @@ class ExportDeviceFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ beaconsRecyclerView = binding.beaconsRecyclerview // Use binding
+ setupRecyclerView()
+ observeViewModel()
+
+ viewModel.loadDevice(deviceAddress, requireContext())
+
mapView = view.findViewById(R.id.map_preview)
Utility.basicMapSetup(mapView)
@@ -146,6 +156,20 @@ class ExportDeviceFragment: Fragment() {
mapView.onPause()
}
+ private fun setupRecyclerView() {
+ beaconsRecyclerView.apply {
+ layoutManager = LinearLayoutManager(requireContext())
+ adapter = beaconPreviewAdapter
+ // setHasFixedSize(true)
+ }
+ }
+
+ private fun observeViewModel() {
+ viewModel.beaconPreviewList.observe(viewLifecycleOwner) { beaconList ->
+ beaconPreviewAdapter.submitList(beaconList)
+ }
+ }
+
private fun setupMapContent() {
lifecycleScope.launch {
val locations = locationRepository.getLocationsForBeacon(deviceAddress!!)
@@ -334,9 +358,7 @@ class ExportDeviceFragment: Fragment() {
val drawablePageHeight = PAGE_HEIGHT - MARGIN - FOOTER_HEIGHT - LINE_SPACING // Usable height for content
- val useLocation = SharedPrefs.useLocationInTrackingDetection
- val deviceRiskLevel = checkRiskLevelForDevice(device, useLocation)
- val trackerFollowing = deviceRiskLevel != RiskLevel.LOW
+ val trackerFollowing = ExportDeviceViewModel.isTrackerFollowing(device)
headerPaint.color = if (trackerFollowing) Color.RED else Color.BLUE
val headerText = if (trackerFollowing) getString(R.string.export_trackers_following) else getString(R.string.export_trackers_not_following)
canvas.drawText(headerText, MARGIN, yPos + TEXT_SIZE_HEADER, headerPaint)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
index cbbb03d2..b7bc0070 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
@@ -1,5 +1,93 @@
package de.seemoo.at_tracking_detection.ui.tracking
+import android.content.Context
+import android.graphics.Color
+import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
+import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
+import de.seemoo.at_tracking_detection.R
+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.DeviceType
+import de.seemoo.at_tracking_detection.util.SharedPrefs
+import de.seemoo.at_tracking_detection.util.risk.RiskLevel
+import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator.Companion.checkRiskLevelForDevice
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
-class ExportDeviceViewModel: ViewModel()
\ No newline at end of file
+class ExportDeviceViewModel : ViewModel() {
+ val followingStatusText = MutableLiveData()
+ var followingStatusColor = MutableLiveData(Color.RED)
+ val basicInfoText = MutableLiveData("Loading")
+ val beaconPreviewList = MutableLiveData>()
+
+ private val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
+
+ fun loadDevice(deviceAddress: String?, context: Context) {
+ if (deviceAddress == null) {
+ return
+ }
+
+ val deviceRepository = ATTrackingDetectionApplication.getCurrentApp().deviceRepository
+ val device = deviceRepository.getDevice(deviceAddress)
+ val beaconRepository = ATTrackingDetectionApplication.getCurrentApp().beaconRepository
+ val beacons = beaconRepository.getDeviceBeacons(deviceAddress)
+ val locationRepository = ATTrackingDetectionApplication.getCurrentApp().locationRepository
+ val locations = locationRepository.getLocationsForBeacon(deviceAddress)
+
+ if (device != null) {
+ val trackerFollowing = isTrackerFollowing(device)
+ if (trackerFollowing) {
+ followingStatusText.postValue(context.getString(R.string.export_trackers_following))
+ followingStatusColor.postValue(Color.RED)
+ } else {
+ followingStatusText.postValue(context.getString(R.string.export_trackers_not_following))
+ followingStatusColor.postValue(Color.BLUE)
+ }
+
+ val deviceTypeStr = DeviceManager.deviceTypeToString(device.deviceType ?: DeviceType.UNKNOWN)
+ val lastSeenDate = device.lastSeen.format(dateTimeFormatter)
+ val firstSeenDate = device.firstDiscovery.format(dateTimeFormatter)
+ val detectionCount = beacons.size
+ val uniqueLocationCount = locations.size
+
+ val infoBuilder = StringBuilder()
+
+ infoBuilder.append(context.getString(R.string.export_trackers_tracker_type, deviceTypeStr)).append("\n")
+ if (device.deviceType != DeviceType.SAMSUNG_TRACKER && device.deviceType != DeviceType.SAMSUNG_FIND_MY_MOBILE) {
+ infoBuilder.append(context.getString(R.string.export_trackers_mac, device.address)).append("\n")
+ }
+ infoBuilder.append(context.getString(R.string.export_trackers_first_seen, firstSeenDate)).append("\n")
+ infoBuilder.append(context.getString(R.string.export_trackers_last_seen, lastSeenDate)).append("\n")
+ infoBuilder.append(context.getString(R.string.export_trackers_detections, detectionCount)).append("\n")
+ infoBuilder.append(context.getString(R.string.export_trackers_unique_locations, uniqueLocationCount))
+
+ basicInfoText.postValue(infoBuilder.toString())
+
+ val previewItems = beacons.map { beacon ->
+ val beaconLocation = locations.find { it.locationId == beacon.locationId }
+ val timeStr = context.getString(R.string.export_trackers_time, beacon.receivedAt.format(dateTimeFormatter))
+ val locationStr = if (beaconLocation != null) {
+ context.getString(R.string.export_trackers_location, "%.4f".format(beaconLocation.latitude), "%.4f".format(beaconLocation.longitude))
+ } else {
+ context.getString(R.string.export_trackers_location_unknown)
+ }
+ BeaconPreviewItem(
+ id = beacon.beaconId,
+ timeText = timeStr,
+ locationText = locationStr
+ )
+ }
+
+ beaconPreviewList.postValue(previewItems)
+ }
+ }
+
+ companion object {
+ fun isTrackerFollowing(device: BaseDevice): Boolean {
+ val useLocation = SharedPrefs.useLocationInTrackingDetection
+ val deviceRiskLevel = checkRiskLevelForDevice(device, useLocation)
+ return deviceRiskLevel != RiskLevel.LOW
+ }
+ }
+}
\ No newline at end of file
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 16d92cd0..b49f794d 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
@@ -12,7 +12,6 @@ import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
-import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
@@ -22,6 +21,7 @@ import androidx.core.app.ActivityCompat.requestPermissions
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.app.ActivityCompat.startActivity
import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
import com.google.android.material.snackbar.Snackbar
import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
import de.seemoo.at_tracking_detection.BuildConfig
@@ -407,7 +407,7 @@ object Utility {
url
}
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(finalUrl))
+ val intent = Intent(Intent.ACTION_VIEW, finalUrl.toUri())
// Check if there's an app to handle this intent
try {
diff --git a/app/src/main/res/layout/fragment_export_device.xml b/app/src/main/res/layout/fragment_export_device.xml
index 6afd6a3b..df945130 100644
--- a/app/src/main/res/layout/fragment_export_device.xml
+++ b/app/src/main/res/layout/fragment_export_device.xml
@@ -10,44 +10,124 @@
-
-
+
+ android:layout_height="match_parent"
+ android:layout_above="@id/bottom_bar_container"
+ android:fillViewport="true">
-
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="16dp">
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
-
+ bind:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+ android:orientation="vertical"
+ android:nestedScrollingEnabled="false"
+ tools:listitem="@layout/item_beacon_preview"
+ tools:itemCount="5" />
+
+
+
+
+
+
+
+
+
+
+
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_beacon_preview.xml b/app/src/main/res/layout/item_beacon_preview.xml
new file mode 100644
index 00000000..55190319
--- /dev/null
+++ b/app/src/main/res/layout/item_beacon_preview.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
From c930c852f3e2c3cf354f0b77ca55a4b4efa32ed7 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Tue, 29 Apr 2025 13:10:48 +0100
Subject: [PATCH 08/71] add error message, when no internet connection during
pdf creation is available
---
.../ui/tracking/ExportDeviceFragment.kt | 98 +++++++++++++------
.../at_tracking_detection/util/Utility.kt | 15 +++
.../res/layout/fragment_export_device.xml | 11 +++
app/src/main/res/values/strings.xml | 1 +
4 files changed, 94 insertions(+), 31 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
index b90b46d6..9c42e55f 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
@@ -20,6 +20,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ProgressBar
+import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.graphics.createBitmap
@@ -72,6 +73,7 @@ class ExportDeviceFragment: Fragment() {
private lateinit var exportDocumentButton: Button
private lateinit var progressBar: ProgressBar
+ private lateinit var noInternetTextView: TextView
private lateinit var mapView: MapView
private var isMapReady = false
@@ -122,16 +124,15 @@ class ExportDeviceFragment: Fragment() {
viewModel.loadDevice(deviceAddress, requireContext())
- mapView = view.findViewById(R.id.map_preview)
+ mapView = binding.mapPreview
+ noInternetTextView = binding.noInternetTextView
Utility.basicMapSetup(mapView)
mapView.setMultiTouchControls(false)
mapView.setBuiltInZoomControls(false)
mapView.setOnTouchListener { _, _ -> true }
- view.post {
- setupMapContent()
- }
+ checkInternetAndLoadMap()
exportDocumentButton = view.findViewById(R.id.create_document_button)
progressBar = view.findViewById(R.id.progress_bar)
@@ -203,6 +204,22 @@ class ExportDeviceFragment: Fragment() {
}
}
+ private fun checkInternetAndLoadMap() {
+ Timber.d("Checking internet connection for map loading")
+ if (Utility.isInternetAvailable(requireContext())) {
+ mapView.visibility = View.VISIBLE
+ noInternetTextView.visibility = View.GONE
+ view?.post {
+ Timber.d("Internet available, setting up map content")
+ setupMapContent()
+ }
+ } else {
+ Timber.d("No internet connection, showing no internet message")
+ mapView.visibility = View.GONE
+ noInternetTextView.visibility = View.VISIBLE
+ }
+ }
+
@Deprecated("Deprecated in Java")
override fun onRequestPermissionsResult(
requestCode: Int,
@@ -331,6 +348,9 @@ class ExportDeviceFragment: Fragment() {
val beacons = beaconRepository.getDeviceBeacons(device.address)
val locations = locationRepository.getLocationsForBeacon(device.address)
+ val hasInternet = Utility.isInternetAvailable(requireContext())
+ Timber.d("Internet available for PDF generation: $hasInternet")
+
val outputStream = ByteArrayOutputStream()
val pdfDocument = PdfDocument()
@@ -355,6 +375,7 @@ class ExportDeviceFragment: Fragment() {
val boldTextPaint = Paint(textPaint).apply { typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) }
val linkPaint = Paint(textPaint).apply { color = Color.BLUE; isUnderlineText = true }
val headerPaint = Paint().apply { textSize = TEXT_SIZE_HEADER; isAntiAlias = true; typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) }
+ val centerTextPaint = Paint(textPaint).apply { textAlign = Paint.Align.CENTER }
val drawablePageHeight = PAGE_HEIGHT - MARGIN - FOOTER_HEIGHT - LINE_SPACING // Usable height for content
@@ -390,25 +411,37 @@ class ExportDeviceFragment: Fragment() {
yPos += SECTION_SPACING
// --- Map Section ---
- val mapBitmap = createMapBitmap()
- val mapHeight = (mapBitmap.height * (PAGE_WIDTH - 2 * MARGIN) / mapBitmap.width)
- val mapRect = Rect(MARGIN.toInt(), yPos.toInt(), (PAGE_WIDTH - MARGIN).toInt(), (yPos + mapHeight).toInt())
- if (yPos + mapHeight > drawablePageHeight) {
- Timber.d("Map doesn't fit on page $currentPageNumber, creating new page.")
- drawPageFooter(canvas, currentPageNumber, totalPages, textPaint) // Add footer to current page
- pdfDocument.finishPage(currentPage) // Finish current page
- currentPageNumber++
- pageInfo = PdfDocument.PageInfo.Builder(PAGE_WIDTH, PAGE_HEIGHT, currentPageNumber).create()
- currentPage = pdfDocument.startPage(pageInfo)
- canvas = currentPage.canvas
- yPos = MARGIN // Reset yPos for new page
+ if (hasInternet) {
+ val mapBitmap = createMapBitmap()
+ val mapHeight = (mapBitmap.height * (PAGE_WIDTH - 2 * MARGIN) / mapBitmap.width)
+ val mapRect = Rect(MARGIN.toInt(), yPos.toInt(), (PAGE_WIDTH - MARGIN).toInt(), (yPos + mapHeight).toInt())
+ if (yPos + mapHeight > drawablePageHeight) {
+ Timber.d("Map doesn't fit on page $currentPageNumber, creating new page.")
+ drawPageFooter(canvas, currentPageNumber, totalPages, textPaint) // Add footer to current page
+ pdfDocument.finishPage(currentPage) // Finish current page
+ currentPageNumber++
+ pageInfo = PdfDocument.PageInfo.Builder(PAGE_WIDTH, PAGE_HEIGHT, currentPageNumber).create()
+ currentPage = pdfDocument.startPage(pageInfo)
+ canvas = currentPage.canvas
+ yPos = MARGIN // Reset yPos for new page
+ }
+
+ canvas.drawText(getString(R.string.export_trackers_map_title), MARGIN, yPos, boldTextPaint)
+ yPos += NORMAL_LINE_HEIGHT
+ canvas.drawBitmap(mapBitmap, null, mapRect, null) // Draw map scaled into rect
+ yPos += mapHeight + SECTION_SPACING
+ mapBitmap.recycle()
+ } else {
+ val noInternetText = getString(R.string.export_trackers_no_internet)
+ val placeholderHeight = 300f // Same height as MapView in layout
+ // Center the text within this placeholder area
+ val textBounds = Rect()
+ centerTextPaint.getTextBounds(noInternetText, 0, noInternetText.length, textBounds)
+ canvas.drawText(noInternetText, PAGE_WIDTH / 2f, yPos + (placeholderHeight / 2) , centerTextPaint)
+ yPos += placeholderHeight // Advance yPos by the placeholder height
}
- canvas.drawText(getString(R.string.export_trackers_map_title), MARGIN, yPos, boldTextPaint)
- yPos += NORMAL_LINE_HEIGHT
- canvas.drawBitmap(mapBitmap, null, mapRect, null) // Draw map scaled into rect
- yPos += mapHeight + SECTION_SPACING
- mapBitmap.recycle()
+ yPos += SECTION_SPACING
// --- Beacon Details Section ---
canvas.drawText(getString(R.string.export_trackers_detections_title), MARGIN, yPos, boldTextPaint)
@@ -545,20 +578,23 @@ class ExportDeviceFragment: Fragment() {
basicInfoHeight += SECTION_SPACING
yPos += basicInfoHeight
-
// --- Simulate Map ---
- val mapBitmap = createMapBitmap() // Need to create it to get dimensions
- val mapHeight = (mapBitmap.height * (PAGE_WIDTH - 2 * MARGIN) / mapBitmap.width).toFloat()
- mapBitmap.recycle()
+ val hasInternet = Utility.isInternetAvailable(requireContext())
+ if (hasInternet) {
+ val mapBitmap = createMapBitmap() // Need to create it to get dimensions
+ val mapHeight = (mapBitmap.height * (PAGE_WIDTH - 2 * MARGIN) / mapBitmap.width).toFloat()
+ mapBitmap.recycle()
- val mapSectionHeight = NORMAL_LINE_HEIGHT + mapHeight + SECTION_SPACING // Title + Map + Spacing
+ val mapSectionHeight = NORMAL_LINE_HEIGHT + mapHeight + SECTION_SPACING // Title + Map + Spacing
- if (yPos + mapSectionHeight > drawablePageHeight) {
- pageCount++
- yPos = MARGIN // Reset for new page
+ if (yPos + mapSectionHeight > drawablePageHeight) {
+ pageCount++
+ yPos = MARGIN // Reset for new page
+ }
+ yPos += mapSectionHeight
+ } else {
+ yPos += 300f
}
- yPos += mapSectionHeight
-
// --- Simulate Beacon Details ---
val beaconListHeaderHeight = NORMAL_LINE_HEIGHT + LINE_SPACING // "Detections:" title + spacing
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 7e2679a7..6c9b2da5 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
@@ -13,6 +13,8 @@ import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.BitmapFactory
+import android.net.ConnectivityManager
+import android.net.NetworkCapabilities
import android.os.Build
import android.os.Bundle
import android.view.View
@@ -296,6 +298,19 @@ object Utility {
}
}
+ fun isInternetAvailable(context: Context): Boolean {
+ val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val network = connectivityManager.activeNetwork ?: return false
+ val activeNetwork = connectivityManager.getNetworkCapabilities(network) ?: return false
+
+ return when {
+ activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
+ activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
+ activeNetwork.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
+ else -> false
+ }
+ }
+
@SuppressLint("MissingPermission")
suspend fun connectAndRetrieveCharacteristics(
context: Context,
diff --git a/app/src/main/res/layout/fragment_export_device.xml b/app/src/main/res/layout/fragment_export_device.xml
index df945130..da36faf7 100644
--- a/app/src/main/res/layout/fragment_export_device.xml
+++ b/app/src/main/res/layout/fragment_export_device.xml
@@ -77,6 +77,17 @@
android:layout_height="300dp"
android:layout_marginBottom="24dp"/>
+
+
+
This tracker was found at (continuation):Page %d/%dWarning: This Entry was matched based on likelihoods. This might be false positive.
+ The map could not load. Please make sure you have an active internet connection.
From 674eab5c952d6843c845d972393f3ec6906d18df Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 30 Apr 2025 16:12:50 +0100
Subject: [PATCH 09/71] fix build error
---
.../database/repository/DeviceRepository.kt | 1 +
1 file changed, 1 insertion(+)
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 81477de6..b616c4ba 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
@@ -3,6 +3,7 @@ package de.seemoo.at_tracking_detection.database.repository
import androidx.annotation.WorkerThread
import de.seemoo.at_tracking_detection.database.daos.DeviceDao
import de.seemoo.at_tracking_detection.database.models.device.BaseDevice
+import de.seemoo.at_tracking_detection.database.models.device.ConnectionState
import de.seemoo.at_tracking_detection.database.models.device.DeviceType
import de.seemoo.at_tracking_detection.database.relations.DeviceBeaconNotification
import de.seemoo.at_tracking_detection.util.Utility
From 4893204c21bc05957eb9019be067de71a0735515 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Fri, 2 May 2025 13:35:52 +0100
Subject: [PATCH 10/71] add Debug Mode toggle for disabling ble error messages
---
.../detection/BackgroundBluetoothScanner.kt | 2 +-
.../detection/PermanentBluetoothScanner.kt | 2 +-
.../at_tracking_detection/ui/settings/SettingsFragment.kt | 4 ++++
.../de/seemoo/at_tracking_detection/util/SharedPrefs.kt | 8 ++++++++
.../util/ble/OpportunisticBLEScanner.kt | 2 +-
app/src/main/res/values/strings.xml | 2 ++
app/src/main/res/xml/fragment_settings.xml | 7 +++++++
7 files changed, 24 insertions(+), 3 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
index 89286ee7..bd600532 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
@@ -286,7 +286,7 @@ object BackgroundBluetoothScanner {
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
Timber.e("Bluetooth scan failed $errorCode")
- if (BuildConfig.DEBUG) {
+ if (BuildConfig.DEBUG && SharedPrefs.sendBLEErrorMessages) {
notificationService.sendBLEErrorNotification()
}
}
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
index d2f965ab..02354326 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
@@ -346,7 +346,7 @@ object PermanentBluetoothScanner: LocationHistoryListener {
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
BLELogger.e("Bluetooth scan failed $errorCode")
- if (BuildConfig.DEBUG) {
+ if (BuildConfig.DEBUG && SharedPrefs.sendBLEErrorMessages) {
notificationService.sendBLEErrorNotification()
}
CoroutineScope(Dispatchers.IO).launch {
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 2e1f2809..d5ae0b75 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
@@ -17,6 +17,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import dagger.hilt.android.AndroidEntryPoint
import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
+import de.seemoo.at_tracking_detection.BuildConfig
import de.seemoo.at_tracking_detection.R
import de.seemoo.at_tracking_detection.util.SharedPrefs
import de.seemoo.at_tracking_detection.util.Utility
@@ -75,6 +76,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
findPreference("delete_study_data")?.isVisible = false
}
+ findPreference("send_ble_error_messages")?.isVisible = BuildConfig.DEBUG
+
+
setAdvancedModeButtonVisibility()
val deactivateBackgroundScanningPref = findPreference("deactivate_background_scanning")
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
index 60cebb7c..92a33571 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
@@ -265,6 +265,14 @@ object SharedPrefs {
sharedPreferences.edit { putBoolean("notification_priority_high", value) }
}
+ var sendBLEErrorMessages: Boolean
+ get() {
+ return sharedPreferences.getBoolean("send_ble_error_messages", true)
+ }
+ set(value) {
+ sharedPreferences.edit { putBoolean("send_ble_error_messages", value) }
+ }
+
private fun getAllDevicesFilterOptions(): Set {
val allOptions = ATTrackingDetectionApplication.getAppContext().resources.getStringArray(R.array.devicesFilterValue)
return allOptions.toSet()
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/OpportunisticBLEScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/OpportunisticBLEScanner.kt
index ec598d84..3dd6db91 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/OpportunisticBLEScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/OpportunisticBLEScanner.kt
@@ -109,7 +109,7 @@ class OpportunisticBLEScanner(var notificationService: NotificationService?) {
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
Timber.e("Bluetooth scan failed $errorCode")
- if (BuildConfig.DEBUG) {
+ if (BuildConfig.DEBUG && SharedPrefs.sendBLEErrorMessages) {
notificationService?.sendBLEErrorNotification()
}
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8577b918..26c0772f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -98,6 +98,7 @@
DarkLightThis settings will use less energy, but also lead to a worse detection of trackers
+ Toggle to disable or enable bluetooth error messagesThis device is not able to play a sound.Scan this AirTag with your phone via NFC to get more Information.Information
@@ -265,6 +266,7 @@
NoneThe app is already scanning in the background. Please wait 10s and restart the manual scanLow Power Scan Mode
+ Send Bluetooth Error MessagesIgnored devicesAirTags foundApple Find My Devices found
diff --git a/app/src/main/res/xml/fragment_settings.xml b/app/src/main/res/xml/fragment_settings.xml
index b3b1f35b..02683969 100644
--- a/app/src/main/res/xml/fragment_settings.xml
+++ b/app/src/main/res/xml/fragment_settings.xml
@@ -41,6 +41,13 @@
android:summary="@string/settings_use_low_power_ble_summary"
android:title="@string/settings_use_low_power_ble_title" />
+
+
Date: Wed, 7 May 2025 15:25:23 +0100
Subject: [PATCH 11/71] update gradle and some dependencies
---
app/build.gradle | 6 +++---
build.gradle | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 7c74f12a..a406a3c8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -160,13 +160,13 @@ dependencies {
implementation 'androidx.compose.material3:material3:1.3.2'
implementation 'androidx.compose.material3:material3-window-size-class:1.3.2'
// Animations
- implementation 'androidx.compose.animation:animation:1.7.8'
+ implementation 'androidx.compose.animation:animation:1.8.0'
// Tooling support (Previews, etc.)
- implementation 'androidx.compose.ui:ui-tooling:1.7.8'
+ implementation 'androidx.compose.ui:ui-tooling:1.8.0'
// Integration with ViewModels
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7'
// UI Tests
- androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.7.8'
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.8.0'
// When using a MDC theme
implementation "com.google.android.material:compose-theme-adapter:1.2.1"
}
diff --git a/build.gradle b/build.gradle
index 97f24404..913c02bf 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,10 +3,10 @@ buildscript {
ext.kotlin_version = '2.1.20'
ext.hilt_compiler_version = '2.56.2'
ext.hilt_version = '1.2.0'
- ext.room_version = '2.7.0'
+ ext.room_version = '2.7.1'
ext.compose_version = '1.7.3'
ext.about_libraries_version = '12.0.0'
- ext.work_version = '2.10.0'
+ ext.work_version = '2.10.1'
ext.ksp_version = '2.1.20-2.0.0'
repositories {
@@ -19,7 +19,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.9.2'
+ classpath 'com.android.tools.build:gradle:8.10.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.8.9"
From ea8726e0a806f97201395fdeb03e4119de3ec49a Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 7 May 2025 15:52:02 +0100
Subject: [PATCH 12/71] safer thread handling of PermanentBluetoothScanner
---
.../detection/PermanentBluetoothScanner.kt | 4 +--
.../worker/ScheduleWorkersReceiver.kt | 28 +++++++++----------
2 files changed, 14 insertions(+), 18 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
index 02354326..70b4d2ce 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
@@ -103,12 +103,10 @@ object PermanentBluetoothScanner: LocationHistoryListener {
private var isScanning = false
-
private var executor = Executors.newFixedThreadPool(2)
private var locationExecutor = Executors.newSingleThreadExecutor()
fun scan() {
-
if (!Utility.checkBluetoothPermission()) {
BLELogger.d("Permission to perform bluetooth scan missing")
return
@@ -130,7 +128,7 @@ object PermanentBluetoothScanner: LocationHistoryListener {
BLELogger.i("Launching new thread for background scanning")
// Launch a new thread
- executor.execute(kotlinx.coroutines.Runnable {
+ executor.execute(Runnable {
BLELogger.i("Thread for background scanning started")
BLELogger.d("Starting permanent Bluetooth scanner")
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 f0207ad3..06ab8775 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
@@ -27,34 +27,30 @@ class ScheduleWorkersReceiver: BroadcastReceiver() {
if (intent?.action == "AlarmManagerWakeUp_Schedule_BackgroundScan") {
// The app has been launched because no scan was performed since two hours
- val backgroundWorkScheduler = ATTrackingDetectionApplication.getCurrentApp()?.backgroundWorkScheduler
+ val backgroundWorkScheduler = ATTrackingDetectionApplication.getCurrentApp().backgroundWorkScheduler
//Schedule the periodic scan worker which runs every 15min
- backgroundWorkScheduler?.launch()
+ backgroundWorkScheduler.launch()
if (SharedPrefs.shareData) {
- backgroundWorkScheduler?.scheduleShareData()
+ backgroundWorkScheduler.scheduleShareData()
}
BackgroundWorkScheduler.scheduleAlarmWakeupIfScansFail()
}else {
// action = AlarmManagerWakeUp_Perform_BackgroundScan
// The app has been launched to perform another scan
BackgroundWorkScheduler.scheduleScanWithAlarm()
- val app = ATTrackingDetectionApplication?.getCurrentApp()
- if (app != null) {
-
- @OptIn(DelicateCoroutinesApi::class)
- GlobalScope.launch {
- Timber.d("Running scan launched from Alert")
- BackgroundBluetoothScanner.scanInBackground(startedFrom = "ScheduleWorkersReceiver")
- }
-
- }else {
- Timber.d("Could not find required dependencies")
+ @OptIn(DelicateCoroutinesApi::class)
+ goAsync {
+ Timber.d("Running scan launched from Alert")
+ BackgroundBluetoothScanner.scanInBackground(startedFrom = "ScheduleWorkersReceiver")
}
}
// Initiate the permanent background scanner.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PermanentBluetoothScanner.scan()
+ goAsync {
+ Timber.d("Attempting to start PermanentBluetoothScanner from ScheduleWorkersReceiver")
+ PermanentBluetoothScanner.scan()
+ }
}
}
@@ -95,6 +91,8 @@ fun BroadcastReceiver.goAsync(
GlobalScope.launch(context) {
try {
block()
+ } catch (e: Exception) {
+ Timber.e(e, "Error in async block for BroadcastReceiver")
} finally {
pendingResult.finish()
}
From 9d75911d8b0a21946e73c00d432fbc882c1212ba Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 7 May 2025 16:05:29 +0100
Subject: [PATCH 13/71] add Website to InformationFragment
---
.../ui/settings/InformationFragment.kt | 14 ++++++--
.../main/res/drawable/ic_home_black_24dp.xml | 1 +
app/src/main/res/drawable/ic_twitter.xml | 6 ----
.../main/res/layout/fragment_information.xml | 32 +++++++++++++++++++
app/src/main/res/values-de/strings.xml | 4 +--
app/src/main/res/values-ja/strings.xml | 2 --
app/src/main/res/values/strings.xml | 4 +--
7 files changed, 48 insertions(+), 15 deletions(-)
delete mode 100644 app/src/main/res/drawable/ic_twitter.xml
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/InformationFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/InformationFragment.kt
index 99f1eb67..753ad149 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/InformationFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/InformationFragment.kt
@@ -2,13 +2,13 @@ package de.seemoo.at_tracking_detection.ui.settings
import android.content.Intent
import android.content.pm.PackageManager
-import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
+import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import androidx.navigation.NavDirections
import androidx.navigation.fragment.findNavController
@@ -59,6 +59,7 @@ class InformationFragment : Fragment() {
val developer = view.findViewById(R.id.developer_mail)
val maintainer = view.findViewById(R.id.maintainer_mail)
val libraries = view.findViewById(R.id.libraries)
+ val website = view.findViewById(R.id.airguard_website)
contact.setOnClickListener {
val emailAddress = "airguard@seemoo.tu-darmstadt.de"
@@ -72,6 +73,13 @@ class InformationFragment : Fragment() {
val emailAddress = "aheinrich@seemoo.tu-darmstadt.de"
composeEmail(emailAddress)
}
+ website.setOnClickListener {
+ val intent = Intent(
+ Intent.ACTION_VIEW,
+ "https://airguard.seemoo.de/".toUri()
+ )
+ startActivity(intent)
+ }
libraries.setOnClickListener {
val directions: NavDirections =
InformationFragmentDirections.actionInformationToAboutLibs()
@@ -92,7 +100,7 @@ class InformationFragment : Fragment() {
}
private fun openAttributionLink(link: String) {
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
+ val intent = Intent(Intent.ACTION_VIEW, link.toUri())
startActivity(intent)
}
@@ -106,7 +114,7 @@ class InformationFragment : Fragment() {
private fun composeEmail(emailAddress: String) {
val emailIntent = Intent(Intent.ACTION_SENDTO).apply {
- data = Uri.parse("mailto:$emailAddress")
+ data = "mailto:$emailAddress".toUri()
putExtra(Intent.EXTRA_SUBJECT, "Subject of the email")
putExtra(Intent.EXTRA_TEXT, "Body of the email")
}
diff --git a/app/src/main/res/drawable/ic_home_black_24dp.xml b/app/src/main/res/drawable/ic_home_black_24dp.xml
index f8bb0b55..105a5210 100644
--- a/app/src/main/res/drawable/ic_home_black_24dp.xml
+++ b/app/src/main/res/drawable/ic_home_black_24dp.xml
@@ -1,6 +1,7 @@
-
-
diff --git a/app/src/main/res/layout/fragment_information.xml b/app/src/main/res/layout/fragment_information.xml
index f951d4f0..97d90005 100644
--- a/app/src/main/res/layout/fragment_information.xml
+++ b/app/src/main/res/layout/fragment_information.xml
@@ -151,6 +151,38 @@
+
+
+
+
+
+
+
+
+
+
Berechtigung erteilenAbbrechenNein danke
- Schau bei uns auf Twitter vorbei
- Twitter
+ Schau bei uns auf Homepage vorbei
+ WebsiteSuche nach Geräten!Manueller ScanBluetooth-Suche
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index c31854a4..d4196009 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -119,8 +119,6 @@
追跡デバイスをスキャンするには、Bluetoothデバイスをスキャンするための許可が必要ですBluetooth接続追跡デバイスが見つかった場合、AirGuardはその上でサウンドを再生する機能を提供します。このためには、このデバイスに接続するための許可が必要です。
- 私達のTwitterを見る
- Twitter手動スキャンスキャン結果安心してください。タグは見つかりませんでした
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 26c0772f..839ef1d5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -178,8 +178,8 @@
If a tracking device is found, AirGuard gives you the ability to play a sound on it. For this we need the permission to connect to this device.Sending NotificationsTo send you notifications when we identified that a tracker is following you, we need the permission to send notifications.
- Check out our Twitter account
- Twitter
+ Check out our Website
+ WebsiteScan Result ItemNo tracking devices were found.\nWe only scan for Apple Find My Trackers (AirTags), Google "Find My Device" Devices and Galaxy SmartTags, Pebblebees, Chipolos and Tiles.Failed to connect to device
From c3c64aba5a1beebe6344ba04f6792c99c6657329 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sun, 18 May 2025 13:22:56 +0100
Subject: [PATCH 14/71] add Website to InformationFragment
---
.../ui/tracking/ExportDeviceFragment.kt | 8 ++++----
.../ui/tracking/ExportDeviceViewModel.kt | 10 ++++++++++
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
index 9c42e55f..f4416574 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
@@ -173,7 +173,7 @@ class ExportDeviceFragment: Fragment() {
private fun setupMapContent() {
lifecycleScope.launch {
- val locations = locationRepository.getLocationsForBeacon(deviceAddress!!)
+ val locations = viewModel.retrievedLocations.value ?: locationRepository.getLocationsForBeacon(deviceAddress!!)
val geoPoints = locations.map { GeoPoint(it.latitude, it.longitude) }
// Add markers
@@ -341,12 +341,12 @@ class ExportDeviceFragment: Fragment() {
private suspend fun generatePdfContent(): ByteArray = withContext(Dispatchers.IO) {
Timber.d("Generating PDF content")
- val device = deviceRepository.getDevice(deviceAddress!!) ?: run {
+ val device = viewModel.retrievedDevice.value ?: deviceRepository.getDevice(deviceAddress!!) ?: run {
Timber.e("Device not found in database: $deviceAddress")
throw IllegalStateException("Device $deviceAddress not found for PDF export.")
}
- val beacons = beaconRepository.getDeviceBeacons(device.address)
- val locations = locationRepository.getLocationsForBeacon(device.address)
+ val beacons = viewModel.retrievedBeacons.value ?: beaconRepository.getDeviceBeacons(device.address)
+ val locations = viewModel.retrievedLocations.value ?: locationRepository.getLocationsForBeacon(device.address)
val hasInternet = Utility.isInternetAvailable(requireContext())
Timber.d("Internet available for PDF generation: $hasInternet")
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
index b7bc0070..37bfc4ac 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
@@ -6,6 +6,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
import de.seemoo.at_tracking_detection.R
+import de.seemoo.at_tracking_detection.database.models.Beacon
+import de.seemoo.at_tracking_detection.database.models.Location
import de.seemoo.at_tracking_detection.database.models.device.BaseDevice
import de.seemoo.at_tracking_detection.database.models.device.DeviceManager
import de.seemoo.at_tracking_detection.database.models.device.DeviceType
@@ -21,6 +23,11 @@ class ExportDeviceViewModel : ViewModel() {
val basicInfoText = MutableLiveData("Loading")
val beaconPreviewList = MutableLiveData>()
+ // Cached Data for the Fragment
+ val retrievedDevice = MutableLiveData()
+ val retrievedBeacons = MutableLiveData>()
+ val retrievedLocations = MutableLiveData>()
+
private val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
fun loadDevice(deviceAddress: String?, context: Context) {
@@ -30,10 +37,13 @@ class ExportDeviceViewModel : ViewModel() {
val deviceRepository = ATTrackingDetectionApplication.getCurrentApp().deviceRepository
val device = deviceRepository.getDevice(deviceAddress)
+ retrievedDevice.postValue(device)
val beaconRepository = ATTrackingDetectionApplication.getCurrentApp().beaconRepository
val beacons = beaconRepository.getDeviceBeacons(deviceAddress)
+ retrievedBeacons.postValue(beacons)
val locationRepository = ATTrackingDetectionApplication.getCurrentApp().locationRepository
val locations = locationRepository.getLocationsForBeacon(deviceAddress)
+ retrievedLocations.postValue(locations)
if (device != null) {
val trackerFollowing = isTrackerFollowing(device)
From a06f081faa1e01aad115e8c091e2ff483b4b99b1 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sun, 18 May 2025 13:34:04 +0100
Subject: [PATCH 15/71] show AirGuard Version Number on pdf report
---
.../ui/tracking/ExportDeviceFragment.kt | 6 +++++-
app/src/main/res/values-de/strings.xml | 2 +-
app/src/main/res/values/strings.xml | 2 +-
3 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
index f4416574..29efcda4 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
@@ -533,7 +533,11 @@ class ExportDeviceFragment: Fragment() {
}
private fun drawPageFooter(canvas: Canvas, pageNumber: Int, totalPages: Int, paint: Paint) {
- val footerText = getString(R.string.export_trackers_created_by)
+ val versionNumber = requireActivity().packageManager.getPackageInfo(
+ requireActivity().packageName,
+ 0
+ ).versionName
+ val footerText = getString(R.string.export_trackers_created_by, versionNumber)
val pageNumberText = getString(R.string.export_page_number, pageNumber, totalPages)
val footerY = PAGE_HEIGHT - MARGIN / 2 // Position slightly above the bottom margin
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 89c7b5eb..f05514ce 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -419,7 +419,7 @@
Karte: %sKarte ist noch nicht geladen...Signalstärke: %d
- Erstellt von AirGuard auf Android
+ Erstellt von AirGuard %s auf AndroidInformationen:Karte:Dieser Tracker wurde gefunden am:
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2f477995..31d4f75f 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -464,7 +464,7 @@
Map: %sMap still loading...Signal Strength: %d"
- Created by AirGuard for Android
+ Created by AirGuard %s for AndroidInformation:Map:This tracker was found at:
From 86f68a57da3f866bad9d91fce93ff00aefb92afe Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sun, 18 May 2025 14:58:23 +0100
Subject: [PATCH 16/71] handle tracker ignored as additional state for pdf
export, fix colors in night mode
---
.../ui/tracking/ExportDeviceFragment.kt | 2 +-
.../ui/tracking/ExportDeviceViewModel.kt | 15 ++++++++++-----
app/src/main/res/values-de/strings.xml | 1 +
app/src/main/res/values-night/colors.xml | 2 ++
app/src/main/res/values/colors.xml | 2 ++
app/src/main/res/values/strings.xml | 1 +
6 files changed, 17 insertions(+), 6 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
index 29efcda4..d62e7906 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
@@ -381,7 +381,7 @@ class ExportDeviceFragment: Fragment() {
val trackerFollowing = ExportDeviceViewModel.isTrackerFollowing(device)
headerPaint.color = if (trackerFollowing) Color.RED else Color.BLUE
- val headerText = if (trackerFollowing) getString(R.string.export_trackers_following) else getString(R.string.export_trackers_not_following)
+ val headerText = if (trackerFollowing) getString(R.string.export_trackers_following) else if (device.ignore) getString(R.string.export_trackers_ignored) else getString(R.string.export_trackers_not_following)
canvas.drawText(headerText, MARGIN, yPos + TEXT_SIZE_HEADER, headerPaint)
yPos += TEXT_SIZE_HEADER + SECTION_SPACING
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
index 37bfc4ac..0aab683d 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
@@ -1,7 +1,7 @@
package de.seemoo.at_tracking_detection.ui.tracking
import android.content.Context
-import android.graphics.Color
+import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
@@ -19,7 +19,7 @@ import java.time.format.FormatStyle
class ExportDeviceViewModel : ViewModel() {
val followingStatusText = MutableLiveData()
- var followingStatusColor = MutableLiveData(Color.RED)
+ var followingStatusColor = MutableLiveData(R.color.warning_light_red)
val basicInfoText = MutableLiveData("Loading")
val beaconPreviewList = MutableLiveData>()
@@ -49,10 +49,15 @@ class ExportDeviceViewModel : ViewModel() {
val trackerFollowing = isTrackerFollowing(device)
if (trackerFollowing) {
followingStatusText.postValue(context.getString(R.string.export_trackers_following))
- followingStatusColor.postValue(Color.RED)
+ followingStatusColor.postValue(ContextCompat.getColor(context, R.color.tracker_following_red))
} else {
- followingStatusText.postValue(context.getString(R.string.export_trackers_not_following))
- followingStatusColor.postValue(Color.BLUE)
+ if (device.ignore) {
+ followingStatusText.postValue(context.getString(R.string.export_trackers_ignored))
+ } else {
+ followingStatusText.postValue(context.getString(R.string.export_trackers_not_following))
+ }
+ followingStatusColor.postValue(ContextCompat.getColor(context, R.color.tracker_not_following_blue))
+
}
val deviceTypeStr = DeviceManager.deviceTypeToString(device.deviceType ?: DeviceType.UNKNOWN)
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index f05514ce..81ae31aa 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -407,6 +407,7 @@
Dokument erstellenDieser Tracker folgte dirDieser Tracker folgte dir nicht
+ Dieser Tracker wurde ignoriertUnbekannter StandortTracker Typ: %sMAC-Adresse: %s
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index bbe04293..89a6a25b 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -3,4 +3,6 @@
#0E8393#FFFFFF#424242
+ #8a8aff
+ #FB6161
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 108a0d8b..e7a3140c 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -20,6 +20,8 @@
#434343#1B9CAE#FF3D2F
+ #0000ff
+ #FF0000#000000#D3D3D3
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 31d4f75f..058baeb4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -452,6 +452,7 @@
Create documentThe tracker has been following youThe tracker has not been following you
+ The tracker has been ignoredLocation: UnknownTracker Type: %sMAC Address: %s
From c8d166863e9c1cbb1730458cb2580ab24eb2de30 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Fri, 23 May 2025 17:07:24 +0100
Subject: [PATCH 17/71] update kotlin version to 2.1.21 and update dependencies
---
app/build.gradle | 16 ++++++++--------
build.gradle | 6 +++---
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index a406a3c8..11fa12cb 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -92,10 +92,10 @@ dependencies {
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.vectordrawable:vectordrawable:1.2.0'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.8.9'
- implementation 'androidx.navigation:navigation-ui-ktx:2.8.9'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.7'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.9.0'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.9.0'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.0'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.cardview:cardview:1.0.0'
@@ -160,13 +160,13 @@ dependencies {
implementation 'androidx.compose.material3:material3:1.3.2'
implementation 'androidx.compose.material3:material3-window-size-class:1.3.2'
// Animations
- implementation 'androidx.compose.animation:animation:1.8.0'
+ implementation 'androidx.compose.animation:animation:1.8.2'
// Tooling support (Previews, etc.)
- implementation 'androidx.compose.ui:ui-tooling:1.8.0'
+ implementation 'androidx.compose.ui:ui-tooling:1.8.2'
// Integration with ViewModels
- implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.0'
// UI Tests
- androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.8.0'
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.8.2'
// When using a MDC theme
implementation "com.google.android.material:compose-theme-adapter:1.2.1"
}
diff --git a/build.gradle b/build.gradle
index 913c02bf..9932c767 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,13 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '2.1.20'
+ ext.kotlin_version = '2.1.21'
ext.hilt_compiler_version = '2.56.2'
ext.hilt_version = '1.2.0'
ext.room_version = '2.7.1'
ext.compose_version = '1.7.3'
- ext.about_libraries_version = '12.0.0'
+ ext.about_libraries_version = '12.1.2'
ext.work_version = '2.10.1'
- ext.ksp_version = '2.1.20-2.0.0'
+ ext.ksp_version = '2.1.21-2.0.1'
repositories {
google()
From 2a23bccaf1c4aff3f9573b1b3d18948c7413ed89 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Fri, 23 May 2025 18:00:54 +0100
Subject: [PATCH 18/71] fix onboarding loop bug
---
.../de/seemoo/at_tracking_detection/ui/OnboardingActivity.kt | 2 +-
app/src/main/res/values-de/strings.xml | 2 +-
app/src/main/res/values/strings.xml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
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 fba18e73..02c11946 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
@@ -68,9 +68,9 @@ class OnboardingActivity : AppIntro() {
SharedPrefs.useLocationInTrackingDetection = locationPermissionState && backgroundPermissionState
-
if (permission == null) {
SharedPrefs.onBoardingCompleted = true
+ SharedPrefs.showOnboarding = false
backgroundWorkScheduler.launch()
finish()
startActivity(Intent(applicationContext, MainActivity::class.java).apply {
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 81ae31aa..b577b65e 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -252,7 +252,7 @@
Umfrage zum Thema Stalking und Location TrackingDurchgeführt von der Technischen Universität DarmstadtGefundene SmartTags
- Immer den Einrichtungsdialog anzeigen
+ Beim nächsten Start den Einrichtungsdialog erneut anzeigenEinrichtungsdialog anzeigenSamsung Gerät %sBenachrichtigung mit hoher Prioriät
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 058baeb4..4d57460d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -292,7 +292,7 @@
Do you want to be reminded about the survey later?Technical University of Darmstadt conducts a survey to find out how people are affected by stalkingSurvey on stalking and location tracking
- Will always show the app onboarding procedure
+ Show the app onboarding procedure on the next launch of the appShow OnboardingChipolo %sBluetooth is turned off. This app requires Bluetooth to scan for nearby trackers.
From 61699fd289e5884587ca7b3d281a1d0528b960d6 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Fri, 23 May 2025 18:10:06 +0100
Subject: [PATCH 19/71] adjust Android 15 System Bar Configuration
---
.../at_tracking_detection/ui/MainActivity.kt | 36 ++++++-------------
1 file changed, 11 insertions(+), 25 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/MainActivity.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/MainActivity.kt
index 8c6e3b14..490fd782 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/MainActivity.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/MainActivity.kt
@@ -4,11 +4,10 @@ import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.os.StrictMode
-import android.view.WindowInsetsController
-import android.view.WindowManager
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsControllerCompat
import androidx.navigation.NavOptions
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
@@ -17,7 +16,6 @@ import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.google.android.material.bottomnavigation.BottomNavigationView
-import com.google.android.material.elevation.SurfaceColors
import dagger.hilt.android.AndroidEntryPoint
import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
import de.seemoo.at_tracking_detection.BuildConfig
@@ -116,31 +114,19 @@ class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceCh
private fun configureSystemBars() {
val isDarkTheme = Utility.isActualThemeDark(context = this)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- window.insetsController?.apply {
- systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
- if (isDarkTheme) {
- setSystemBarsAppearance(
- 0, // Clear APPEARANCE_LIGHT_STATUS_BARS
- WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
- )
- } else {
- setSystemBarsAppearance(
- WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
- WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
- )
- }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
+ windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+
+ if (isDarkTheme) {
+ windowInsetsController.isAppearanceLightStatusBars = false
+ windowInsetsController.isAppearanceLightNavigationBars = false
+ } else {
+ windowInsetsController.isAppearanceLightStatusBars = true
+ windowInsetsController.isAppearanceLightNavigationBars = true
}
- val lp = window.attributes
- lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
- window.attributes = lp
} else {
WindowCompat.setDecorFitsSystemWindows(window, false)
- window.statusBarColor = SurfaceColors.SURFACE_2.getColor(this)
- window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this)
- val lp = window.attributes
- lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
- window.attributes = lp
}
}
From 786c67e6072bceb04be4126f4ff5316d11b04531 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 26 May 2025 17:23:00 +0100
Subject: [PATCH 20/71] initial implementation of review controller
---
app/build.gradle | 5 +
.../ui/dashboard/DashboardRiskFragment.kt | 21 +++
.../ui/dashboard/ReviewController.kt | 159 ++++++++++++++++++
.../ui/dashboard/ReviewModule.kt | 20 +++
.../at_tracking_detection/util/SharedPrefs.kt | 18 ++
5 files changed, 223 insertions(+)
create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ReviewController.kt
create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ReviewModule.kt
diff --git a/app/build.gradle b/app/build.gradle
index 11fa12cb..bff098e2 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -169,6 +169,11 @@ dependencies {
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.8.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.7.0")
}
ksp {
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 5a69ddfd..fdd28878 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
@@ -19,12 +19,14 @@ import androidx.navigation.fragment.findNavController
import com.bumptech.glide.Glide
import com.google.android.material.card.MaterialCardView
import dagger.hilt.android.AndroidEntryPoint
+import de.seemoo.at_tracking_detection.BuildConfig
import de.seemoo.at_tracking_detection.R
import de.seemoo.at_tracking_detection.databinding.FragmentDashboardRiskBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
+import javax.inject.Inject
@AndroidEntryPoint
@@ -35,6 +37,9 @@ class DashboardRiskFragment : Fragment() {
private var _binding: FragmentDashboardRiskBinding? = null
private val binding get() = _binding!!
+ @Inject
+ lateinit var reviewController: ReviewController
+
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@@ -56,6 +61,9 @@ class DashboardRiskFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ // Increment app open count
+ reviewController.incrementAppOpenCount()
+
val riskCard: MaterialCardView = view.findViewById(R.id.risk_card)
riskCard.setOnClickListener {
val directions: NavDirections =
@@ -142,6 +150,19 @@ class DashboardRiskFragment : Fragment() {
progressBar.visibility = View.GONE
}
}
+
+ // Check if we should show review after data is loaded
+ 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")
+ }
}
override fun 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
new file mode 100644
index 00000000..b37c9c0e
--- /dev/null
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ReviewController.kt
@@ -0,0 +1,159 @@
+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
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
new file mode 100644
index 00000000..88732e56
--- /dev/null
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/ReviewModule.kt
@@ -0,0 +1,20 @@
+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
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
index 92a33571..11c0d6ba 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
@@ -240,6 +240,24 @@ object SharedPrefs {
sharedPreferences.edit { putString("risk_sensitivity", value) }
}
+ var appOpenCount: Int
+ // How often the app has been opened
+ get() {
+ return sharedPreferences.getInt("app_open_count", 0)
+ }
+ set(value) {
+ sharedPreferences.edit { putInt("app_open_count", value) }
+ }
+
+ var reviewShown: Boolean
+ // If the review dialog has been shown
+ get() {
+ return sharedPreferences.getBoolean("review_shown", false)
+ }
+ set(value) {
+ sharedPreferences.edit { putBoolean("review_shown", value) }
+ }
+
var devicesFilter: Set
get() {
val allOptions = getAllDevicesFilterOptions()
From 18cf3932cbb15986b8d8901084509b9e8d266f30 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sat, 7 Jun 2025 16:03:32 +0100
Subject: [PATCH 21/71] update gradle
---
build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/build.gradle b/build.gradle
index 9932c767..6822ad0d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -19,7 +19,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.10.0'
+ classpath 'com.android.tools.build:gradle:8.10.1'
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.8.9"
From 1d7ab6a87ffd35172373cb35a2180e553236cea2 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 11 Jun 2025 11:07:59 +0100
Subject: [PATCH 22/71] update dependencies
---
app/build.gradle | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 11fa12cb..a3d37d1a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -88,14 +88,14 @@ dependencies {
implementation 'com.github.bastienpaulfr:Treessence:1.1.2'
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation 'androidx.core:core-ktx:1.16.0'
- implementation 'androidx.appcompat:appcompat:1.7.0'
+ implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.vectordrawable:vectordrawable:1.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.9.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.9.0'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.0'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.1'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.cardview:cardview:1.0.0'
@@ -164,7 +164,7 @@ dependencies {
// Tooling support (Previews, etc.)
implementation 'androidx.compose.ui:ui-tooling:1.8.2'
// Integration with ViewModels
- implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.0'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1'
// UI Tests
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.8.2'
// When using a MDC theme
From 4b6f86cb06c660ea9f32194be22d34ef9e489f17 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Fri, 20 Jun 2025 17:02:25 +0100
Subject: [PATCH 23/71] fix: pdf export shows that the tracker is following
even if the notification has been longer than a month ago
---
.../ui/tracking/ExportDeviceViewModel.kt | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
index 0aab683d..351dd9c7 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
@@ -100,9 +100,14 @@ class ExportDeviceViewModel : ViewModel() {
companion object {
fun isTrackerFollowing(device: BaseDevice): Boolean {
+ // Backup in case something with the notification went wrong
val useLocation = SharedPrefs.useLocationInTrackingDetection
val deviceRiskLevel = checkRiskLevelForDevice(device, useLocation)
- return deviceRiskLevel != RiskLevel.LOW
+
+ // This is the actual logic to determine if a tracker is following
+ val notificationRepository = ATTrackingDetectionApplication.getCurrentApp().notificationRepository
+ val notificationExits = notificationRepository.existsNotificationForDevice(device.address)
+ return notificationExits || (deviceRiskLevel != RiskLevel.LOW)
}
}
}
\ No newline at end of file
From 8996f730c95379fa1091755a9a9d4df22a5434da Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Fri, 20 Jun 2025 17:23:20 +0100
Subject: [PATCH 24/71] pdf export: optimize loading times by creating map of
locations instead of using the find on the database
---
.../ui/tracking/ExportDeviceFragment.kt | 10 ++++++++--
.../ui/tracking/ExportDeviceViewModel.kt | 6 +++++-
2 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
index d62e7906..e7502cd2 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceFragment.kt
@@ -447,8 +447,11 @@ class ExportDeviceFragment: Fragment() {
canvas.drawText(getString(R.string.export_trackers_detections_title), MARGIN, yPos, boldTextPaint)
yPos += NORMAL_LINE_HEIGHT
+ // This creates a map of locations for faster lookups
+ val locationMap = locations.associateBy { it.locationId }
+
for (beacon in beacons) {
- val beaconLocation = locations.find { it.locationId == beacon.locationId }
+ val beaconLocation = locationMap[beacon.locationId]
val beaconConnectionState: ConnectionState = Utility.getConnectionStateFromString(beacon.connectionState)
val show15MinuteWarning = beaconConnectionState !in DeviceManager.unsafeConnectionState
@@ -608,8 +611,11 @@ class ExportDeviceFragment: Fragment() {
}
yPos += beaconListHeaderHeight
+ // This creates a map of locations for faster lookups
+ val locationMap = locations.associateBy { it.locationId }
+
for (beacon in beacons) {
- val beaconLocation = locations.find { it.locationId == beacon.locationId }
+ val beaconLocation = locationMap[beacon.locationId]
val beaconConnectionState: ConnectionState = Utility.getConnectionStateFromString(beacon.connectionState)
val show15MinuteWarning = beaconConnectionState !in DeviceManager.unsafeConnectionState
val entryHeight = getBeaconEntryHeight(beaconLocation, show15MinuteWarning) + LINE_SPACING // Add spacing between entries
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
index 351dd9c7..92c8f680 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/ExportDeviceViewModel.kt
@@ -79,8 +79,12 @@ class ExportDeviceViewModel : ViewModel() {
basicInfoText.postValue(infoBuilder.toString())
+ // This creates a map of locations for faster lookups
+ val locations = locationRepository.getLocationsForBeacon(deviceAddress)
+ val locationMap = locations.associateBy { it.locationId }
+
val previewItems = beacons.map { beacon ->
- val beaconLocation = locations.find { it.locationId == beacon.locationId }
+ val beaconLocation = locationMap[beacon.locationId]
val timeStr = context.getString(R.string.export_trackers_time, beacon.receivedAt.format(dateTimeFormatter))
val locationStr = if (beaconLocation != null) {
context.getString(R.string.export_trackers_location, "%.4f".format(beaconLocation.latitude), "%.4f".format(beaconLocation.longitude))
From f16978470a09cf03586750c4fb23d3bd2e2f541a Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Fri, 20 Jun 2025 17:26:37 +0100
Subject: [PATCH 25/71] optimize loading in pdf export by adding new indices to
beacon
---
.../23.json | 517 ++++++++++++++++++
.../database/AppDatabase.kt | 3 +-
.../database/models/Beacon.kt | 3 +-
3 files changed, 521 insertions(+), 2 deletions(-)
create mode 100644 app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/23.json
diff --git a/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/23.json b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/23.json
new file mode 100644
index 00000000..977b925c
--- /dev/null
+++ b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/23.json
@@ -0,0 +1,517 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 23,
+ "identityHash": "02ece92683e68b94e5d86d969409d99c",
+ "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, `subDeviceType` TEXT NOT NULL DEFAULT 'UNKNOWN', `riskLevel` INTEGER NOT NULL DEFAULT 0, `lastCalculatedRiskDate` TEXT, `nextObservationNotification` TEXT, `currentObservationDuration` INTEGER, `safeTracker` INTEGER NOT NULL DEFAULT false, `additionalData` TEXT, `alternativeIdentifier` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "deviceId",
+ "columnName": "deviceId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uniqueId",
+ "columnName": "uniqueId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ignore",
+ "columnName": "ignore",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "connectable",
+ "columnName": "connectable",
+ "affinity": "INTEGER",
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "payloadData",
+ "columnName": "payloadData",
+ "affinity": "INTEGER"
+ },
+ {
+ "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"
+ },
+ {
+ "fieldPath": "deviceType",
+ "columnName": "deviceType",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "subDeviceType",
+ "columnName": "subDeviceType",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'UNKNOWN'"
+ },
+ {
+ "fieldPath": "riskLevel",
+ "columnName": "riskLevel",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "lastCalculatedRiskDate",
+ "columnName": "lastCalculatedRiskDate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "nextObservationNotification",
+ "columnName": "nextObservationNotification",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "currentObservationDuration",
+ "columnName": "currentObservationDuration",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "safeTracker",
+ "columnName": "safeTracker",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "additionalData",
+ "columnName": "additionalData",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "alternativeIdentifier",
+ "columnName": "alternativeIdentifier",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "deviceId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_device_lastSeen",
+ "unique": false,
+ "columnNames": [
+ "lastSeen"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_device_lastSeen` ON `${TABLE_NAME}` (`lastSeen`)"
+ },
+ {
+ "name": "index_device_address",
+ "unique": true,
+ "columnNames": [
+ "address"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_device_address` ON `${TABLE_NAME}` (`address`)"
+ },
+ {
+ "name": "index_device_notificationSent",
+ "unique": false,
+ "columnNames": [
+ "notificationSent"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_device_notificationSent` ON `${TABLE_NAME}` (`notificationSent`)"
+ },
+ {
+ "name": "index_device_deviceType",
+ "unique": false,
+ "columnNames": [
+ "deviceType"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_device_deviceType` ON `${TABLE_NAME}` (`deviceType`)"
+ },
+ {
+ "name": "index_device_lastSeen_deviceType",
+ "unique": false,
+ "columnNames": [
+ "lastSeen",
+ "deviceType"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_device_lastSeen_deviceType` ON `${TABLE_NAME}` (`lastSeen`, `deviceType`)"
+ }
+ ]
+ },
+ {
+ "tableName": "notification",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceAddress` TEXT NOT NULL, `falseAlarm` INTEGER NOT NULL, `dismissed` INTEGER, `clicked` INTEGER, `createdAt` TEXT NOT NULL, `sensitivity` INTEGER NOT NULL DEFAULT -1)",
+ "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"
+ },
+ {
+ "fieldPath": "clicked",
+ "columnName": "clicked",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sensitivity",
+ "columnName": "sensitivity",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "notificationId"
+ ]
+ }
+ },
+ {
+ "tableName": "beacon",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`beaconId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `receivedAt` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `deviceAddress` TEXT NOT NULL, `locationId` INTEGER, `mfg` BLOB, `serviceUUIDs` TEXT, `connectionState` TEXT NOT NULL DEFAULT 'UNKNOWN', FOREIGN KEY(`deviceAddress`) REFERENCES `device`(`address`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "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"
+ },
+ {
+ "fieldPath": "manufacturerData",
+ "columnName": "mfg",
+ "affinity": "BLOB"
+ },
+ {
+ "fieldPath": "serviceUUIDs",
+ "columnName": "serviceUUIDs",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "connectionState",
+ "columnName": "connectionState",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'UNKNOWN'"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "beaconId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_beacon_receivedAt",
+ "unique": false,
+ "columnNames": [
+ "receivedAt"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_receivedAt` ON `${TABLE_NAME}` (`receivedAt`)"
+ },
+ {
+ "name": "index_beacon_deviceAddress",
+ "unique": false,
+ "columnNames": [
+ "deviceAddress"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_deviceAddress` ON `${TABLE_NAME}` (`deviceAddress`)"
+ },
+ {
+ "name": "index_beacon_connectionState",
+ "unique": false,
+ "columnNames": [
+ "connectionState"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_connectionState` ON `${TABLE_NAME}` (`connectionState`)"
+ },
+ {
+ "name": "index_beacon_deviceAddress_receivedAt",
+ "unique": false,
+ "columnNames": [
+ "deviceAddress",
+ "receivedAt"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_deviceAddress_receivedAt` ON `${TABLE_NAME}` (`deviceAddress`, `receivedAt`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "device",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "deviceAddress"
+ ],
+ "referencedColumns": [
+ "address"
+ ]
+ }
+ ]
+ },
+ {
+ "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"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "feedbackId"
+ ]
+ }
+ },
+ {
+ "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, `location_deg` TEXT, `location` INTEGER, `device_addresses_found` TEXT, `device_types_found` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "scanId",
+ "columnName": "scanId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "endDate",
+ "columnName": "endDate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "noDevicesFound",
+ "columnName": "noDevicesFound",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "duration",
+ "columnName": "duration",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "isManual",
+ "columnName": "isManual",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scanMode",
+ "columnName": "scanMode",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startDate",
+ "columnName": "startDate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "locationDeg",
+ "columnName": "location_deg",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "locationId",
+ "columnName": "location",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "devicesAddressesFound",
+ "columnName": "device_addresses_found",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "devicesTypesFound",
+ "columnName": "device_types_found",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "scanId"
+ ]
+ }
+ },
+ {
+ "tableName": "location",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `firstDiscovery` TEXT NOT NULL, `lastSeen` TEXT NOT NULL, `longitude` REAL NOT NULL, `latitude` REAL NOT NULL, `altitude` REAL, `accuracy` REAL)",
+ "fields": [
+ {
+ "fieldPath": "locationId",
+ "columnName": "locationId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "firstDiscovery",
+ "columnName": "firstDiscovery",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastSeen",
+ "columnName": "lastSeen",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "longitude",
+ "columnName": "longitude",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "latitude",
+ "columnName": "latitude",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "altitude",
+ "columnName": "altitude",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "accuracy",
+ "columnName": "accuracy",
+ "affinity": "REAL"
+ }
+ ],
+ "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`)"
+ }
+ ]
+ }
+ ],
+ "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, '02ece92683e68b94e5d86d969409d99c')"
+ ]
+ }
+}
\ 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 9a491efe..1d2011d8 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 = 22,
+ version = 23,
entities = [
BaseDevice::class,
Notification::class,
@@ -35,6 +35,7 @@ import de.seemoo.at_tracking_detection.util.converter.DateTimeConverter
AutoMigration(from=19, to=20),
AutoMigration(from=20, to=21),
AutoMigration(from=21, to=22),
+ AutoMigration(from=22, to=23)
],
exportSchema = true
)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Beacon.kt b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Beacon.kt
index f4c8a8c9..fe428762 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Beacon.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/database/models/Beacon.kt
@@ -14,7 +14,8 @@ import java.time.format.FormatStyle
indices = [
Index(value = ["receivedAt"]),
Index(value = ["deviceAddress"]),
- Index(value = ["connectionState"])
+ Index(value = ["connectionState"]),
+ Index(value = ["deviceAddress", "receivedAt"])
],
foreignKeys = [
ForeignKey(
From 2adf75a7544047413f7a738b6481f52008b2016b Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sat, 21 Jun 2025 14:59:14 +0100
Subject: [PATCH 26/71] fix intent handling for notifications on newer android
versions
---
.../notifications/NotificationActionReceiver.kt | 6 +++++-
.../notifications/NotificationBuilder.kt | 13 ++++++++++---
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationActionReceiver.kt b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationActionReceiver.kt
index 24c37e8b..3e504878 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationActionReceiver.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationActionReceiver.kt
@@ -32,17 +32,20 @@ class NotificationActionReceiver : BroadcastReceiver() {
@OptIn(DelicateCoroutinesApi::class)
override fun onReceive(context: Context, intent: Intent) {
Timber.d("Intent received!")
+
val notificationId = intent.getIntExtra("notificationId", -1)
if (notificationId == -1) {
Timber.e("Notification id missing!")
return
}
+
when (intent.action) {
NotificationConstants.FALSE_ALARM_ACTION -> {
Timber.d("False Alarm intent for notification $notificationId received")
GlobalScope.launch(Dispatchers.IO) {
notificationRepository.setFalseAlarm(notificationId, true)
}
+ // Dismiss the notification immediately for this action.
notificationManagerCompat.cancel(notificationId)
}
NotificationConstants.IGNORE_DEVICE_ACTION -> {
@@ -55,12 +58,14 @@ class NotificationActionReceiver : BroadcastReceiver() {
GlobalScope.launch(Dispatchers.IO) {
deviceRepository.setIgnoreFlag(deviceAddress, true)
}
+ // Dismiss the notification immediately for this action.
notificationManagerCompat.cancel(notificationId)
}
NotificationConstants.CLICKED_ACTION -> {
GlobalScope.launch(Dispatchers.IO) {
notificationRepository.setClicked(notificationId, true)
}
+ notificationManagerCompat.cancel(notificationId)
}
NotificationConstants.DISMISSED_ACTION -> {
GlobalScope.launch(Dispatchers.IO) {
@@ -68,7 +73,6 @@ class NotificationActionReceiver : BroadcastReceiver() {
}
}
}
- notificationManagerCompat.cancel(notificationId)
}
}
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 cee0c2f0..ca3f21e5 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
@@ -89,11 +89,18 @@ class NotificationBuilder @Inject constructor(
action = notificationAction
putExtras(bundle)
}
+
+ val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+
return PendingIntent.getBroadcast(
context,
code,
intent,
- PendingIntent.FLAG_IMMUTABLE
+ flags
)
}
@@ -160,7 +167,7 @@ class NotificationBuilder @Inject constructor(
NotificationConstants.FALSE_ALARM_ACTION,
NotificationConstants.FALSE_ALARM_CODE
)
- )
+ ).setAutoCancel(true)
if (baseDevice.deviceType != null && baseDevice.deviceType.canBeIgnored(ConnectionState.OVERMATURE_OFFLINE)) {
notification = notification.addAction(
@@ -171,7 +178,7 @@ class NotificationBuilder @Inject constructor(
NotificationConstants.IGNORE_DEVICE_ACTION,
NotificationConstants.IGNORE_DEVICE_CODE
)
- )
+ ).setAutoCancel(true)
}
notification = notification.setDeleteIntent(
From 9c85ad25a8ffe85f352c97bcc0407feff267f03e Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 25 Jun 2025 11:23:05 +0100
Subject: [PATCH 27/71] update dependencies, kotlin version to 2.2.0 and gradle
---
app/build.gradle | 18 +++++++++---------
build.gradle | 14 +++++++-------
gradle/wrapper/gradle-wrapper.properties | 2 +-
3 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index a3d37d1a..8b569e2d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -102,14 +102,14 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.4.0'
implementation "androidx.profileinstaller:profileinstaller:1.4.1"
- implementation 'com.squareup.retrofit2:retrofit:2.11.0'
- implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
- implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14'
- implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14'
- implementation 'com.google.code.gson:gson:2.11.0'
+ implementation 'com.squareup.retrofit2:retrofit:3.0.0'
+ implementation 'com.squareup.retrofit2:converter-gson:3.0.0'
+ implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.16'
+ implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.16'
+ implementation 'com.google.code.gson:gson:2.13.1'
implementation "androidx.work:work-testing:$work_version"
implementation 'androidx.core:core-ktx:1.16.0'
- debugImplementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14'
+ debugImplementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.16'
implementation "com.google.dagger:hilt-android:$hilt_compiler_version"
implementation "androidx.hilt:hilt-work:$hilt_version"
@@ -160,13 +160,13 @@ dependencies {
implementation 'androidx.compose.material3:material3:1.3.2'
implementation 'androidx.compose.material3:material3-window-size-class:1.3.2'
// Animations
- implementation 'androidx.compose.animation:animation:1.8.2'
+ implementation 'androidx.compose.animation:animation:1.8.3'
// Tooling support (Previews, etc.)
- implementation 'androidx.compose.ui:ui-tooling:1.8.2'
+ implementation 'androidx.compose.ui:ui-tooling:1.8.3'
// Integration with ViewModels
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1'
// UI Tests
- androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.8.2'
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.8.3'
// When using a MDC theme
implementation "com.google.android.material:compose-theme-adapter:1.2.1"
}
diff --git a/build.gradle b/build.gradle
index 6822ad0d..ea5ca5b7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,13 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '2.1.21'
+ ext.kotlin_version = '2.2.0'
ext.hilt_compiler_version = '2.56.2'
ext.hilt_version = '1.2.0'
- ext.room_version = '2.7.1'
+ ext.room_version = '2.7.2'
ext.compose_version = '1.7.3'
- ext.about_libraries_version = '12.1.2'
- ext.work_version = '2.10.1'
- ext.ksp_version = '2.1.21-2.0.1'
+ ext.about_libraries_version = '12.2.3'
+ ext.work_version = '2.10.2'
+ ext.ksp_version = '2.2.0-2.0.2'
repositories {
google()
@@ -19,10 +19,10 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.10.1'
+ classpath 'com.android.tools.build:gradle:8.11.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.8.9"
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.9.0"
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/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b0ae8c96..d678ea47 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Wed Aug 02 18:45:44 CEST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
From 36dc91ea98b5732210420a9d1819da1726b0f221 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 2 Jul 2025 11:38:25 +0100
Subject: [PATCH 28/71] change last name of maintainer
---
CITATION.cff | 2 +-
.../de/seemoo/at_tracking_detection/ui/dashboard/Article.kt | 4 ++--
.../at_tracking_detection/ui/settings/InformationFragment.kt | 4 ++--
privacy-policy/de.md | 4 ++--
privacy-policy/en.md | 4 ++--
5 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/CITATION.cff b/CITATION.cff
index a3e02380..3590bad4 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -10,7 +10,7 @@ authors:
family-names: Bittner
affiliation: 'SEEMOO, TU Darmstadt'
- given-names: Alexander
- family-names: Heinrich
+ family-names: Matern
affiliation: 'SEEMOO, TU Darmstadt'
orcid: 'https://orcid.org/0000-0002-1150-1922'
- given-names: Dennis
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/Article.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/Article.kt
index 848aa18d..d5eb9910 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/Article.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/Article.kt
@@ -52,7 +52,7 @@ fun downloadJson(): String {
},
"article1": {
"title": "$iveGotANotification",
- "author": "Alexander Heinrich",
+ "author": "Alexander Matern",
"readingTime": 0,
"previewText": "$searchManually",
"cardColor": "gray_card_background",
@@ -60,7 +60,7 @@ fun downloadJson(): String {
},
"article2": {
"title": "$iCanNotFindTracker",
- "author": "Alexander Heinrich",
+ "author": "Alexander Matern",
"readingTime": 0,
"previewText": "$findTackerHelp",
"cardColor": "gray_card_background",
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/InformationFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/InformationFragment.kt
index 1607dd27..84156801 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/InformationFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/settings/InformationFragment.kt
@@ -55,7 +55,7 @@ class InformationFragment : Fragment() {
val maintainerText = view.findViewById(R.id.maintainer_text)
developerText.text = getString(R.string.developer, "Dennis Arndt")
- maintainerText.text = getString(R.string.maintainer, "Alexander Heinrich")
+ maintainerText.text = getString(R.string.maintainer, "Alexander Matern")
val contact = view.findViewById(R.id.contact_mail)
val developer = view.findViewById(R.id.developer_mail)
@@ -72,7 +72,7 @@ class InformationFragment : Fragment() {
composeEmail(emailAddress)
}
maintainer.setOnClickListener {
- val emailAddress = "aheinrich@seemoo.tu-darmstadt.de"
+ val emailAddress = "amatern@seemoo.tu-darmstadt.de"
composeEmail(emailAddress)
}
website.setOnClickListener {
diff --git a/privacy-policy/de.md b/privacy-policy/de.md
index 8edf475f..9868a674 100644
--- a/privacy-policy/de.md
+++ b/privacy-policy/de.md
@@ -5,10 +5,10 @@ Dieses Projekt ist Teil einer wissenschaftlichen Abschlussarbeit zum Erwerb des
## Verantwortliche
Verantwortlich für die Entwicklung der App und des Studienservers ist *Niklas Bittner*, Student der Informatik an der Technischen Universität Darmstadt.
-Hauptbetreuer dieser Arbeit ist *Alexander Heinrich, M.Sc* als Doktorand und wissenschaftlicher Mitarbeiter an der Technischen Universität Darmstadt im Secure Mobile Networking Lab.
+Hauptbetreuer dieser Arbeit ist *Alexander Matern, M.Sc* als Doktorand und wissenschaftlicher Mitarbeiter an der Technischen Universität Darmstadt im Secure Mobile Networking Lab.
Anschrift:
-Alexander Heinrich
+Alexander Matern
Pankratiusstraße 2,
64289 Darmstadt
diff --git a/privacy-policy/en.md b/privacy-policy/en.md
index d3c2e43e..9f282ab4 100644
--- a/privacy-policy/en.md
+++ b/privacy-policy/en.md
@@ -2,9 +2,9 @@
This project is part of a scientific thesis to earn the degree of Bachelor of Science. To protect private data, we have used the Minimal Data Usage principle and the Offline First principle. Accordingly, all personal data is stored only locally on the device. In addition, we do not collect any data that is not necessary for the use of the app.
## Responsibility
-Responsible for the development of the app and the our study server is *Niklas Bittner*, student of computer science at the Technical University of Darmstadt. The main supervisor of this work is *Alexander Heinrich, M.Sc* as a PhD student and research assistant at the Technical University of Darmstadt at the Secure Mobile Networking Lab.
+Responsible for the development of the app and the our study server is *Niklas Bittner*, student of computer science at the Technical University of Darmstadt. The main supervisor of this work is *Alexander Matern, M.Sc* as a PhD student and research assistant at the Technical University of Darmstadt at the Secure Mobile Networking Lab.
-Address: Alexander Heinrich
+Address: Alexander Matern
Pankratiusstraße 2,
64289 Darmstadt
From 3c7b460d88dc9ece760d17602c02a79213db9805 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 2 Jul 2025 12:08:09 +0100
Subject: [PATCH 29/71] update dependencies
---
app/build.gradle | 6 +++---
.../at_tracking_detection/ui/settings/SettingsFragment.kt | 8 ++++----
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 8b569e2d..09a2f3c9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -104,12 +104,12 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:3.0.0'
implementation 'com.squareup.retrofit2:converter-gson:3.0.0'
- implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.16'
- implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.16'
+ implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.17'
+ implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.17'
implementation 'com.google.code.gson:gson:2.13.1'
implementation "androidx.work:work-testing:$work_version"
implementation 'androidx.core:core-ktx:1.16.0'
- debugImplementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.16'
+ debugImplementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.17'
implementation "com.google.dagger:hilt-android:$hilt_compiler_version"
implementation "androidx.hilt:hilt-work:$hilt_version"
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 9327801a..64b3133e 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
@@ -1,14 +1,14 @@
package de.seemoo.at_tracking_detection.ui.settings
import android.Manifest
+import android.app.AlertDialog
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
-import android.net.Uri
import android.os.Build
import android.os.Bundle
import androidx.core.content.ContextCompat
-import android.app.AlertDialog
+import androidx.core.net.toUri
import androidx.navigation.findNavController
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
@@ -141,7 +141,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
Preference.OnPreferenceClickListener {
val intent = Intent(
Intent.ACTION_VIEW,
- Uri.parse("https://tpe.seemoo.tu-darmstadt.de/privacy-policy.html")
+ "https://tpe.seemoo.tu-darmstadt.de/privacy-policy.html".toUri()
)
startActivity(intent)
return@OnPreferenceClickListener true
@@ -150,7 +150,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
findPreference("survey")?.setOnPreferenceClickListener {
val intent = Intent(
Intent.ACTION_VIEW,
- Uri.parse(ATTrackingDetectionApplication.SURVEY_URL)
+ ATTrackingDetectionApplication.SURVEY_URL.toUri()
)
startActivity(intent)
return@setOnPreferenceClickListener true
From 412b4119ec7d6a6c7481763e19401c5f153c802a Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 7 Jul 2025 12:01:38 +0100
Subject: [PATCH 30/71] fix crash in some navigation cases in TrackingFragment
---
.../ui/tracking/TrackingFragment.kt | 46 ++++++++-----------
1 file changed, 18 insertions(+), 28 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingFragment.kt
index 70f371e9..09294fa8 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingFragment.kt
@@ -1,7 +1,11 @@
package de.seemoo.at_tracking_detection.ui.tracking
import android.annotation.SuppressLint
-import android.content.*
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
import android.os.Build
import android.os.Bundle
import android.os.IBinder
@@ -14,7 +18,6 @@ import android.widget.EditText
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
-import android.window.OnBackInvokedDispatcher
import androidx.activity.addCallback
import androidx.cardview.widget.CardView
import androidx.databinding.DataBindingUtil
@@ -125,32 +128,6 @@ class TrackingFragment : Fragment() {
mapView.onPause()
}
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- // Register OnBackPressedCallback
- requireActivity().onBackPressedDispatcher.addCallback(this) {
- if (safeArgs.notificationId != -1) {
- requireActivity().finish()
- } else {
- findNavController().navigateUp()
- }
- }
-
- // For Android 13+ (API level 33), register OnBackInvokedCallback for predictive gestures
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- requireActivity().onBackInvokedDispatcher.registerOnBackInvokedCallback(
- OnBackInvokedDispatcher.PRIORITY_DEFAULT
- ) {
- if (safeArgs.notificationId != -1) {
- requireActivity().finish()
- } else {
- findNavController().navigateUp()
- }
- }
- }
- }
-
private fun initializeMap() {
Utility.basicMapSetup(mapView)
@@ -174,6 +151,19 @@ class TrackingFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ // Register OnBackPressedCallback
+ requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
+ if (safeArgs.notificationId != -1) {
+ // Check if the activity is not finishing before calling finish()
+ if (!requireActivity().isFinishing) {
+ requireActivity().finish()
+ }
+ } else {
+ // Safely find NavController as the view is attached
+ findNavController().navigateUp()
+ }
+ }
+
mapView = view.findViewById(R.id.map)
view.post {
From 87d111b49d28ee3cc3da09227e030b347c162bf6 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 7 Jul 2025 12:05:20 +0100
Subject: [PATCH 31/71] update dependencies
---
app/build.gradle | 4 ++--
build.gradle | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 09a2f3c9..dae6b9bb 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -92,8 +92,8 @@ dependencies {
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.vectordrawable:vectordrawable:1.2.0'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.9.0'
- implementation 'androidx.navigation:navigation-ui-ktx:2.9.0'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.9.1'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.9.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
diff --git a/build.gradle b/build.gradle
index ea5ca5b7..abfdec21 100644
--- a/build.gradle
+++ b/build.gradle
@@ -22,7 +22,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:8.11.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.0"
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.9.1"
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"
From 9954db4562583deb414c19a8633e9bde597a4ce7 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 16 Jul 2025 09:48:15 +0100
Subject: [PATCH 32/71] update grade and some dependencies
---
app/build.gradle | 6 +++---
build.gradle | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index dae6b9bb..bad03605 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -104,12 +104,12 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:3.0.0'
implementation 'com.squareup.retrofit2:converter-gson:3.0.0'
- implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.17'
- implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.17'
+ implementation 'com.squareup.okhttp3:okhttp:5.1.0'
+ implementation 'com.squareup.okhttp3:logging-interceptor:5.1.0'
implementation 'com.google.code.gson:gson:2.13.1'
implementation "androidx.work:work-testing:$work_version"
implementation 'androidx.core:core-ktx:1.16.0'
- debugImplementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.17'
+ debugImplementation 'com.squareup.okhttp3:logging-interceptor:5.1.0'
implementation "com.google.dagger:hilt-android:$hilt_compiler_version"
implementation "androidx.hilt:hilt-work:$hilt_version"
diff --git a/build.gradle b/build.gradle
index abfdec21..f3d434cc 100644
--- a/build.gradle
+++ b/build.gradle
@@ -19,7 +19,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.11.0'
+ classpath 'com.android.tools.build:gradle:8.11.1'
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.1"
From adc51a2df99feaeb397691912c9c90ee43ceef4f Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 23 Jul 2025 14:02:02 +0100
Subject: [PATCH 33/71] update some dependencies
---
app/build.gradle | 10 +++++-----
build.gradle | 4 ++--
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index bad03605..242a8380 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -92,10 +92,10 @@ dependencies {
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.vectordrawable:vectordrawable:1.2.0'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.9.1'
- implementation 'androidx.navigation:navigation-ui-ktx:2.9.1'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.1'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.1'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.9.2'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.9.2'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.2'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.2'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.cardview:cardview:1.0.0'
@@ -164,7 +164,7 @@ dependencies {
// Tooling support (Previews, etc.)
implementation 'androidx.compose.ui:ui-tooling:1.8.3'
// Integration with ViewModels
- implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2'
// UI Tests
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.8.3'
// When using a MDC theme
diff --git a/build.gradle b/build.gradle
index f3d434cc..35988e6c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '2.2.0'
- ext.hilt_compiler_version = '2.56.2'
+ ext.hilt_compiler_version = '2.57'
ext.hilt_version = '1.2.0'
ext.room_version = '2.7.2'
ext.compose_version = '1.7.3'
@@ -22,7 +22,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:8.11.1'
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.1"
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.9.2"
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"
From 21f24afea8f9e81600b9e85fbb7f3eda586c435a Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 23 Jul 2025 14:18:54 +0100
Subject: [PATCH 34/71] fix some issues with the PermanentBluetoothScanner
---
.../detection/BackgroundBluetoothScanner.kt | 2 +-
.../detection/PermanentBluetoothScanner.kt | 78 ++++++++++---------
2 files changed, 42 insertions(+), 38 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
index bd600532..fab4d452 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
@@ -522,7 +522,7 @@ object BackgroundBluetoothScanner {
}
}
- if (wrappedScanResult.isConnectable == false) {
+ if (!wrappedScanResult.isConnectable) {
// Note:
// Google Find My Network Devices which can be bought are connectable
// If a Device is not connectable, it means that is has been designed by someone else
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
index 70b4d2ce..c3dcdfe3 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
@@ -51,8 +51,8 @@ object PermanentBluetoothScanner: LocationHistoryListener {
* The duration how long a device remains in the recently seen. 7 min.
* Afterward, the device can be added to the DB again with a new sighting.
*/
- private val COOL_DOWN_TIME_MS = 420_000
- private val MAX_LOCATION_AGE_S = 300
+ private const val COOL_DOWN_TIME_MS = 420_000
+ private const val MAX_LOCATION_AGE_S = 300
private val applicationContext: Context
get() {
@@ -228,7 +228,7 @@ object PermanentBluetoothScanner: LocationHistoryListener {
}
private suspend fun insertPendingDevices() {
- if (pendingFoundDevices.size == 0) {
+ if (pendingFoundDevices.isEmpty()) {
return
}
@@ -266,44 +266,48 @@ object PermanentBluetoothScanner: LocationHistoryListener {
location = null
}
-// BLELogger.d("${device.wrappedScanResult.uniqueIdentifier}: Found a location with ${timeDiff}s difference ${location?.privacyPrint()})")
- BLELogger.d("Inserting ${device.wrappedScanResult.uniqueIdentifier} ${device.wrappedScanResult.deviceType} at ${location?.privacyPrint()}")
- val pair = BackgroundBluetoothScanner.insertScanResult(
- device.wrappedScanResult,
- latitude = location?.latitude,
- longitude = location?.longitude,
- altitude = location?.altitude,
- accuracy = location?.accuracy,
- discoveryDate = device.discoveryDate
- )
- savedDevices.add(device)
- recentlySeenDevices.add(device)
-
- if (pair.first != null && pair.second != null) {
- BLELogger.d("Inserted device ${pair.first?.address} (${pair.first?.deviceType}) at ${pair.second?.locationId} to the DB")
- // Logging this as a scan
- scanRepository.insert(
- Scan(
- endDate = device.discoveryDate,
- duration = 0,
- noDevicesFound = 1,
- isManual = false,
- scanMode = ScanSettings.SCAN_MODE_LOW_POWER,
- startDate = device.discoveryDate,
- locationDeg = "${PermanentBluetoothScanner.location?.longitude},${PermanentBluetoothScanner.location?.latitude}",
- locationId = pair.second?.locationId,
- devicesAddressesFound = pair.first?.address,
- devicesTypesFound = pair.first?.deviceType?.name
- )
+ // BLELogger.d("${device.wrappedScanResult.uniqueIdentifier}: Found a location with ${timeDiff}s difference ${location?.privacyPrint()})")
+ if (!Utility.getSkipDevice(device.wrappedScanResult) && device.wrappedScanResult.deviceType in validDeviceTypes) {
+ BLELogger.d("Inserting ${device.wrappedScanResult.uniqueIdentifier} ${device.wrappedScanResult.deviceType} at ${location?.privacyPrint()}")
+ val pair = BackgroundBluetoothScanner.insertScanResult(
+ device.wrappedScanResult,
+ latitude = location?.latitude,
+ longitude = location?.longitude,
+ altitude = location?.altitude,
+ accuracy = location?.accuracy,
+ discoveryDate = device.discoveryDate
)
+ savedDevices.add(device)
+ recentlySeenDevices.add(device)
+
+ if (pair.first != null && pair.second != null) {
+ BLELogger.d("Inserted device ${pair.first?.address} (${pair.first?.deviceType}) at ${pair.second?.locationId} to the DB")
+ // Logging this as a scan
+ scanRepository.insert(
+ Scan(
+ endDate = device.discoveryDate,
+ duration = 0,
+ noDevicesFound = 1,
+ isManual = false,
+ scanMode = ScanSettings.SCAN_MODE_LOW_POWER,
+ startDate = device.discoveryDate,
+ locationDeg = "${PermanentBluetoothScanner.location?.longitude},${PermanentBluetoothScanner.location?.latitude}",
+ locationId = pair.second?.locationId,
+ devicesAddressesFound = pair.first?.address,
+ devicesTypesFound = pair.first?.deviceType?.name
+ )
+ )
- SharedPrefs.lastScanDate = device.discoveryDate
- }else {
- BLELogger.d("Device ${device.wrappedScanResult.deviceAddress} not added to DB")
+ SharedPrefs.lastScanDate = device.discoveryDate
+ }else {
+ BLELogger.d("Device ${device.wrappedScanResult.deviceAddress} not added to DB")
+ }
+ } else {
+ BLELogger.d("Skipping device ${device.wrappedScanResult.uniqueIdentifier} because it should not be saved in the current configuration")
}
}
- if (savedDevices.size == 0 && pendingFoundDevices.size > 0) {
+ if (savedDevices.isEmpty() && pendingFoundDevices.isNotEmpty()) {
// No new devices were added.
isWaitingForLocationUpdate = true
} else {
@@ -349,7 +353,7 @@ object PermanentBluetoothScanner: LocationHistoryListener {
}
CoroutineScope(Dispatchers.IO).launch {
Thread.sleep(2_000)
- PermanentBluetoothScanner.scan()
+ scan()
}
}
}
From d99750d58a3b90daa5273ee874c28bc82461c0e6 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 23 Jul 2025 14:30:16 +0100
Subject: [PATCH 35/71] fix issues in BluetoothLeService
---
.../util/ble/BluetoothLeService.kt | 34 ++++++++++++-------
1 file changed, 22 insertions(+), 12 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BluetoothLeService.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BluetoothLeService.kt
index 40762612..c63b66d6 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BluetoothLeService.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BluetoothLeService.kt
@@ -51,21 +51,31 @@ class BluetoothLeService : Service() {
Timber.d("Device type is ${baseDevice.deviceType} and therefore not able to play a sound!")
return false
}
+
+ // Close any existing connection before starting a new one
+ if (bluetoothGatt != null) {
+ Timber.d("Closing previous GATT connection before creating a new one.")
+ stopBLEService(bluetoothGatt)
+ bluetoothGatt = null
+ }
+
broadcastUpdate(BluetoothConstants.ACTION_GATT_CONNECTING)
- bluetoothAdapter?.let {
- return try {
- val device = it.getRemoteDevice(baseDevice.address)
- bluetoothGatt =
- device.connectGatt(this, false, baseDevice.device.bluetoothGattCallback)
- true
- } catch (e: IllegalArgumentException) {
- Timber.e("Failed to connect to device!")
- false
- }
- } ?: run {
- Timber.w("Bluetooth adapter is not initialized!")
+
+ if (bluetoothAdapter == null || !bluetoothAdapter!!.isEnabled) {
+ Timber.w("Bluetooth adapter is not initialized or not enabled!")
return false
}
+
+ return try {
+ val device = bluetoothAdapter?.getRemoteDevice(baseDevice.address)
+ bluetoothGatt =
+ device?.connectGatt(this, false, baseDevice.device.bluetoothGattCallback)
+ Timber.d("New GATT connection initiated.")
+ true
+ } catch (e: IllegalArgumentException) {
+ Timber.e("Failed to connect to device!")
+ false
+ }
}
@SuppressLint("MissingPermission")
From b2788d787eca0207beb626690821bf63ddb7b23f Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 23 Jul 2025 14:33:13 +0100
Subject: [PATCH 36/71] fix issues with gatt disconnect in Utility
---
.../at_tracking_detection/util/Utility.kt | 47 ++++++++++++++-----
1 file changed, 36 insertions(+), 11 deletions(-)
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 e8c1ed39..5b71a313 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
@@ -327,7 +327,16 @@ object Utility {
val resultsMap = mutableMapOf()
var currentCharacteristicIndex = 0
+ // Forward declaration of gatt
+ var gatt: BluetoothGatt? = null
+
val gattCallback = object : BluetoothGattCallback() {
+ @SuppressLint("MissingPermission")
+ private fun closeGatt() {
+ gatt?.close()
+ gatt = null
+ }
+
@SuppressLint("MissingPermission")
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
@@ -338,6 +347,7 @@ object Utility {
if (continuation.isActive) {
continuation.resume(resultsMap)
}
+ closeGatt()
}
}
@@ -345,37 +355,40 @@ object Utility {
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Timber.d("Services discovered.")
- gatt?.let {
- readNextCharacteristic(it)
- }
+ gatt?.let { readNextCharacteristic(it) }
} else {
Timber.w("onServicesDiscovered received: $status")
if (continuation.isActive) {
continuation.resumeWithException(Exception("Failed to discover services: $status"))
}
+ gatt?.disconnect()
+ closeGatt()
}
}
@Deprecated("Deprecated in Java")
@SuppressLint("MissingPermission")
override fun onCharacteristicRead(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) {
+ super.onCharacteristicRead(gatt, characteristic, status)
if (status == BluetoothGatt.GATT_SUCCESS) {
characteristic?.let {
- val dataType = characteristicsToRead[currentCharacteristicIndex].third
+ val (_, characteristicUUID, dataType) = characteristicsToRead[currentCharacteristicIndex]
val value = when (dataType) {
- "int" -> it.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 0) // Example: 16-bit unsigned integer
- "string" -> it.getStringValue(0) // Read as a string
- "hex" -> it.value.joinToString("") { byte -> "%02x".format(byte) } // Convert to hex string
+ "int" -> it.getIntValue(BluetoothGattCharacteristic.FORMAT_SINT16, 0)
+ "string" -> it.getStringValue(0)
+ "hex" -> it.value.joinToString("") { byte -> "%02x".format(byte) }
else -> null
}
- resultsMap[it.uuid] = value
+ resultsMap[characteristicUUID] = value
}
} else {
Timber.w("Failed to read characteristic: $status")
}
+ currentCharacteristicIndex++
gatt?.let { readNextCharacteristic(it) }
}
+
@SuppressLint("MissingPermission")
private fun readNextCharacteristic(gatt: BluetoothGatt) {
if (currentCharacteristicIndex < characteristicsToRead.size) {
@@ -383,23 +396,35 @@ object Utility {
val service = gatt.getService(serviceUUID)
val char = service?.getCharacteristic(characteristicUUID)
- if (char != null) {
+ if (char != null && (char.properties and BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
gatt.readCharacteristic(char)
} else {
- resultsMap[characteristicUUID] = "Unknown"
+ Timber.w("Characteristic $characteristicUUID not found or not readable.")
+ resultsMap[characteristicUUID] = null // Indicate failure for this char
currentCharacteristicIndex++
readNextCharacteristic(gatt)
}
} else {
+ // Finished reading all characteristics
if (continuation.isActive) {
continuation.resume(resultsMap)
}
gatt.disconnect()
+ closeGatt()
}
}
}
- bluetoothDevice.connectGatt(context, false, gattCallback)
+ // Create the GATT connection
+ gatt = bluetoothDevice.connectGatt(context, false, gattCallback)
+
+ // Handle cancellation of the coroutine
+ continuation.invokeOnCancellation {
+ Timber.d("Coroutine cancelled. Disconnecting and closing GATT.")
+ gatt?.disconnect()
+ gatt?.close()
+ gatt = null
+ }
}
suspend fun isValidURL(url: URL): Boolean = withContext(Dispatchers.IO) {
From 478b5ba4e1dd6a825eff5ecac8757ecfe1fd2df5 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sun, 27 Jul 2025 12:04:49 +0200
Subject: [PATCH 37/71] increment version number to 2.5.1
---
app/build.gradle | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 242a8380..1e004e5a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -28,8 +28,8 @@ android {
applicationId "de.seemoo.at_tracking_detection"
minSdkVersion 28
targetSdk = 35
- versionCode 52
- versionName "2.5"
+ versionCode 54
+ versionName "2.5.1"
buildConfigField "String", "API_KEY", apiProperties["API_KEY"]
buildConfigField "String", "API_BASE_ADDRESS", apiProperties["API_BASE_ADDRESS"]
From 81e0f6c2fd3d5f823d84892597a8b55e8ea7003f Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Tue, 5 Aug 2025 14:17:25 +0200
Subject: [PATCH 38/71] show error message on home screen if samsung android 15
ble bug occurs
---
.../detection/BackgroundBluetoothScanner.kt | 28 +++++++++++--------
.../detection/PermanentBluetoothScanner.kt | 26 +++++++++--------
.../ui/dashboard/DashboardRiskFragment.kt | 26 +++++++++++++----
.../ui/scan/ScanFragment.kt | 11 ++++++--
.../at_tracking_detection/util/SharedPrefs.kt | 7 +++++
.../at_tracking_detection/util/Utility.kt | 3 ++
.../util/ble/OpportunisticBLEScanner.kt | 1 +
app/src/main/res/values-de/strings.xml | 3 ++
app/src/main/res/values/strings.xml | 3 ++
9 files changed, 78 insertions(+), 30 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
index fab4d452..feb6ced5 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
@@ -177,19 +177,24 @@ object BackgroundBluetoothScanner {
val scanSettings = ScanSettings.Builder().setScanMode(scanMode).build()
SharedPrefs.isScanningInBackground = true
- bluetoothAdapter?.bluetoothLeScanner?.let { scanner ->
- BLEScanCallback.startScanning(
- scanner,
- DeviceManager.scanFilter,
- scanSettings,
- leScanCallback
- )
- } ?: run {
- Timber.e("Bluetooth LE Scanner is null, cannot perform scan.")
- isScanning = false
- return BackgroundScanResults(0, 0, 0, true)
+ try {
+ bluetoothAdapter?.bluetoothLeScanner?.let { scanner ->
+ BLEScanCallback.startScanning(
+ scanner,
+ DeviceManager.scanFilter,
+ scanSettings,
+ leScanCallback
+ )
+ } ?: run {
+ Timber.e("Bluetooth LE Scanner is null, cannot perform scan.")
+ isScanning = false
+ return BackgroundScanResults(0, 0, 0, true)
+ }
+ } catch (e: InterruptedException) {
+ Timber.e(e, "Caught InterruptedException")
}
+
val scanDuration: Long = getScanDuration()
delay(scanDuration)
bluetoothAdapter?.bluetoothLeScanner?.let { scanner ->
@@ -275,6 +280,7 @@ object BackgroundBluetoothScanner {
override fun onScanResult(callbackType: Int, scanResult: ScanResult) {
super.onScanResult(callbackType, scanResult)
val wrappedScanResult = ScanResultWrapper(scanResult)
+ SharedPrefs.showSamsungAndroid15BugNotification = false
//Checks if the device has been found already
if (!scanResultDictionary.containsKey(wrappedScanResult.uniqueIdentifier)) {
Timber.d("Found ${wrappedScanResult.uniqueIdentifier} at ${LocalDateTime.now()}")
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
index c3dcdfe3..2d90cbb3 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
@@ -123,6 +123,7 @@ object PermanentBluetoothScanner: LocationHistoryListener {
if (SharedPrefs.deactivateBackgroundScanning) {
BLELogger.d("Background scanning is deactivated")
+ return
}
BLELogger.i("Launching new thread for background scanning")
@@ -164,16 +165,18 @@ object PermanentBluetoothScanner: LocationHistoryListener {
.build()
-
- bluetoothAdapter?.bluetoothLeScanner?.startScan(
- DeviceManager.scanFilter,
- scanSettings,
- leScanCallback
- )
- ?: run {
+ try {
+ bluetoothAdapter?.bluetoothLeScanner?.startScan(
+ DeviceManager.scanFilter,
+ scanSettings,
+ leScanCallback
+ ) ?: run {
BLELogger.e("Bluetooth LE Scanner is null, cannot perform scan.")
isScanning = false
}
+ } catch (e: InterruptedException) {
+ BLELogger.e("Caught InterruptedException")
+ }
LocationHistoryController.listenToLocationChanges(this)
BLELogger.d("Requesting fused location updates")
@@ -183,11 +186,6 @@ object PermanentBluetoothScanner: LocationHistoryListener {
)
BLELogger.d("Requesting passive location updates")
locationProvider.requestPassiveLocationProviderUpdates(LocationHistoryController)
-
-// while (true) {
-// BLELogger.i( "Keeping permanent scanner active.")
-// Thread.sleep(10000);
-// }
})
}
@@ -335,6 +333,9 @@ object PermanentBluetoothScanner: LocationHistoryListener {
private val leScanCallback: ScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, scanResult: ScanResult) {
super.onScanResult(callbackType, scanResult)
+
+ SharedPrefs.showSamsungAndroid15BugNotification = false
+
val wrappedScanResult = ScanResultWrapper(scanResult)
//Checks if the device has been found already
@@ -348,6 +349,7 @@ object PermanentBluetoothScanner: LocationHistoryListener {
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
BLELogger.e("Bluetooth scan failed $errorCode")
+
if (BuildConfig.DEBUG && SharedPrefs.sendBLEErrorMessages) {
notificationService.sendBLEErrorNotification()
}
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 5a69ddfd..5d63c272 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt
@@ -1,6 +1,7 @@
package de.seemoo.at_tracking_detection.ui.dashboard
import android.annotation.SuppressLint
+import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -21,6 +22,7 @@ import com.google.android.material.card.MaterialCardView
import dagger.hilt.android.AndroidEntryPoint
import de.seemoo.at_tracking_detection.R
import de.seemoo.at_tracking_detection.databinding.FragmentDashboardRiskBinding
+import de.seemoo.at_tracking_detection.util.SharedPrefs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -73,9 +75,23 @@ class DashboardRiskFragment : Fragment() {
Timber.d("Articles JSON: %s", articlesJSON)
withContext(Dispatchers.Main) {
- val articles = parseArticles(articlesJSON)
+ var articles = parseArticles(articlesJSON)
Timber.d("Number of Articles: %s", articles.size)
+ // Show an error message if the Bluetooth scan bug on Android 15 is detected
+ if (Build.VERSION.SDK_INT >= 35 && SharedPrefs.showSamsungAndroid15BugNotification) {
+ val bugArticle = Article(
+ title = getString(R.string.samsung_bug_notification_title),
+ author = "System",
+ readingTime = 0,
+ previewText = getString(R.string.samsung_bug_notification_text),
+ cardColor = "warning_light_red",
+ preview_image = "",
+ filename = ""
+ )
+ articles = listOf(bugArticle) + articles
+ }
+
// Create a new LinearLayout to hold the ArticleCards
val articleCardsLinearLayout = LinearLayout(context)
articleCardsLinearLayout.orientation = LinearLayout.VERTICAL
@@ -98,7 +114,7 @@ class DashboardRiskFragment : Fragment() {
}
val colorResourceId = resources.getIdentifier(article.cardColor, "color", context?.packageName)
- materialCard.setBackgroundColor(colorResourceId)
+ materialCard.setCardBackgroundColor(resources.getColor(colorResourceId, null))
articleCard.addView(layout)
Timber.tag("CardAdded").d("Article card added: %s", article.title)
@@ -110,8 +126,8 @@ class DashboardRiskFragment : Fragment() {
topMargin = 22
}
- if (!article.preview_image.isNullOrEmpty()) { // TODO: Rename when in production to PreviewImage, also in JSON
- val imageURL = getURL(article.preview_image) // TODO: Rename when in production to PreviewImage, also in JSON
+ if (!article.preview_image.isNullOrEmpty()) {
+ val imageURL = getURL(article.preview_image)
context?.let {
Glide.with(it)
.load(imageURL)
@@ -149,4 +165,4 @@ class DashboardRiskFragment : Fragment() {
viewModel.updateRiskLevel()
}
-}
+}
\ 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 976b3684..04e00a65 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
@@ -11,6 +11,7 @@ import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageButton
import android.widget.LinearLayout
+import androidx.core.view.isVisible
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@@ -26,6 +27,7 @@ import de.seemoo.at_tracking_detection.database.models.device.types.SamsungTrack
import de.seemoo.at_tracking_detection.database.repository.ScanRepository
import de.seemoo.at_tracking_detection.databinding.FragmentScanBinding
import de.seemoo.at_tracking_detection.detection.BackgroundBluetoothScanner.getScanMode
+import de.seemoo.at_tracking_detection.util.SharedPrefs
import de.seemoo.at_tracking_detection.util.ble.BLEScanner
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -102,13 +104,14 @@ class ScanFragment : Fragment() {
private fun toggleInfoLayoutVisibility(view: View) {
view.findViewById(R.id.info_layout).apply {
- visibility = if (visibility == View.VISIBLE) View.GONE else View.VISIBLE
+ visibility = if (isVisible) View.GONE else View.VISIBLE
}
}
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
+ SharedPrefs.showSamsungAndroid15BugNotification = false
result?.let { scanViewModel.addScanResult(it) }
}
@@ -129,7 +132,11 @@ class ScanFragment : Fragment() {
}
if (!BLEScanner.isScanning) {
- BLEScanner.startBluetoothScan(requireContext())
+ try {
+ BLEScanner.startBluetoothScan(requireContext())
+ } catch (e: InterruptedException) {
+ Timber.e(e, "Caught InterruptedException")
+ }
}
if (scanCallback !in BLEScanner.callbacks) {
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
index 92a33571..fa72ffec 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
@@ -201,6 +201,13 @@ object SharedPrefs {
sharedPreferences.edit { putBoolean("dismiss_survey_information", value) }
}
+ var showSamsungAndroid15BugNotification: Boolean
+ get() {
+ return sharedPreferences.getBoolean("samsung_bug_notification", false)
+ }set(value) {
+ sharedPreferences.edit { putBoolean("samsung_bug_notification", value) }
+ }
+
var surveyNotificationDate: LocalDateTime?
get() {
val dateString = sharedPreferences.getString("survey_notification_date", null)
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 5b71a313..2dc80333 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
@@ -532,6 +532,9 @@ object Utility {
}
fun e(message: String) {
+ if (message.contains("Bluetooth scan failed 2")) {
+ SharedPrefs.showSamsungAndroid15BugNotification = true
+ }
Timber.e(message)
logger.e(message)
}
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/OpportunisticBLEScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/OpportunisticBLEScanner.kt
index 3dd6db91..b5bfc574 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/OpportunisticBLEScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/OpportunisticBLEScanner.kt
@@ -93,6 +93,7 @@ class OpportunisticBLEScanner(var notificationService: NotificationService?) {
Timber.d("Scan received during background scan")
}else {
Timber.d("Scan outside of background scan $scanResult")
+ SharedPrefs.showSamsungAndroid15BugNotification = false
scanResult.timestampNanos
val millisecondsSinceEvent = (SystemClock.elapsedRealtimeNanos() - scanResult.timestampNanos) / 1000000L
val timeOfEvent = System.currentTimeMillis() - millisecondsSinceEvent
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index b577b65e..0981ebb6 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -427,4 +427,7 @@
Dieser Tracker wurde gefunden am (Fortsetzung):Seite %d/%dHinweis: Dieser Eintrag ist eine Schätzung. Es könnte sich um eine falsch positive Zuordnung handeln.
+
+ Bluetooth Fehler erkannt
+ Ihr Gerät ist von einem Fehler betroffen, der bei einigen Geräten mit Android 15 oder höher auftritt. Alle Apps können derzeit kein Bluetooth Low Energy-Scanning verwenden. Bitte starten Sie Ihr Telefon neu, um das Problem zu beheben.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4d57460d..564caa34 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -473,4 +473,7 @@
Page %d/%dWarning: This Entry was matched based on likelihoods. This might be false positive.The map could not load. Please make sure you have an active internet connection.
+
+ Bluetooth Error detected
+ Your device is affected by an error that occurs on some devices running Android 15 or higher. All apps are currently unable to use Bluetooth Low Energy scanning. Please restart your phone to fix this.
From 6d80330066fca9d3fd3d5fdcad0fabb8546b5cf2 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Tue, 5 Aug 2025 14:29:41 +0200
Subject: [PATCH 39/71] update gradle and dependencies
---
app/build.gradle | 18 +++++++++---------
build.gradle | 6 +++---
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 1e004e5a..28ed4132 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -92,8 +92,8 @@ dependencies {
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.vectordrawable:vectordrawable:1.2.0'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.9.2'
- implementation 'androidx.navigation:navigation-ui-ktx:2.9.2'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.9.3'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.9.3'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.2'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.2'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
@@ -137,16 +137,16 @@ dependencies {
implementation "androidx.room:room-ktx:$room_version"
ksp "androidx.room:room-compiler:$room_version"
- androidTestImplementation 'androidx.test.ext:junit:1.2.1'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
+ androidTestImplementation 'androidx.test.ext:junit:1.3.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
androidTestImplementation "androidx.room:room-testing:$room_version"
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2'
- androidTestImplementation 'androidx.test:core:1.6.1'
- androidTestImplementation 'androidx.test:core-ktx:1.6.1'
- androidTestImplementation 'androidx.test.ext:junit-ktx:1.2.1'
- androidTestImplementation 'androidx.test:runner:1.6.2'
- androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.6.1'
+ androidTestImplementation 'androidx.test:core:1.7.0'
+ androidTestImplementation 'androidx.test:core-ktx:1.7.0'
+ androidTestImplementation 'androidx.test.ext:junit-ktx:1.3.0'
+ androidTestImplementation 'androidx.test:runner:1.7.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.7.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
diff --git a/build.gradle b/build.gradle
index 35988e6c..2fe341f6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,7 +6,7 @@ buildscript {
ext.room_version = '2.7.2'
ext.compose_version = '1.7.3'
ext.about_libraries_version = '12.2.3'
- ext.work_version = '2.10.2'
+ ext.work_version = '2.10.3'
ext.ksp_version = '2.2.0-2.0.2'
repositories {
@@ -19,10 +19,10 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.11.1'
+ classpath 'com.android.tools.build:gradle:8.12.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.2"
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.9.3"
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"
From 69725aff1027f8c5ed0bfd175ee8124cffd30689 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 11 Aug 2025 14:34:26 +0200
Subject: [PATCH 40/71] improve Bluetooth Error Detection
---
.../detection/BackgroundBluetoothScanner.kt | 9 +++++++++
.../detection/PermanentBluetoothScanner.kt | 8 ++++++++
.../ui/dashboard/DashboardRiskFragment.kt | 11 +++++++++++
.../seemoo/at_tracking_detection/util/SharedPrefs.kt | 7 +++++++
.../de/seemoo/at_tracking_detection/util/Utility.kt | 3 ---
.../util/ble/OpportunisticBLEScanner.kt | 1 +
app/src/main/res/values-de/strings.xml | 1 +
app/src/main/res/values/strings.xml | 1 +
8 files changed, 38 insertions(+), 3 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
index feb6ced5..3479be8d 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
@@ -6,6 +6,7 @@ import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
+import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
@@ -281,6 +282,7 @@ object BackgroundBluetoothScanner {
super.onScanResult(callbackType, scanResult)
val wrappedScanResult = ScanResultWrapper(scanResult)
SharedPrefs.showSamsungAndroid15BugNotification = false
+ SharedPrefs.showGenericBluetoothBugNotification = false
//Checks if the device has been found already
if (!scanResultDictionary.containsKey(wrappedScanResult.uniqueIdentifier)) {
Timber.d("Found ${wrappedScanResult.uniqueIdentifier} at ${LocalDateTime.now()}")
@@ -291,6 +293,13 @@ object BackgroundBluetoothScanner {
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
+
+ if (errorCode == 2 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ SharedPrefs.showSamsungAndroid15BugNotification = true
+ } else {
+ SharedPrefs.showGenericBluetoothBugNotification = true
+ }
+
Timber.e("Bluetooth scan failed $errorCode")
if (BuildConfig.DEBUG && SharedPrefs.sendBLEErrorMessages) {
notificationService.sendBLEErrorNotification()
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
index 2d90cbb3..75748902 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
@@ -335,6 +335,7 @@ object PermanentBluetoothScanner: LocationHistoryListener {
super.onScanResult(callbackType, scanResult)
SharedPrefs.showSamsungAndroid15BugNotification = false
+ SharedPrefs.showGenericBluetoothBugNotification = false
val wrappedScanResult = ScanResultWrapper(scanResult)
//Checks if the device has been found already
@@ -348,6 +349,13 @@ object PermanentBluetoothScanner: LocationHistoryListener {
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
+
+ if (errorCode == 2 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ SharedPrefs.showSamsungAndroid15BugNotification = true
+ } else {
+ SharedPrefs.showGenericBluetoothBugNotification = true
+ }
+
BLELogger.e("Bluetooth scan failed $errorCode")
if (BuildConfig.DEBUG && SharedPrefs.sendBLEErrorMessages) {
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 5d63c272..1c22d58e 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
@@ -90,6 +90,17 @@ class DashboardRiskFragment : Fragment() {
filename = ""
)
articles = listOf(bugArticle) + articles
+ } else if (SharedPrefs.showGenericBluetoothBugNotification) {
+ val bugArticle = Article(
+ title = getString(R.string.samsung_bug_notification_title),
+ author = "System",
+ readingTime = 0,
+ previewText = getString(R.string.generic_bluetooth_bug_notification_text),
+ cardColor = "warning_light_red",
+ preview_image = "",
+ filename = ""
+ )
+ articles = listOf(bugArticle) + articles
}
// Create a new LinearLayout to hold the ArticleCards
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
index fa72ffec..f2cca0bd 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
@@ -208,6 +208,13 @@ object SharedPrefs {
sharedPreferences.edit { putBoolean("samsung_bug_notification", value) }
}
+ var showGenericBluetoothBugNotification: Boolean
+ get() {
+ return sharedPreferences.getBoolean("generic_bluetooth_bug_notification", false)
+ }set(value) {
+ sharedPreferences.edit { putBoolean("generic_bluetooth_bug_notification", value) }
+ }
+
var surveyNotificationDate: LocalDateTime?
get() {
val dateString = sharedPreferences.getString("survey_notification_date", null)
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 2dc80333..5b71a313 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
@@ -532,9 +532,6 @@ object Utility {
}
fun e(message: String) {
- if (message.contains("Bluetooth scan failed 2")) {
- SharedPrefs.showSamsungAndroid15BugNotification = true
- }
Timber.e(message)
logger.e(message)
}
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/OpportunisticBLEScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/OpportunisticBLEScanner.kt
index b5bfc574..7349b4be 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/OpportunisticBLEScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/OpportunisticBLEScanner.kt
@@ -94,6 +94,7 @@ class OpportunisticBLEScanner(var notificationService: NotificationService?) {
}else {
Timber.d("Scan outside of background scan $scanResult")
SharedPrefs.showSamsungAndroid15BugNotification = false
+ SharedPrefs.showGenericBluetoothBugNotification = false
scanResult.timestampNanos
val millisecondsSinceEvent = (SystemClock.elapsedRealtimeNanos() - scanResult.timestampNanos) / 1000000L
val timeOfEvent = System.currentTimeMillis() - millisecondsSinceEvent
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 0981ebb6..b4b52ba5 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -430,4 +430,5 @@
Bluetooth Fehler erkanntIhr Gerät ist von einem Fehler betroffen, der bei einigen Geräten mit Android 15 oder höher auftritt. Alle Apps können derzeit kein Bluetooth Low Energy-Scanning verwenden. Bitte starten Sie Ihr Telefon neu, um das Problem zu beheben.
+ Es gab Probleme beim Scannen von Bluetooth. Stelle sicher, dass AirGuard alle erforderlichen Berechtigungen hat und dass Bluetooth eingeschaltet ist. Falls dieser Fehler weiterhin auftritt, versuche, dein Smartphone neu zu starten.
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 564caa34..c5729c07 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -476,4 +476,5 @@
Bluetooth Error detectedYour device is affected by an error that occurs on some devices running Android 15 or higher. All apps are currently unable to use Bluetooth Low Energy scanning. Please restart your phone to fix this.
+ There have been issues while scanning bluetooth. Make sure that AirGuard has all required permissions and that Bluetooth is turned on. If this error still persists, try to restart your phone.
From 1f60b11b0d83151f945ad27ae9bea0a11fd661eb Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 11 Aug 2025 15:44:19 +0200
Subject: [PATCH 41/71] add ScanOrchestrator to avoid accessing Bluetooth
Resources on too many Threads at once
---
.../detection/BackgroundBluetoothScanner.kt | 40 +--
.../detection/PermanentBluetoothScanner.kt | 136 ++++----
.../util/ble/BLEScanner.kt | 116 +++----
.../util/ble/ScanOrchestrator.kt | 323 ++++++++++++++++++
4 files changed, 455 insertions(+), 160 deletions(-)
create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
index 3479be8d..e944d29f 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/BackgroundBluetoothScanner.kt
@@ -29,7 +29,7 @@ import de.seemoo.at_tracking_detection.ui.scan.ScanResultWrapper
import de.seemoo.at_tracking_detection.util.SharedPrefs
import de.seemoo.at_tracking_detection.util.Utility
import de.seemoo.at_tracking_detection.util.Utility.LocationLogger
-import de.seemoo.at_tracking_detection.util.ble.BLEScanCallback
+import de.seemoo.at_tracking_detection.util.ble.ScanOrchestrator
import de.seemoo.at_tracking_detection.util.privacyPrint
import de.seemoo.at_tracking_detection.util.risk.RiskLevelEvaluator
import de.seemoo.at_tracking_detection.worker.BackgroundWorkScheduler
@@ -179,28 +179,28 @@ object BackgroundBluetoothScanner {
SharedPrefs.isScanningInBackground = true
try {
- bluetoothAdapter?.bluetoothLeScanner?.let { scanner ->
- BLEScanCallback.startScanning(
- scanner,
- DeviceManager.scanFilter,
- scanSettings,
- leScanCallback
- )
- } ?: run {
- Timber.e("Bluetooth LE Scanner is null, cannot perform scan.")
- isScanning = false
- return BackgroundScanResults(0, 0, 0, true)
- }
- } catch (e: InterruptedException) {
- Timber.e(e, "Caught InterruptedException")
+ // Use a process-wide static callback for background
+ val callback = leScanCallback
+ val filters = DeviceManager.scanFilter
+ ScanOrchestrator.startScan(
+ callerTag = "BackgroundBluetoothScanner",
+ filters = filters,
+ settings = scanSettings,
+ callback = callback,
+ allowReplaceExisting = true,
+ priority = ScanOrchestrator.Priority.MEDIUM
+ )
+ } catch (t: Throwable) {
+ Timber.e(t, "Failed to request scan start")
+ isScanning = false
+ return BackgroundScanResults(0, 0, 0, true)
}
-
val scanDuration: Long = getScanDuration()
delay(scanDuration)
- bluetoothAdapter?.bluetoothLeScanner?.let { scanner ->
- BLEScanCallback.stopScanning(scanner)
- }
+
+ // Stop scan via orchestrator
+ ScanOrchestrator.stopScan("BackgroundBluetoothScanner", leScanCallback)
isScanning = false
Timber.d("Scanning for bluetooth le devices stopped!. Discovered ${scanResultDictionary.size} devices")
@@ -440,7 +440,7 @@ object BackgroundBluetoothScanner {
// We get the beacons of the last 15 min.
// The DB gets too slow if we add too many beacons, so we should only add one every 15min.
- var beacon: Beacon? = null
+ var beacon: Beacon?
val beacons = beaconRepository.getDeviceBeaconsSince(
deviceAddress = uniqueIdentifier,
since = discoveryDate.minusMinutes(TIME_BETWEEN_BEACONS)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
index 75748902..e05baa88 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/detection/PermanentBluetoothScanner.kt
@@ -19,6 +19,7 @@ import de.seemoo.at_tracking_detection.ui.scan.ScanResultWrapper
import de.seemoo.at_tracking_detection.util.SharedPrefs
import de.seemoo.at_tracking_detection.util.Utility
import de.seemoo.at_tracking_detection.util.Utility.BLELogger
+import de.seemoo.at_tracking_detection.util.ble.ScanOrchestrator
import de.seemoo.at_tracking_detection.util.privacyPrint
import de.seemoo.at_tracking_detection.worker.BackgroundWorkScheduler
import kotlinx.coroutines.CoroutineScope
@@ -26,7 +27,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
-import timber.log.Timber
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.temporal.ChronoUnit
@@ -38,6 +38,9 @@ import kotlin.math.abs
object PermanentBluetoothScanner: LocationHistoryListener {
private var bluetoothAdapter: BluetoothAdapter? = null
+ private var executor = Executors.newSingleThreadExecutor()
+ @Volatile private var keepRunning = false
+
private var pendingFoundDevices: ArrayList =
ArrayList()
@@ -103,90 +106,78 @@ object PermanentBluetoothScanner: LocationHistoryListener {
private var isScanning = false
- private var executor = Executors.newFixedThreadPool(2)
- private var locationExecutor = Executors.newSingleThreadExecutor()
-
fun scan() {
- if (!Utility.checkBluetoothPermission()) {
- BLELogger.d("Permission to perform bluetooth scan missing")
- return
- }
-
- // Scan permanently on a background thread
- if (isScanning) {
- Timber.d("App is already scanning in background. Stopping ongoing scans and restarting.")
- bluetoothAdapter?.bluetoothLeScanner?.stopScan(leScanCallback)
- executor.shutdownNow()
- executor = Executors.newFixedThreadPool(2)
- Timber.d("Ongoing scans have been stopped.")
- }
-
if (SharedPrefs.deactivateBackgroundScanning) {
BLELogger.d("Background scanning is deactivated")
return
+ } else if (!Utility.checkBluetoothPermission()) {
+ BLELogger.d("Permission to perform bluetooth scan missing")
+ return
}
- BLELogger.i("Launching new thread for background scanning")
+ keepRunning = true
- // Launch a new thread
- executor.execute(Runnable {
- BLELogger.i("Thread for background scanning started")
-
- BLELogger.d("Starting permanent Bluetooth scanner")
- // For a permanent scanner, we use low power
+ // Avoid creating multiple loops
+ executor.execute {
+ BLELogger.i("Permanent scanning loop started")
val scanMode = ScanSettings.SCAN_MODE_LOW_POWER
+ val scanSettings = ScanSettings.Builder().setScanMode(scanMode).build()
+ val filters = DeviceManager.scanFilter
+
+ while (keepRunning) {
+ try {
+ // Check adapter/scanner availability
+ val manager = applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+ val adapter = manager.adapter
+ val scanner = try { adapter?.bluetoothLeScanner } catch (_: Throwable) { null }
+ if (adapter == null || scanner == null || !adapter.isEnabled) {
+ BLELogger.d("Adapter/scanner not ready; sleeping")
+ Thread.sleep(1000)
+ continue
+ }
- try {
- val bluetoothManager =
- applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
- bluetoothAdapter = bluetoothManager.adapter
- if (bluetoothAdapter == null) {
- BLELogger.e("BluetoothAdapter is null, cannot perform scan.")
- } else if (!bluetoothAdapter!!.isEnabled) {
- BLELogger.e("Bluetooth is disabled, cannot perform scan.")
- } else if (bluetoothAdapter!!.bluetoothLeScanner == null) {
- BLELogger.e("BLE is not supported on this device.")
- }
- } catch (e: Throwable) {
- BLELogger.e("BluetoothAdapter not found or BLE not supported!")
- }
-
- isScanning = true
- location = null
-
-
- // Starting BLE Scan
- BLELogger.d("Start Scanning for bluetooth le devices...")
- val scanSettings = ScanSettings.Builder()
- .setScanMode(scanMode)
- // Adding other scan settings results in not receiving any devices in the callback
-// .setReportDelay(5_000)
-// .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
- .build()
-
+ // Ask orchestrator to ensure a running low-power session (our lease)
+ ScanOrchestrator.ensureRunningLease(
+ callerTag = "PermanentBluetoothScanner",
+ filters = filters,
+ settings = scanSettings,
+ callback = leScanCallback
+ )
- try {
- bluetoothAdapter?.bluetoothLeScanner?.startScan(
- DeviceManager.scanFilter,
- scanSettings,
- leScanCallback
- ) ?: run {
- BLELogger.e("Bluetooth LE Scanner is null, cannot perform scan.")
- isScanning = false
+ // Sleep a bit before re-asserting; not too often to avoid churn
+ Thread.sleep(3000)
+
+ } catch (ie: InterruptedException) {
+ // If the app wants to stop, executor shutdown will interrupt; exit cleanly
+ BLELogger.d("Permanent scanning loop interrupted")
+ break
+ } catch (t: Throwable) {
+ BLELogger.e("Permanent scanning loop error: ${t.message}")
+ // Back off a little to avoid tight loops
+ try { Thread.sleep(1500) } catch (_: InterruptedException) { break }
}
- } catch (e: InterruptedException) {
- BLELogger.e("Caught InterruptedException")
}
- LocationHistoryController.listenToLocationChanges(this)
- BLELogger.d("Requesting fused location updates")
- locationProvider.requestFusedBackgroundLocationUpdates(
- locationExecutor,
- LocationHistoryController
- )
- BLELogger.d("Requesting passive location updates")
- locationProvider.requestPassiveLocationProviderUpdates(LocationHistoryController)
- })
+ // Release the lease when leaving
+ ScanOrchestrator.releaseLease("PermanentBluetoothScanner")
+ BLELogger.i("Permanent scanning loop finished")
+ }
+ }
+
+ fun stopPermanentScan() {
+ keepRunning = false
+ // Do NOT call shutdownNow; it interrupts threads mid-call and can trigger InterruptedException in the framework.
+ executor.shutdown()
+ // Optionally, replace the executor with a new one if you need to start again later
+ if (executor.isShutdown || executor.isTerminated) {
+ executor = Executors.newSingleThreadExecutor()
+ }
+ // Ask orchestrator to stop only if we are the current callback
+ try {
+ ScanOrchestrator.stopScan("PermanentBluetoothScanner", leScanCallback)
+ } catch (_: Throwable) {
+ // ignore
+ }
}
/**
@@ -329,7 +320,6 @@ object PermanentBluetoothScanner: LocationHistoryListener {
}
}
-
private val leScanCallback: ScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, scanResult: ScanResult) {
super.onScanResult(callbackType, scanResult)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BLEScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BLEScanner.kt
index 81d2f4cd..ba21585f 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BLEScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BLEScanner.kt
@@ -1,18 +1,19 @@
package de.seemoo.at_tracking_detection.util.ble
-import android.bluetooth.BluetoothAdapter
-import android.bluetooth.BluetoothManager
+import android.Manifest
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
import android.location.Location
+import android.os.Build
import android.provider.Settings
+import androidx.core.content.ContextCompat
import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
import de.seemoo.at_tracking_detection.database.models.device.DeviceManager
import de.seemoo.at_tracking_detection.detection.LocationRequester
-import de.seemoo.at_tracking_detection.util.Utility
import timber.log.Timber
@@ -21,7 +22,6 @@ import timber.log.Timber
* Not to be used for Background scanning. This is handled in the `ScanBluetoothWorker`
*/
object BLEScanner {
- private var bluetoothManager: BluetoothManager? = null
var callbacks = ArrayList()
var isScanning = false
private var lastLocation: Location? = null
@@ -30,57 +30,48 @@ object BLEScanner {
private var scanResults = ArrayList()
fun startBluetoothScan(appContext: Context): Boolean {
- this.bluetoothManager = appContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
- val bluetoothAdapter = this.bluetoothManager?.adapter
-
- if (bluetoothAdapter == null) {
- Timber.d("Bluetooth adapter is null.")
+ if (!hasScanPermission()) {
+ Timber.d("Missing BLUETOOTH_SCAN permission.")
return false
- } else if (!bluetoothAdapter.isEnabled) {
- Timber.d("Bluetooth is not enabled")
+ }
+ val scanner = ScanOrchestrator.getLeScanner()
+ if (scanner == null) {
+ Timber.d("BluetoothLeScanner is null (adapter off or transient).")
return false
}
val scanSettings = ScanSettings.Builder()
- .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .build()
scanResults.clear()
-
- if (bluetoothAdapter.state == BluetoothAdapter.STATE_ON && Utility.checkBluetoothPermission()) {
- val leScanner = bluetoothAdapter.bluetoothLeScanner
- val scanFilter = DeviceManager.scanFilter
- leScanner.startScan(scanFilter, scanSettings, ownScanCallback)
- isScanning = true
- fetchCurrentLocation()
- Timber.d("Bluetooth foreground scan started")
- return true
- }
-
- return false
+ val scanFilter = DeviceManager.scanFilter
+
+ ScanOrchestrator.startScan(
+ callerTag = "BLEScanner",
+ filters = scanFilter,
+ settings = scanSettings,
+ callback = ownScanCallback,
+ allowReplaceExisting = true,
+ priority = ScanOrchestrator.Priority.HIGH
+ )
+ isScanning = true
+ fetchCurrentLocation()
+ Timber.d("Bluetooth foreground scan requested")
+ return true
}
-
fun stopBluetoothScan() {
callbacks.clear()
- bluetoothManager?.let { manager ->
- val adapter = manager.adapter
- if (adapter != null && adapter.state == BluetoothAdapter.STATE_ON) {
- if (!Utility.checkBluetoothPermission()) {
- return
- }
- adapter.bluetoothLeScanner.stopScan(ownScanCallback)
- }
- }
+ ScanOrchestrator.stopScan("BLEScanner", ownScanCallback)
isScanning = false
scanResults.clear()
- Timber.d("Bluetooth scan stopped")
+ Timber.d("Bluetooth scan stopped (requested)")
}
fun registerCallback(callback: ScanCallback) {
callbacks.add(callback)
Timber.d("New BLE ScanCallback registered")
-
- //Pass the last results
scanResults.forEach {
callback.onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, it)
}
@@ -88,49 +79,31 @@ object BLEScanner {
fun unregisterCallback(callback: ScanCallback) {
callbacks.remove(callback)
- // Timber.d("BLE ScanCallback unregistered")
}
- private var ownScanCallback = object: ScanCallback() {
+ private var ownScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
super.onScanResult(callbackType, result)
- // TODO: Add scan result to DB here. Detection events should not be to close after each other.
- // New detection events (Beacons) every 15min
- // Timber.d("Found a device $result")
result?.let { scanResult ->
scanResults.add(0, scanResult)
- if (scanResults.size > 10) {
- scanResults.removeLastOrNull()
- }
- }
-
- callbacks.forEach {
- it.onScanResult(callbackType, result)
+ if (scanResults.size > 10) scanResults.removeLastOrNull()
+ callbacks.forEach { it.onScanResult(callbackType, result) }
}
}
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
- callbacks.forEach {
- it.onScanFailed(errorCode)
- }
+ Timber.e("BLE Scan failed. $errorCode")
+ isScanning = false
}
override fun onBatchScanResults(results: MutableList?) {
super.onBatchScanResults(results)
- // Batch results are only delivered when using low power scanning.
- // We use low latency so this method should not be called.
- // We implement it anyways to be save.
-
- //TODO: Add scan result to DB here
- callbacks.forEach {
- it.onBatchScanResults(results)
- }
+ callbacks.forEach { it.onBatchScanResults(results) }
}
}
private fun fetchCurrentLocation() {
- // We fetch the current location and cache for saving the results to the DB
val locationProvider = ATTrackingDetectionApplication.getCurrentApp().locationProvider
val loc =
locationProvider.lastKnownOrRequestLocationUpdates(locationRequester, timeoutMillis = null)
@@ -139,20 +112,29 @@ object BLEScanner {
}
}
- private var locationRequester = object: LocationRequester() {
+ private var locationRequester = object : LocationRequester() {
override fun receivedAccurateLocationUpdate(location: Location) {
this@BLEScanner.lastLocation = location
}
}
+ // Consider Bluetooth "available" for scanning when scanner exists and permission granted
fun isBluetoothOn(): Boolean {
- val adapter = bluetoothManager?.adapter
- return adapter != null && adapter.isEnabled
+ if (!hasScanPermission()) return false
+ return ScanOrchestrator.getLeScanner() != null
}
fun openBluetoothSettings(context: Context) {
- val intentOpenBluetoothSettings = Intent()
- intentOpenBluetoothSettings.action = Settings.ACTION_BLUETOOTH_SETTINGS
+ val intentOpenBluetoothSettings = Intent(Settings.ACTION_BLUETOOTH_SETTINGS)
context.startActivity(intentOpenBluetoothSettings)
}
-}
\ No newline at end of file
+
+ private fun hasScanPermission(): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ ContextCompat.checkSelfPermission(
+ ATTrackingDetectionApplication.getAppContext(),
+ Manifest.permission.BLUETOOTH_SCAN
+ ) == PackageManager.PERMISSION_GRANTED
+ } else true
+ }
+}
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt
new file mode 100644
index 00000000..9a6ad991
--- /dev/null
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt
@@ -0,0 +1,323 @@
+package de.seemoo.at_tracking_detection.util.ble
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothManager
+import android.bluetooth.le.BluetoothLeScanner
+import android.bluetooth.le.ScanCallback
+import android.bluetooth.le.ScanFilter
+import android.bluetooth.le.ScanSettings
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.core.content.ContextCompat
+import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
+import timber.log.Timber
+import java.util.ArrayDeque
+
+object ScanOrchestrator {
+ enum class Priority { HIGH, MEDIUM, LOW }
+
+ private const val STOP_START_GRACE_MS = 400L
+ private const val THROTTLE_WINDOW_MS = 30_000L
+ private const val THROTTLE_MAX_STARTS = 5
+
+ private val appContext: Context
+ get() = ATTrackingDetectionApplication.getAppContext()
+
+ private val bluetoothManager: BluetoothManager? by lazy {
+ appContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
+ }
+
+ private val handlerThread = HandlerThread("BleScanOrchestrator")
+ private val handler: Handler
+
+ @Volatile private var currentCallback: ScanCallback? = null
+ @Volatile private var currentFilters: List? = null
+ @Volatile private var currentSettings: ScanSettings? = null
+ @Volatile private var currentPriority: Priority? = null
+ @Volatile private var currentOwnerTag: String? = null
+
+ @Volatile private var isStartingOrRunning = false
+ @Volatile private var isStopping = false
+
+ // Permanent lease: tag and desired config to auto-resume when idle
+ @Volatile private var leaseHolderTag: String? = null
+ @Volatile private var leaseFilters: List? = null
+ @Volatile private var leaseSettings: ScanSettings? = null
+ @Volatile private var leaseCallback: ScanCallback? = null
+
+ // timestamps for throttle
+ private val recentStarts = ArrayDeque(THROTTLE_MAX_STARTS + 1)
+
+ init {
+ handlerThread.start()
+ handler = Handler(handlerThread.looper)
+ }
+
+ private fun hasPermission(permission: String): Boolean {
+ return ContextCompat.checkSelfPermission(appContext, permission) == PackageManager.PERMISSION_GRANTED
+ }
+
+ private fun hasScanPermission(): Boolean {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ hasPermission(Manifest.permission.BLUETOOTH_SCAN)
+ } else true
+ }
+
+ fun getLeScanner(): BluetoothLeScanner? {
+ val adapter = bluetoothManager?.adapter ?: return null
+ return try { adapter.bluetoothLeScanner } catch (_: Throwable) { null }
+ }
+
+ private fun canStartNow(): Boolean {
+ val now = System.currentTimeMillis()
+ while (true) {
+ val head = recentStarts.peekFirst() ?: break
+ if (now - head > THROTTLE_WINDOW_MS) {
+ recentStarts.removeFirst()
+ } else break
+ }
+ return recentStarts.size < THROTTLE_MAX_STARTS
+ }
+
+ private fun recordStart() {
+ val now = System.currentTimeMillis()
+ recentStarts.addLast(now)
+ while (recentStarts.size > THROTTLE_MAX_STARTS) {
+ recentStarts.removeFirst()
+ }
+ }
+
+// Public API
+
+ // Higher priority can preempt lower priority
+ fun startScan(
+ callerTag: String,
+ filters: List,
+ settings: ScanSettings,
+ callback: ScanCallback,
+ priority: Priority,
+ allowReplaceExisting: Boolean = true
+ ) {
+ handler.post {
+ Timber.d("ScanOrchestrator.startScan by $callerTag priority=$priority")
+ if (!hasScanPermission()) {
+ Timber.w("BLUETOOTH_SCAN not granted")
+ return@post
+ }
+ val scanner = getLeScanner()
+ if (scanner == null) {
+ Timber.w("BluetoothLeScanner is null (adapter off or transient)")
+ return@post
+ }
+
+ // Preemption logic
+ if (isStartingOrRunning || isStopping) {
+ val runningPrio = currentPriority
+ // If current is lower priority than requested, preempt
+ val shouldPreempt = runningPrio != null && priorityHigherThan(priority, runningPrio)
+ if (shouldPreempt) {
+ Timber.d("Preempting $currentOwnerTag ($runningPrio) with $callerTag ($priority)")
+ internalStop(scanner, currentCallback)
+ handler.postDelayed({
+ internalStart(scanner, filters, settings, callback, priority, callerTag)
+ }, STOP_START_GRACE_MS)
+ return@post
+ }
+
+ // If not preempting, only replace if allowed and same or lower priority caller requests it
+ if (!allowReplaceExisting) {
+ Timber.d("Scan already running or stopping; ignoring request from $callerTag")
+ return@post
+ }
+ // If same caller and same or higher priority, we can replace (e.g., change mode)
+ if (callerTag == currentOwnerTag) {
+ Timber.d("Replacing running scan with new request from same owner $callerTag")
+ internalStop(scanner, currentCallback)
+ handler.postDelayed({
+ internalStart(scanner, filters, settings, callback, priority, callerTag)
+ }, STOP_START_GRACE_MS)
+ } else {
+ Timber.d("Another scan running by $currentOwnerTag with priority=$runningPrio; not replacing with $callerTag ($priority)")
+ }
+ return@post
+ }
+
+ if (!canStartNow()) {
+ Timber.w("Scan start throttled: too many starts within window")
+ return@post
+ }
+
+ internalStart(scanner, filters, settings, callback, priority, callerTag)
+ }
+ }
+
+ // Permanent lease (LOW priority). It will hold/auto-resume when no higher-priority scan is active.
+ fun ensureRunningLease(
+ callerTag: String,
+ filters: List,
+ settings: ScanSettings,
+ callback: ScanCallback
+ ) {
+ handler.post {
+ leaseHolderTag = callerTag
+ leaseFilters = filters
+ leaseSettings = settings
+ leaseCallback = callback
+
+ if (!hasScanPermission()) {
+ Timber.w("ensureRunningLease: BLUETOOTH_SCAN not granted")
+ return@post
+ }
+ val scanner = getLeScanner() ?: run {
+ Timber.w("ensureRunningLease: scanner null")
+ return@post
+ }
+
+ // If a scan is running with higher or equal priority, don't replace.
+ val runningPrio = currentPriority
+ if (isStartingOrRunning || isStopping) {
+ if (runningPrio == Priority.LOW && currentOwnerTag != callerTag) {
+ // Replace another LOW with our lease if desired
+ internalStop(scanner, currentCallback)
+ handler.postDelayed({
+ internalStart(scanner, filters, settings, callback, Priority.LOW, callerTag)
+ }, STOP_START_GRACE_MS)
+ }
+ return@post
+ }
+
+ if (!canStartNow()) {
+ Timber.w("ensureRunningLease: throttled; will not start now")
+ return@post
+ }
+ internalStart(scanner, filters, settings, callback, Priority.LOW, callerTag)
+ }
+ }
+
+ fun releaseLease(callerTag: String) {
+ handler.post {
+ if (leaseHolderTag == callerTag) {
+ leaseHolderTag = null
+ leaseFilters = null
+ leaseSettings = null
+ leaseCallback = null
+ }
+ }
+ }
+
+ fun stopScan(callerTag: String, callback: ScanCallback?) {
+ handler.post {
+ Timber.d("ScanOrchestrator.stopScan by $callerTag")
+ val scanner = getLeScanner()
+ if (scanner == null) {
+ clearState()
+ // Try resume a lease if exists
+ tryResumeLease()
+ return@post
+ }
+ internalStop(scanner, callback ?: currentCallback)
+ if (leaseHolderTag == callerTag) {
+ // If the stop comes from the lease owner, also release lease
+ releaseLease(callerTag)
+ }
+ // Attempt to resume lease if we stopped a higher-priority scan
+ handler.postDelayed({ tryResumeLease() }, STOP_START_GRACE_MS)
+ }
+ }
+
+// Internal operations
+
+ private fun internalStart(
+ scanner: BluetoothLeScanner,
+ filters: List,
+ settings: ScanSettings,
+ callback: ScanCallback,
+ priority: Priority,
+ ownerTag: String
+ ) {
+ if (isStartingOrRunning || isStopping) return
+ try {
+ isStartingOrRunning = true
+ currentCallback = callback
+ currentFilters = filters
+ currentSettings = settings
+ currentPriority = priority
+ currentOwnerTag = ownerTag
+ recordStart()
+ scanner.startScan(filters, settings, callback)
+ Timber.d("BLE scan started owner=$ownerTag mode=${settings.scanMode} priority=$priority")
+ } catch (sec: SecurityException) {
+ Timber.e(sec, "SecurityException starting scan")
+ clearState()
+ } catch (ise: IllegalStateException) {
+ Timber.w(ise, "IllegalStateException starting scan")
+ clearState()
+ } catch (t: Throwable) {
+ Timber.e(t, "Unexpected error starting scan")
+ clearState()
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun internalStop(scanner: BluetoothLeScanner, callback: ScanCallback?) {
+ if (callback == null) {
+ clearState()
+ return
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !hasScanPermission()) {
+ Timber.w("Missing BLUETOOTH_SCAN for stopScan; clearing state without calling stop")
+ clearState()
+ return
+ }
+ if (!isStartingOrRunning && !isStopping) {
+ clearState()
+ return
+ }
+ try {
+ isStopping = true
+ scanner.stopScan(callback)
+ Timber.d("BLE scan stopped owner=$currentOwnerTag")
+ } catch (t: Throwable) {
+ Timber.w(t, "Error stopping scan (ignored)")
+ } finally {
+ clearState()
+ }
+ }
+
+ private fun clearState() {
+ isStartingOrRunning = false
+ isStopping = false
+ currentCallback = null
+ currentFilters = null
+ currentSettings = null
+ currentPriority = null
+ currentOwnerTag = null
+ }
+
+ private fun tryResumeLease() {
+ // If nothing running and we have a lease, resume it
+ if (isStartingOrRunning || isStopping) return
+ val lTag = leaseHolderTag ?: return
+ val lFilters = leaseFilters ?: return
+ val lSettings = leaseSettings ?: return
+ val lCallback = leaseCallback ?: return
+ if (!hasScanPermission()) return
+ val scanner = getLeScanner() ?: return
+ if (!canStartNow()) return
+
+ internalStart(scanner, lFilters, lSettings, lCallback, Priority.LOW, lTag)
+ }
+
+ private fun priorityHigherThan(a: Priority, b: Priority): Boolean {
+ // HIGH > MEDIUM > LOW
+ return when (a) {
+ Priority.HIGH -> b != Priority.HIGH
+ Priority.MEDIUM -> b == Priority.LOW
+ Priority.LOW -> false
+ }
+ }
+}
\ No newline at end of file
From 150a4dd2f28f92679a8394e52432e3234f0c6fbe Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 13 Aug 2025 13:36:15 +0200
Subject: [PATCH 42/71] add Listeners to check if bluetooth or location is
enabled in the system
---
.../detection/LocationProvider.kt | 1 +
.../ui/scan/ScanDistanceFragment.kt | 19 ++++++
.../ui/scan/ScanDistanceViewModel.kt | 50 +++++++++++++++-
.../ui/scan/ScanFragment.kt | 15 +++++
.../ui/scan/ScanViewModel.kt | 52 +++++++++++++++-
.../util/ble/BLEScanner.kt | 6 ++
.../util/ble/BluetoothStateMonitor.kt | 60 +++++++++++++++++++
.../util/ble/LocationStateMonitor.kt | 20 +++++++
.../util/ble/ScanOrchestrator.kt | 20 +++++++
9 files changed, 240 insertions(+), 3 deletions(-)
create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BluetoothStateMonitor.kt
create mode 100644 app/src/main/java/de/seemoo/at_tracking_detection/util/ble/LocationStateMonitor.kt
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 bcc3940e..89c8809b 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
@@ -411,6 +411,7 @@ open class LocationProvider @Inject constructor(
const val MAX_AGE_SECONDS = 120L
const val MIN_ACCURACY_METER = 120L
+ // Legacy Function to check if location is turned on
fun isLocationTurnedOn(): Boolean {
val context = ATTrackingDetectionApplication.getAppContext()
val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
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 79a734c6..90cd2b06 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
@@ -180,6 +180,12 @@ class ScanDistanceFragment : Fragment() {
setHeight(1f, 100L)
}
+ private fun readyToScan(): Boolean {
+ val btOn = viewModel.bluetoothEnabled.value == true
+ val locOn = viewModel.locationEnabled.value == true
+ return btOn && locOn
+ }
+
private fun determineDeviceTypeButtonVisible() {
if (latestWrappedScanResult == null) {
binding.performActionButton.visibility = View.GONE
@@ -278,6 +284,8 @@ class ScanDistanceFragment : Fragment() {
}
private fun startBluetoothScan() {
+ if (!readyToScan()) return
+
// Start a scan if the BLEScanner is not already running
if (!BLEScanner.isScanning) {
BLEScanner.startBluetoothScan(this.requireContext())
@@ -561,12 +569,23 @@ class ScanDistanceFragment : Fragment() {
override fun onResume() {
super.onResume()
+ viewModel.refreshLocationState()
viewModel.isFirstScanCallback.postValue(true)
determineDeviceTypeButtonVisible()
showSearchMessage()
startBluetoothScan()
}
+ override fun onStart() {
+ super.onStart()
+ viewModel.startMonitoringSystemToggles()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ viewModel.stopMonitoringSystemToggles()
+ }
+
override fun onPause() {
super.onPause()
showSearchMessage()
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
index d563b85c..14f56cce 100644
--- 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
@@ -18,7 +18,55 @@ class ScanDistanceViewModel: ViewModel() {
var isFirstScanCallback = MutableLiveData(true)
var bluetoothEnabled = MutableLiveData(true)
+ var locationEnabled = MutableLiveData(true)
+
+ private val btListener = object : de.seemoo.at_tracking_detection.util.ble.BluetoothStateMonitor.Listener {
+ override fun onBluetoothStateChanged(enabled: Boolean) {
+ bluetoothEnabled.postValue(enabled)
+ if (!enabled) {
+ // Stop any running scan immediately if Bluetooth turns off
+ stopForegroundScanIfAny()
+ }
+ }
+ }
+
init {
- bluetoothEnabled.value = BLEScanner.isBluetoothOn()
+ val btOn = de.seemoo.at_tracking_detection.util.ble.BluetoothStateMonitor.isBluetoothEnabled()
+ bluetoothEnabled.value = btOn
+
+ val locOn = de.seemoo.at_tracking_detection.util.ble.LocationStateMonitor.isLocationEnabled()
+ locationEnabled.value = locOn
+ }
+
+ fun stopMonitoringSystemToggles() {
+ de.seemoo.at_tracking_detection.util.ble.BluetoothStateMonitor.removeListener(btListener)
+ }
+
+ fun startMonitoringSystemToggles() {
+ de.seemoo.at_tracking_detection.util.ble.BluetoothStateMonitor.addListener(btListener)
+ // Refresh location state whenever the view becomes visible
+ locationEnabled.postValue(
+ de.seemoo.at_tracking_detection.util.ble.LocationStateMonitor.isLocationEnabled()
+ )
+ }
+
+ fun refreshLocationState() {
+ val locOn = de.seemoo.at_tracking_detection.util.ble.LocationStateMonitor.isLocationEnabled()
+ val previous = locationEnabled.value
+ locationEnabled.postValue(locOn)
+ if (previous == true && !locOn) {
+ // Stop any running scan immediately if Location becomes off and your feature requires it
+ stopForegroundScanIfAny()
+ }
+ }
+
+ private fun stopForegroundScanIfAny() {
+ // Foreground BLEScanner is your UI-time scanner; stop via orchestrator safely
+ try {
+ BLEScanner.unregisterAllForViewModelStop()
+ } catch (_: Throwable) { /* ignore */ }
+ try {
+ de.seemoo.at_tracking_detection.util.ble.ScanOrchestrator.stopScan("ScanDistanceViewModel", null)
+ } catch (_: Throwable) { /* ignore */ }
}
}
\ 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 04e00a65..e1929bf0 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
@@ -99,9 +99,21 @@ class ScanFragment : Fragment() {
override fun onStart() {
super.onStart()
+ scanViewModel.startMonitoringSystemToggles()
scanViewModel.bluetoothEnabled.postValue(BLEScanner.isBluetoothOn())
}
+ override fun onStop() {
+ super.onStop()
+ scanViewModel.stopMonitoringSystemToggles()
+ }
+
+ private fun readyToScan(): Boolean {
+ val btOn = scanViewModel.bluetoothEnabled.value == true
+ val locOn = scanViewModel.locationEnabled.value == true
+ return btOn && locOn
+ }
+
private fun toggleInfoLayoutVisibility(view: View) {
view.findViewById(R.id.info_layout).apply {
visibility = if (isVisible) View.GONE else View.VISIBLE
@@ -126,6 +138,8 @@ class ScanFragment : Fragment() {
}
private fun startBluetoothScanIfNeeded() {
+ if (!readyToScan()) return
+
if (!hasActiveScan) {
createNewScanRecord()
hasActiveScan = true
@@ -193,6 +207,7 @@ class ScanFragment : Fragment() {
override fun onResume() {
super.onResume()
+ scanViewModel.refreshLocationState()
if (scanViewModel.scanFinished.value == false) {
startBluetoothScanIfNeeded()
}
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 71ca69e6..7dbd5e1b 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
@@ -42,13 +42,61 @@ class ScanViewModel @Inject constructor(
val bluetoothEnabled = MutableLiveData(true)
val locationEnabled = MutableLiveData(true)
+ private val btListener = object : de.seemoo.at_tracking_detection.util.ble.BluetoothStateMonitor.Listener {
+ override fun onBluetoothStateChanged(enabled: Boolean) {
+ bluetoothEnabled.postValue(enabled)
+ if (!enabled) {
+ // Stop any running scan immediately if Bluetooth turns off
+ stopForegroundScanIfAny()
+ }
+ }
+ }
+
init {
+ // Initial values: query system state synchronously
bluetoothDeviceListHighRisk.value = mutableListOf()
bluetoothDeviceListLowRisk.value = mutableListOf()
- bluetoothEnabled.value = BLEScanner.isBluetoothOn()
- locationEnabled.value = LocationProvider.isLocationTurnedOn()
+
+ val btOn = de.seemoo.at_tracking_detection.util.ble.BluetoothStateMonitor.isBluetoothEnabled()
+ bluetoothEnabled.value = btOn
+
+ val locOn = de.seemoo.at_tracking_detection.util.ble.LocationStateMonitor.isLocationEnabled()
+ locationEnabled.value = locOn
+ }
+
+ fun startMonitoringSystemToggles() {
+ de.seemoo.at_tracking_detection.util.ble.BluetoothStateMonitor.addListener(btListener)
+ // Refresh location state whenever the view becomes visible
+ locationEnabled.postValue(
+ de.seemoo.at_tracking_detection.util.ble.LocationStateMonitor.isLocationEnabled()
+ )
}
+ fun stopMonitoringSystemToggles() {
+ de.seemoo.at_tracking_detection.util.ble.BluetoothStateMonitor.removeListener(btListener)
+ }
+
+ fun refreshLocationState() {
+ val locOn = de.seemoo.at_tracking_detection.util.ble.LocationStateMonitor.isLocationEnabled()
+ val previous = locationEnabled.value
+ locationEnabled.postValue(locOn)
+ if (previous == true && !locOn) {
+ // Stop any running scan immediately if Location becomes off and your feature requires it
+ stopForegroundScanIfAny()
+ }
+ }
+
+ private fun stopForegroundScanIfAny() {
+ // Foreground BLEScanner is your UI-time scanner; stop via orchestrator safely
+ try {
+ BLEScanner.unregisterAllForViewModelStop()
+ } catch (_: Throwable) { /* ignore */ }
+ try {
+ de.seemoo.at_tracking_detection.util.ble.ScanOrchestrator.stopScan("ScanViewModel", null)
+ } catch (_: Throwable) { /* ignore */ }
+ }
+
+
fun addScanResult(scanResult: ScanResult) = viewModelScope.launch(Dispatchers.IO) {
if (scanFinished.value == true) {
return@launch
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BLEScanner.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BLEScanner.kt
index ba21585f..d7769390 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BLEScanner.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BLEScanner.kt
@@ -129,6 +129,12 @@ object BLEScanner {
context.startActivity(intentOpenBluetoothSettings)
}
+ fun unregisterAllForViewModelStop() {
+ try { callbacks.clear() } catch (_: Throwable) {}
+ try { ScanOrchestrator.stopScan("BLEScanner-VMStop", ownScanCallback) } catch (_: Throwable) {}
+ isScanning = false
+ }
+
private fun hasScanPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ContextCompat.checkSelfPermission(
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BluetoothStateMonitor.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BluetoothStateMonitor.kt
new file mode 100644
index 00000000..2755836f
--- /dev/null
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/BluetoothStateMonitor.kt
@@ -0,0 +1,60 @@
+package de.seemoo.at_tracking_detection.util.ble
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
+import timber.log.Timber
+
+object BluetoothStateMonitor {
+ interface Listener { fun onBluetoothStateChanged(enabled: Boolean) }
+
+ private val ctx: Context get() = ATTrackingDetectionApplication.getAppContext()
+ private val listeners = mutableSetOf()
+ private var registered = false
+
+ fun isBluetoothEnabled(): Boolean {
+ val mgr = ctx.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?
+ return mgr?.adapter?.isEnabled == true
+ }
+
+ fun addListener(listener: Listener) {
+ listeners.add(listener)
+ ensureRegistered()
+ // Also emit current immediately
+ listener.onBluetoothStateChanged(isBluetoothEnabled())
+ }
+
+ fun removeListener(listener: Listener) {
+ listeners.remove(listener)
+ if (listeners.isEmpty()) unregister()
+ }
+
+ private val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (BluetoothAdapter.ACTION_STATE_CHANGED == intent.action) {
+ val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)
+ val enabled = state == BluetoothAdapter.STATE_ON
+ Timber.d("Bluetooth state changed: $state (enabled=$enabled)")
+ listeners.forEach { it.onBluetoothStateChanged(enabled) }
+ }
+ }
+ }
+
+ private fun ensureRegistered() {
+ if (!registered) {
+ ctx.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
+ registered = true
+ }
+ }
+
+ private fun unregister() {
+ if (registered) {
+ try { ctx.unregisterReceiver(receiver) } catch (_: Throwable) {}
+ registered = false
+ }
+ }
+}
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/LocationStateMonitor.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/LocationStateMonitor.kt
new file mode 100644
index 00000000..fd1a08d1
--- /dev/null
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/LocationStateMonitor.kt
@@ -0,0 +1,20 @@
+package de.seemoo.at_tracking_detection.util.ble
+
+import android.content.Context
+import android.location.LocationManager
+import android.os.Build
+import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
+
+object LocationStateMonitor {
+ fun isLocationEnabled(): Boolean {
+ val ctx = ATTrackingDetectionApplication.getAppContext()
+ val lm = ctx.getSystemService(Context.LOCATION_SERVICE) as LocationManager
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ lm.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
+ lm.isProviderEnabled(LocationManager.FUSED_PROVIDER)
+ } else {
+ lm.isProviderEnabled(LocationManager.GPS_PROVIDER) ||
+ lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
+ }
+ }
+}
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt
index 9a6ad991..bc5d8761 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt
@@ -55,8 +55,28 @@ object ScanOrchestrator {
init {
handlerThread.start()
handler = Handler(handlerThread.looper)
+
+ // Stop immediately if Bluetooth turns off while a scan is running
+ BluetoothStateMonitor.addListener(
+ object : BluetoothStateMonitor.Listener {
+ override fun onBluetoothStateChanged(enabled: Boolean) {
+ if (!enabled) {
+ handler.post {
+ val scanner = getLeScanner()
+ // Stop running scan if any
+ if (scanner != null && (isStartingOrRunning || isStopping)) {
+ internalStop(scanner, currentCallback)
+ } else {
+ clearState()
+ }
+ }
+ }
+ }
+ }
+ )
}
+
private fun hasPermission(permission: String): Boolean {
return ContextCompat.checkSelfPermission(appContext, permission) == PackageManager.PERMISSION_GRANTED
}
From 10d318ab389df59671aeb1a74f13fd61a48d62ed Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Wed, 13 Aug 2025 13:51:53 +0200
Subject: [PATCH 43/71] replace specific bluetooth error message on main page
with generic one
---
.../ui/dashboard/DashboardRiskFragment.kt | 25 ++++++++++---------
1 file changed, 13 insertions(+), 12 deletions(-)
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 1c22d58e..656d3623 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
@@ -79,18 +79,19 @@ class DashboardRiskFragment : Fragment() {
Timber.d("Number of Articles: %s", articles.size)
// Show an error message if the Bluetooth scan bug on Android 15 is detected
- if (Build.VERSION.SDK_INT >= 35 && SharedPrefs.showSamsungAndroid15BugNotification) {
- val bugArticle = Article(
- title = getString(R.string.samsung_bug_notification_title),
- author = "System",
- readingTime = 0,
- previewText = getString(R.string.samsung_bug_notification_text),
- cardColor = "warning_light_red",
- preview_image = "",
- filename = ""
- )
- articles = listOf(bugArticle) + articles
- } else if (SharedPrefs.showGenericBluetoothBugNotification) {
+// if (Build.VERSION.SDK_INT >= 35 && SharedPrefs.showSamsungAndroid15BugNotification) {
+// val bugArticle = Article(
+// title = getString(R.string.samsung_bug_notification_title),
+// author = "System",
+// readingTime = 0,
+// previewText = getString(R.string.samsung_bug_notification_text),
+// cardColor = "warning_light_red",
+// preview_image = "",
+// filename = ""
+// )
+// articles = listOf(bugArticle) + articles
+// } else
+ if (SharedPrefs.showGenericBluetoothBugNotification || SharedPrefs.showSamsungAndroid15BugNotification) {
val bugArticle = Article(
title = getString(R.string.samsung_bug_notification_title),
author = "System",
From eb9166a3974801fb241740b4b539e21fc5bf388c Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 18 Aug 2025 15:37:01 +0200
Subject: [PATCH 44/71] bypass High Priority Scan in ScanOrchestrator
immediately to ensure correct front end services
---
.../at_tracking_detection/util/ble/ScanOrchestrator.kt | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt
index bc5d8761..164b5deb 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt
@@ -166,8 +166,9 @@ object ScanOrchestrator {
return@post
}
- if (!canStartNow()) {
- Timber.w("Scan start throttled: too many starts within window")
+ // High priority scans bypass throttling. This ensures front end scans always work
+ if (priority != Priority.HIGH && !canStartNow()) {
+ Timber.w("Scan start throttled for $callerTag: too many starts within window")
return@post
}
@@ -249,7 +250,7 @@ object ScanOrchestrator {
}
}
-// Internal operations
+ // Internal operations
private fun internalStart(
scanner: BluetoothLeScanner,
From 1fe4965d5c52597e6d070d0a1f6f854388362500 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Fri, 22 Aug 2025 15:32:50 +0200
Subject: [PATCH 45/71] upgrade gradle, dependencies and kotlin version and
target Android 16
---
app/build.gradle | 14 +++++++-------
build.gradle | 10 +++++-----
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 28ed4132..ae9e7e20 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -22,12 +22,12 @@ android {
buildConfig true
}
- compileSdk = 35
+ compileSdk = 36
defaultConfig {
applicationId "de.seemoo.at_tracking_detection"
minSdkVersion 28
- targetSdk = 35
+ targetSdk = 36
versionCode 54
versionName "2.5.1"
buildConfigField "String", "API_KEY", apiProperties["API_KEY"]
@@ -87,7 +87,7 @@ dependencies {
implementation 'com.jakewharton.timber:timber:5.0.1'
implementation 'com.github.bastienpaulfr:Treessence:1.1.2'
implementation "androidx.work:work-runtime-ktx:$work_version"
- implementation 'androidx.core:core-ktx:1.16.0'
+ implementation 'androidx.core:core-ktx:1.17.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
@@ -108,7 +108,7 @@ dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:5.1.0'
implementation 'com.google.code.gson:gson:2.13.1'
implementation "androidx.work:work-testing:$work_version"
- implementation 'androidx.core:core-ktx:1.16.0'
+ implementation 'androidx.core:core-ktx:1.17.0'
debugImplementation 'com.squareup.okhttp3:logging-interceptor:5.1.0'
implementation "com.google.dagger:hilt-android:$hilt_compiler_version"
@@ -160,13 +160,13 @@ dependencies {
implementation 'androidx.compose.material3:material3:1.3.2'
implementation 'androidx.compose.material3:material3-window-size-class:1.3.2'
// Animations
- implementation 'androidx.compose.animation:animation:1.8.3'
+ implementation 'androidx.compose.animation:animation:1.9.0'
// Tooling support (Previews, etc.)
- implementation 'androidx.compose.ui:ui-tooling:1.8.3'
+ implementation 'androidx.compose.ui:ui-tooling:1.9.0'
// Integration with ViewModels
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2'
// UI Tests
- androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.8.3'
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.9.0'
// When using a MDC theme
implementation "com.google.android.material:compose-theme-adapter:1.2.1"
}
diff --git a/build.gradle b/build.gradle
index 2fe341f6..47a828c2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,13 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '2.2.0'
- ext.hilt_compiler_version = '2.57'
+ ext.kotlin_version = '2.2.10'
+ ext.hilt_compiler_version = '2.57.1'
ext.hilt_version = '1.2.0'
ext.room_version = '2.7.2'
ext.compose_version = '1.7.3'
- ext.about_libraries_version = '12.2.3'
+ ext.about_libraries_version = '12.2.4'
ext.work_version = '2.10.3'
- ext.ksp_version = '2.2.0-2.0.2'
+ ext.ksp_version = '2.2.10-2.0.2'
repositories {
google()
@@ -19,7 +19,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.12.0'
+ classpath 'com.android.tools.build:gradle:8.12.1'
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.3"
From 6281681498410170cc497b8d1488b41875267c61 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sat, 23 Aug 2025 15:03:51 +0200
Subject: [PATCH 46/71] add dependency for Android 16 predictive back
compatibility
---
app/build.gradle | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/build.gradle b/app/build.gradle
index ae9e7e20..cf1c6434 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -102,6 +102,7 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.4.0'
implementation "androidx.profileinstaller:profileinstaller:1.4.1"
+ implementation "androidx.activity:activity-ktx:1.10.1"
implementation 'com.squareup.retrofit2:retrofit:3.0.0'
implementation 'com.squareup.retrofit2:converter-gson:3.0.0'
implementation 'com.squareup.okhttp3:okhttp:5.1.0'
From 14fdc75de5149508c5568fef84af150409ba62a3 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 25 Aug 2025 14:56:37 +0200
Subject: [PATCH 47/71] make PermanentBluetoothScheduler a toggle in the expert
settings
---
.../ATTrackingDetectionApplication.kt | 2 +-
.../ui/settings/SettingsFragment.kt | 30 +++++++++++++++++++
.../at_tracking_detection/util/SharedPrefs.kt | 8 +++++
.../worker/ScheduleWorkersReceiver.kt | 2 +-
app/src/main/res/values-de/strings.xml | 2 ++
app/src/main/res/values/strings.xml | 2 ++
app/src/main/res/xml/fragment_settings.xml | 6 ++++
7 files changed, 50 insertions(+), 2 deletions(-)
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 c92dc253..2b8566a3 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
@@ -154,7 +154,7 @@ class ATTrackingDetectionApplication : Application(), Configuration.Provider {
}
// Initiate the permanent background scan
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && SharedPrefs.usePermanentBluetoothScanner) {
PermanentBluetoothScanner.scan()
}
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 64b3133e..fc69ea7a 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
@@ -19,9 +19,13 @@ import dagger.hilt.android.AndroidEntryPoint
import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
import de.seemoo.at_tracking_detection.BuildConfig
import de.seemoo.at_tracking_detection.R
+import de.seemoo.at_tracking_detection.detection.PermanentBluetoothScanner
import de.seemoo.at_tracking_detection.util.SharedPrefs
import de.seemoo.at_tracking_detection.util.Utility
import de.seemoo.at_tracking_detection.worker.BackgroundWorkScheduler
+import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -157,6 +161,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
+ @OptIn(DelicateCoroutinesApi::class)
private val sharedPreferenceListener =
SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, preferenceKey ->
when (preferenceKey) {
@@ -189,6 +194,29 @@ class SettingsFragment : PreferenceFragmentCompat() {
ATTrackingDetectionApplication.getCurrentActivity()?.recreate()
}
}
+ "use_permanent_bluetooth_scanner" -> {
+ if (SharedPrefs.usePermanentBluetoothScanner) {
+ Timber.d("Enabled permanent bluetooth scanner!")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ try {
+ GlobalScope.launch(kotlinx.coroutines.Dispatchers.IO) {
+ PermanentBluetoothScanner.scan()
+ }
+ } catch (t: Throwable) {
+ Timber.e(t, "Failed to start PermanentBluetoothScanner from Settings")
+ }
+ }
+ } else {
+ Timber.d("Disabled permanent bluetooth scanner!")
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ try {
+ PermanentBluetoothScanner.stopPermanentScan()
+ } catch (t: Throwable) {
+ Timber.e(t, "Failed to stop PermanentBluetoothScanner from Settings")
+ }
+ }
+ }
+ }
}
}
@@ -196,6 +224,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_low_power_ble")?.isVisible = true
findPreference("notification_priority_high")?.isVisible = true
findPreference("show_onboarding")?.isVisible = true
@@ -204,6 +233,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
} else {
Timber.d("Disabled advanced mode!")
findPreference("use_location")?.isVisible = false
+ findPreference("use_permanent_bluetooth_scanner")?.isVisible = false
findPreference("use_low_power_ble")?.isVisible = false
findPreference("notification_priority_high")?.isVisible = false
findPreference("show_onboarding")?.isVisible = false
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
index f2cca0bd..8b7bbc73 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
@@ -287,6 +287,14 @@ object SharedPrefs {
sharedPreferences.edit { putBoolean("send_ble_error_messages", value) }
}
+ var usePermanentBluetoothScanner: Boolean
+ get() {
+ return sharedPreferences.getBoolean("use_permanent_bluetooth_scanner", true)
+ }
+ set(value) {
+ sharedPreferences.edit { putBoolean("use_permanent_bluetooth_scanner", value) }
+ }
+
private fun getAllDevicesFilterOptions(): Set {
val allOptions = ATTrackingDetectionApplication.getAppContext().resources.getStringArray(R.array.devicesFilterValue)
return allOptions.toSet()
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 06ab8775..3128c952 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
@@ -46,7 +46,7 @@ class ScheduleWorkersReceiver: BroadcastReceiver() {
}
// Initiate the permanent background scanner.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && SharedPrefs.usePermanentBluetoothScanner) {
goAsync {
Timber.d("Attempting to start PermanentBluetoothScanner from ScheduleWorkersReceiver")
PermanentBluetoothScanner.scan()
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index b4b52ba5..ba14c3c2 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -29,6 +29,8 @@
Immer wenn Du eine Benachrichtigung erhalten hast, dass Du von einem Dir bekannten Gerät (z.B. einem Tile, den Du besitzt) verfolgt wirst, dann kannst Du dies als Fehlalarm kennzeichnen. Geräte, die ihre Bluetooth-Adresse nicht ändern (z.B. Pebblebee, Tile oder Chipolo) kannst Du auch ignorieren. Diese ignorierten Geräte werden hier angezeigt. Denke daran: Ein AirTag ändert seine Bluetooth-Adresse einmal am Tag, kann also nicht ignoriert werden und wird am nächsten Tag als neuer AirTag angezeigt.Hintergrundscan deaktivierenDiese 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.Aktivieren, um detailliertere Standortinformationen bei einer Warnung zu erhaltenStandort nutzenVerbundene Geräte anzeigen
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c5729c07..c8121291 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -34,6 +34,8 @@
Expert ModeDeactivate background scanThis 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.Discovered:BeaconsTimes seen
diff --git a/app/src/main/res/xml/fragment_settings.xml b/app/src/main/res/xml/fragment_settings.xml
index 0ef75c38..5653b549 100644
--- a/app/src/main/res/xml/fragment_settings.xml
+++ b/app/src/main/res/xml/fragment_settings.xml
@@ -23,6 +23,12 @@
android:summary="@string/settings_deactivate_scanning_summary"
app:title="@string/settings_deactivate_scanning_title" />
+
+
Date: Thu, 28 Aug 2025 14:02:48 +0200
Subject: [PATCH 48/71] increase version number to 2.6.0
---
app/build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/build.gradle b/app/build.gradle
index cf1c6434..97da0153 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -29,7 +29,7 @@ android {
minSdkVersion 28
targetSdk = 36
versionCode 54
- versionName "2.5.1"
+ versionName "2.6.0"
buildConfigField "String", "API_KEY", apiProperties["API_KEY"]
buildConfigField "String", "API_BASE_ADDRESS", apiProperties["API_BASE_ADDRESS"]
From beda836c3a4aaba058bc9b3e84a48e37d127f113 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Fri, 29 Aug 2025 15:01:20 +0200
Subject: [PATCH 49/71] update dependencies
---
app/build.gradle | 6 +++---
build.gradle | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 97da0153..07dc95d3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -94,8 +94,8 @@ dependencies {
implementation 'androidx.vectordrawable:vectordrawable:1.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.9.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.9.3'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.2'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.2'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.3'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.cardview:cardview:1.0.0'
@@ -165,7 +165,7 @@ dependencies {
// Tooling support (Previews, etc.)
implementation 'androidx.compose.ui:ui-tooling:1.9.0'
// Integration with ViewModels
- implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.2'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.3'
// UI Tests
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.9.0'
// When using a MDC theme
diff --git a/build.gradle b/build.gradle
index 47a828c2..bc8b745e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -19,7 +19,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.12.1'
+ classpath 'com.android.tools.build:gradle:8.12.2'
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.3"
From 57de63ed28b0762d587a033da30b7d1938b500e2 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 1 Sep 2025 14:08:08 +0200
Subject: [PATCH 50/71] add comment function for each individual tracker
---
.../24.json | 522 ++++++++++++++++++
.../database/AppDatabase.kt | 5 +-
.../database/models/device/BaseDevice.kt | 1 +
.../ui/tracking/TrackingFragment.kt | 28 +
.../ui/tracking/TrackingViewModel.kt | 18 +
app/src/main/res/layout/fragment_tracking.xml | 25 +-
app/src/main/res/values-de/strings.xml | 2 +
app/src/main/res/values/strings.xml | 2 +
8 files changed, 599 insertions(+), 4 deletions(-)
create mode 100644 app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/24.json
diff --git a/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/24.json b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/24.json
new file mode 100644
index 00000000..6f0a0abd
--- /dev/null
+++ b/app/schemas/de.seemoo.at_tracking_detection.database.AppDatabase/24.json
@@ -0,0 +1,522 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 24,
+ "identityHash": "e07b584966c3ccd9080f350684edf3ba",
+ "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, `subDeviceType` TEXT NOT NULL DEFAULT 'UNKNOWN', `riskLevel` INTEGER NOT NULL DEFAULT 0, `lastCalculatedRiskDate` TEXT, `nextObservationNotification` TEXT, `currentObservationDuration` INTEGER, `safeTracker` INTEGER NOT NULL DEFAULT false, `additionalData` TEXT, `alternativeIdentifier` TEXT, `comment` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "deviceId",
+ "columnName": "deviceId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uniqueId",
+ "columnName": "uniqueId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "address",
+ "columnName": "address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ignore",
+ "columnName": "ignore",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "connectable",
+ "columnName": "connectable",
+ "affinity": "INTEGER",
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "payloadData",
+ "columnName": "payloadData",
+ "affinity": "INTEGER"
+ },
+ {
+ "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"
+ },
+ {
+ "fieldPath": "deviceType",
+ "columnName": "deviceType",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "subDeviceType",
+ "columnName": "subDeviceType",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'UNKNOWN'"
+ },
+ {
+ "fieldPath": "riskLevel",
+ "columnName": "riskLevel",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "lastCalculatedRiskDate",
+ "columnName": "lastCalculatedRiskDate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "nextObservationNotification",
+ "columnName": "nextObservationNotification",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "currentObservationDuration",
+ "columnName": "currentObservationDuration",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "safeTracker",
+ "columnName": "safeTracker",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "additionalData",
+ "columnName": "additionalData",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "alternativeIdentifier",
+ "columnName": "alternativeIdentifier",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "comment",
+ "columnName": "comment",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "deviceId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_device_lastSeen",
+ "unique": false,
+ "columnNames": [
+ "lastSeen"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_device_lastSeen` ON `${TABLE_NAME}` (`lastSeen`)"
+ },
+ {
+ "name": "index_device_address",
+ "unique": true,
+ "columnNames": [
+ "address"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_device_address` ON `${TABLE_NAME}` (`address`)"
+ },
+ {
+ "name": "index_device_notificationSent",
+ "unique": false,
+ "columnNames": [
+ "notificationSent"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_device_notificationSent` ON `${TABLE_NAME}` (`notificationSent`)"
+ },
+ {
+ "name": "index_device_deviceType",
+ "unique": false,
+ "columnNames": [
+ "deviceType"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_device_deviceType` ON `${TABLE_NAME}` (`deviceType`)"
+ },
+ {
+ "name": "index_device_lastSeen_deviceType",
+ "unique": false,
+ "columnNames": [
+ "lastSeen",
+ "deviceType"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_device_lastSeen_deviceType` ON `${TABLE_NAME}` (`lastSeen`, `deviceType`)"
+ }
+ ]
+ },
+ {
+ "tableName": "notification",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notificationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceAddress` TEXT NOT NULL, `falseAlarm` INTEGER NOT NULL, `dismissed` INTEGER, `clicked` INTEGER, `createdAt` TEXT NOT NULL, `sensitivity` INTEGER NOT NULL DEFAULT -1)",
+ "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"
+ },
+ {
+ "fieldPath": "clicked",
+ "columnName": "clicked",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sensitivity",
+ "columnName": "sensitivity",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "notificationId"
+ ]
+ }
+ },
+ {
+ "tableName": "beacon",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`beaconId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `receivedAt` TEXT NOT NULL, `rssi` INTEGER NOT NULL, `deviceAddress` TEXT NOT NULL, `locationId` INTEGER, `mfg` BLOB, `serviceUUIDs` TEXT, `connectionState` TEXT NOT NULL DEFAULT 'UNKNOWN', FOREIGN KEY(`deviceAddress`) REFERENCES `device`(`address`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "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"
+ },
+ {
+ "fieldPath": "manufacturerData",
+ "columnName": "mfg",
+ "affinity": "BLOB"
+ },
+ {
+ "fieldPath": "serviceUUIDs",
+ "columnName": "serviceUUIDs",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "connectionState",
+ "columnName": "connectionState",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'UNKNOWN'"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "beaconId"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_beacon_receivedAt",
+ "unique": false,
+ "columnNames": [
+ "receivedAt"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_receivedAt` ON `${TABLE_NAME}` (`receivedAt`)"
+ },
+ {
+ "name": "index_beacon_deviceAddress",
+ "unique": false,
+ "columnNames": [
+ "deviceAddress"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_deviceAddress` ON `${TABLE_NAME}` (`deviceAddress`)"
+ },
+ {
+ "name": "index_beacon_connectionState",
+ "unique": false,
+ "columnNames": [
+ "connectionState"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_connectionState` ON `${TABLE_NAME}` (`connectionState`)"
+ },
+ {
+ "name": "index_beacon_deviceAddress_receivedAt",
+ "unique": false,
+ "columnNames": [
+ "deviceAddress",
+ "receivedAt"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_beacon_deviceAddress_receivedAt` ON `${TABLE_NAME}` (`deviceAddress`, `receivedAt`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "device",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "deviceAddress"
+ ],
+ "referencedColumns": [
+ "address"
+ ]
+ }
+ ]
+ },
+ {
+ "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"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "feedbackId"
+ ]
+ }
+ },
+ {
+ "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, `location_deg` TEXT, `location` INTEGER, `device_addresses_found` TEXT, `device_types_found` TEXT)",
+ "fields": [
+ {
+ "fieldPath": "scanId",
+ "columnName": "scanId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "endDate",
+ "columnName": "endDate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "noDevicesFound",
+ "columnName": "noDevicesFound",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "duration",
+ "columnName": "duration",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "isManual",
+ "columnName": "isManual",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scanMode",
+ "columnName": "scanMode",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "startDate",
+ "columnName": "startDate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "locationDeg",
+ "columnName": "location_deg",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "locationId",
+ "columnName": "location",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "devicesAddressesFound",
+ "columnName": "device_addresses_found",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "devicesTypesFound",
+ "columnName": "device_types_found",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "scanId"
+ ]
+ }
+ },
+ {
+ "tableName": "location",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `firstDiscovery` TEXT NOT NULL, `lastSeen` TEXT NOT NULL, `longitude` REAL NOT NULL, `latitude` REAL NOT NULL, `altitude` REAL, `accuracy` REAL)",
+ "fields": [
+ {
+ "fieldPath": "locationId",
+ "columnName": "locationId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "firstDiscovery",
+ "columnName": "firstDiscovery",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastSeen",
+ "columnName": "lastSeen",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "longitude",
+ "columnName": "longitude",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "latitude",
+ "columnName": "latitude",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "altitude",
+ "columnName": "altitude",
+ "affinity": "REAL"
+ },
+ {
+ "fieldPath": "accuracy",
+ "columnName": "accuracy",
+ "affinity": "REAL"
+ }
+ ],
+ "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`)"
+ }
+ ]
+ }
+ ],
+ "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, 'e07b584966c3ccd9080f350684edf3ba')"
+ ]
+ }
+}
\ 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 1d2011d8..08183f98 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 = 23,
+ version = 24,
entities = [
BaseDevice::class,
Notification::class,
@@ -35,7 +35,8 @@ import de.seemoo.at_tracking_detection.util.converter.DateTimeConverter
AutoMigration(from=19, to=20),
AutoMigration(from=20, to=21),
AutoMigration(from=21, to=22),
- AutoMigration(from=22, to=23)
+ AutoMigration(from=22, to=23),
+ AutoMigration(from=23, to=24)
],
exportSchema = true
)
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 7261cbfc..b778a14e 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
@@ -64,6 +64,7 @@ data class BaseDevice(
@ColumnInfo(name = "safeTracker", defaultValue = "false") var safeTracker: Boolean = false,
@ColumnInfo(name = "additionalData") var additionalData: String?, // This is used for matching Samsung Devices and saving ConnectionState for Google Devices
@ColumnInfo(name = "alternativeIdentifier") var alternativeIdentifier: String? = null,
+ @ColumnInfo(name = "comment") var comment: String? = null,
) {
constructor(
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingFragment.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingFragment.kt
index 09294fa8..eebc4f1a 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/tracking/TrackingFragment.kt
@@ -20,6 +20,7 @@ import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback
import androidx.cardview.widget.CardView
+import androidx.core.graphics.toColorInt
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
@@ -261,6 +262,33 @@ class TrackingFragment : Fragment() {
.show()
}
}
+
+ val commentEditText = view.findViewById(R.id.device_comment)
+
+ // Set colors of the EditText for the comment based on the selected theme
+ // Hint: In the future this should be replaced with the new color, theme system
+ if (Utility.isActualThemeDark(requireContext())) {
+ commentEditText.setBackgroundColor(resources.getColor(R.color.light_black))
+ commentEditText.setTextColor("#FFFFFF".toColorInt())
+ commentEditText.setHintTextColor(resources.getColor(R.color.light_grey))
+ } else {
+ commentEditText.setBackgroundColor("#FFFFFF".toColorInt())
+ commentEditText.setTextColor("#222222".toColorInt())
+ commentEditText.setHintTextColor(resources.getColor(R.color.grey))
+ }
+
+ trackingViewModel.deviceComment.observe(viewLifecycleOwner) { comment ->
+ if (commentEditText.text.toString() != comment) {
+ commentEditText.setText(comment ?: "")
+ }
+ }
+ commentEditText.addTextChangedListener(object : android.text.TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ trackingViewModel.updateDeviceComment(s?.toString() ?: "")
+ }
+ override fun afterTextChanged(s: android.text.Editable?) {}
+ })
}
private fun zoomToMarkers() {
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 db3602b1..746bb274 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
@@ -69,6 +69,8 @@ class TrackingViewModel @Inject constructor(
val expertMode = MutableLiveData(false)
+ val deviceComment = MutableLiveData("")
+
fun loadDevice(address: String, deviceTypeOverride: DeviceType) =
deviceRepository.getDevice(address).also { device ->
this.device.postValue(device)
@@ -93,8 +95,11 @@ class TrackingViewModel @Inject constructor(
loadLastSeenTimes(device)
expertMode.postValue(SharedPrefs.advancedMode)
}
+
+ deviceComment.postValue(device.comment ?: "")
} else {
noLocationsYet.postValue(true)
+ deviceComment.postValue("")
}
showNfcHint.postValue(deviceType.value == DeviceType.AIRTAG)
if (deviceType.value != null) {
@@ -142,4 +147,17 @@ class TrackingViewModel @Inject constructor(
context.startActivity(intent)
}
}
+
+ // Add function to update comment and save to DB
+ fun updateDeviceComment(newComment: String) {
+ device.value?.let { baseDevice ->
+ if (baseDevice.comment != newComment) {
+ baseDevice.comment = newComment.ifBlank { null }
+ viewModelScope.launch {
+ deviceRepository.update(baseDevice)
+ }
+ }
+ }
+ deviceComment.postValue(newComment)
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_tracking.xml b/app/src/main/res/layout/fragment_tracking.xml
index d6639d73..37cb3bc2 100644
--- a/app/src/main/res/layout/fragment_tracking.xml
+++ b/app/src/main/res/layout/fragment_tracking.xml
@@ -126,7 +126,7 @@
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="vertical"
- app:layout_constraintBottom_toTopOf="@id/last_seen_times_layout"
+ app:layout_constraintBottom_toTopOf="@id/device_comment"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/map_container">
@@ -266,6 +266,27 @@
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/device_comment">
Bluetooth Fehler erkanntIhr Gerät ist von einem Fehler betroffen, der bei einigen Geräten mit Android 15 oder höher auftritt. Alle Apps können derzeit kein Bluetooth Low Energy-Scanning verwenden. Bitte starten Sie Ihr Telefon neu, um das Problem zu beheben.Es gab Probleme beim Scannen von Bluetooth. Stelle sicher, dass AirGuard alle erforderlichen Berechtigungen hat und dass Bluetooth eingeschaltet ist. Falls dieser Fehler weiterhin auftritt, versuche, dein Smartphone neu zu starten.
+
+ Kommentar zu diesem Tracker
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c8121291..b03802b0 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -479,4 +479,6 @@
Bluetooth Error detectedYour device is affected by an error that occurs on some devices running Android 15 or higher. All apps are currently unable to use Bluetooth Low Energy scanning. Please restart your phone to fix this.There have been issues while scanning bluetooth. Make sure that AirGuard has all required permissions and that Bluetooth is turned on. If this error still persists, try to restart your phone.
+
+ Add a comment
From 3404af033149da13b12602b6b2d0d16c6ce0d5a0 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 1 Sep 2025 14:14:03 +0200
Subject: [PATCH 51/71] show comment function only if database device exists
---
app/src/main/res/layout/fragment_tracking.xml | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/src/main/res/layout/fragment_tracking.xml b/app/src/main/res/layout/fragment_tracking.xml
index 37cb3bc2..bb529237 100644
--- a/app/src/main/res/layout/fragment_tracking.xml
+++ b/app/src/main/res/layout/fragment_tracking.xml
@@ -281,6 +281,7 @@
android:maxLines="6"
android:inputType="textMultiLine"
android:imeOptions="actionDone"
+ android:visibility="@{vm.noLocationsYet ? View.GONE : View.VISIBLE}"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/tracking_tiles"
From 85410490a51286d981f3240943c4adf3183c1823 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 1 Sep 2025 14:25:45 +0200
Subject: [PATCH 52/71] add japanese and german translations
---
app/src/main/res/values-de/strings.xml | 6 +-
app/src/main/res/values-ja/strings.xml | 192 +++++++++++++++++++++++++
app/src/main/res/values/strings.xml | 2 +-
3 files changed, 198 insertions(+), 2 deletions(-)
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 4040b599..bf01d8ea 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -10,6 +10,7 @@
Gerät mit ID: {deviceAddress}Orten: {deviceAddress}Scan
+ Exportiere Daten: {deviceAddress}Alle GeräteKarte: %1$sGeräte
@@ -84,6 +85,7 @@
HellDiese Einstellung verwendet weniger Strom, führt aber auch zu einer langsameren und schlechteren Erkennung von TrackernLow Power Scan Modus
+ Bluetooth Fehlermeldungen anzeigenDieses Gerät kann keinen Ton abspielen.Scanne diesen AirTag mit Deinem Telefon über NFC, um weitere Informationen zu erhalten.Informationen
@@ -236,6 +238,7 @@
Öffnen, um weitere Informationen zu erhalten. Ein Gerät ist Dir für mindestens %d Minuten gefolgt.Ein Tracker folgt dir
+ Sende Bluetooth Fehler MeldungenIgnorierte GeräteGefundene AirTagsGefundene Find My Geräte
@@ -420,7 +423,7 @@
Uhrzeit: %sStandort: Latitude: %s, Longitude: %sKarte: %s
- Karte ist noch nicht geladen...
+ Karte ist noch nicht geladen…Signalstärke: %dErstellt von AirGuard %s auf AndroidInformationen:
@@ -430,6 +433,7 @@
Seite %d/%dHinweis: Dieser Eintrag ist eine Schätzung. Es könnte sich um eine falsch positive Zuordnung handeln.
+ Die Karte konnte nicht laden. überprüfe ob du eine aktive Internetverbindung hast.Bluetooth Fehler erkanntIhr Gerät ist von einem Fehler betroffen, der bei einigen Geräten mit Android 15 oder höher auftritt. Alle Apps können derzeit kein Bluetooth Low Energy-Scanning verwenden. Bitte starten Sie Ihr Telefon neu, um das Problem zu beheben.Es gab Probleme beim Scannen von Bluetooth. Stelle sicher, dass AirGuard alle erforderlichen Berechtigungen hat und dass Bluetooth eingeschaltet ist. Falls dieser Fehler weiterhin auftritt, versuche, dein Smartphone neu zu starten.
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index d4196009..ee0ed00b 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -213,4 +213,196 @@
後で調査について知りたいですかダルムシュタット工科大学は、ストーカー行為によって人々がどのように影響を受けるかを調べるために調査を実施していますストーカーと位置追跡に関する調査
+ タグを探す: {deviceAddress}
+ タグを監視する: {deviceAddress}
+ タグデータをエクスポート: {deviceAddress}
+ マップ: %1$s
+ データ削除リクエスト
+ 記事
+ AirGuardに高度なオプションを追加します。これらのオプションは上級者向けです。
+ エキスパートモード
+ バックグラウンドスキャンを無効化
+ これによりAirGuardのバックグラウンド動作が無効化されます。注意: 追跡されても警告されなくなります。
+ 常時スキャン
+ 常時バックグラウンドスキャナーを使用します。精度が向上しますが、稀にアプリの安定性に影響する場合があります。
+ 検知回数
+ AirTag %s
+ Google「Find My Device」%s
+ 接続済みデバイスを表示
+ スキャンタブで所有者デバイスに接続されているデバイスを表示する
+ 高優先度通知
+ 有効にすると通知が高優先度のアラームとして発行されます
+ 謝辞
+ 謝辞のリストを表示する
+ 検出されたタグをエクスポート
+ 検出されたタグの一覧を文書として作成する
+ 研究データの削除
+ 研究データの削除をリクエストする
+ デバイスタイプを除外
+ 除外するデバイスタイプ
+ AirGuardが無視するデバイスタイプを選択します。選択したタイプは手動スキャンに表示されず、追跡通知もされません。
+ Tile %s
+ Pebblebee %s
+ Samsungデバイス %s
+ Samsung Tracker %s
+ AirPods %s
+ Chipolo %s
+ Bluetoothがオフになっています。このアプリは近くのタグを検出するためにBluetoothが必要です。
+ 位置情報がオフになっています。このアプリは近くのタグを検出するために位置情報の許可が必要です。システム設定で位置情報をオンにしてください。\n\n効果を反映するにはアプリの再起動が必要な場合があります。
+ Bluetooth設定を開く
+ 位置情報設定を開く
+ バッテリー残量:
+ 満タン
+ 中
+ 低
+ 非常に低い
+ 不明
+ オフライン
+ デバイスは所有者から切り離されています
+ 長時間オフライン
+ デバイスは少なくとも15分間所有者から切り離されています
+ デバイスは少なくとも8時間所有者から切り離されています
+ デバイスは少なくとも30分間所有者から切り離されています
+ 短時間オフライン
+ 過去15分以内に所有者と接続されていました
+ 接続済み
+ デバイスは現在所有者と接続されています
+ 不明
+ デバイスの接続状態に関する情報
+ トラッカーの接続状態を判定できません
+ スキャン開始/停止
+ 信号強度順
+ 最近検知順
+ MACアドレス順
+ 名前順
+ タイプ順
+ デバイスが範囲外です
+ このアイコンはタグのバッテリー状態を示します
+ このアイコンをクリックすると接続状態の説明が表示されます
+ ゆっくり動いて信号強度を最大化しタグを探してください。
+ 追跡デバイスで音を鳴らします
+ このデバイスはトラッカーとして識別されました
+ デバイスを検索中
+ バッテリー:
+ Airtag
+ Airpods
+ Appleデバイス
+ Chipolo
+ Pebblebee
+ Apple FindMyデバイス
+ Samsungトラッカー
+ Samsungトラッカー
+ Samsung Find My Mobile
+ Tile
+ Google「Find My Device」デバイス
+ AirGuard for Android
+ バージョン %s
+ お問い合わせ
+ Google「Find My Device」
+ 「Find My Device」トラッカー
+ Androidスマートフォン
+ Apple「FindMy」
+ Appleデバイス
+ AirTag
+ AirPods
+ Chipolo
+ Tile
+ Pebblebee
+ 記事のプレビュー画像
+ 説明文を表示
+ PDFにエクスポート
+ このトラッカーのPDF概要ファイルを作成する
+ トラッカーを監視する
+ 時計の画像
+ 監視開始
+ 監視停止
+ このトラッカーは現在監視されています。
+ このトラッカーがあなたを追跡しているか不明な場合は手動で監視できます。\n\n1時間後、AirGuardはこのトラッカーを検索し、すぐに通知します。トラッカーが近くにいなければ通知されます。
+ このトラッカーは識別子を変更する可能性があります。検知できなくても追跡されている場合があります。
+ このトラッカーがあなたを追跡しているか確認する
+ 監視開始しました
+ 監視停止しました
+ すでにこのトラッカーを監視しています
+ クレジット
+ SmartTag
+ SmartTag(UWB付き)
+ メーカーのウェブサイト
+ トラッカーのメーカーのウェブサイトを開く
+ ブラウザが見つかりません
+ データ削除をリクエストする
+ 研究の一環として収集したすべてのデータを削除したい場合は、下のボタンをクリックしてください。これにより研究への参加も停止されます。再度参加したい場合は、オンボーディングを再度有効化し「はい」を選択してください。
+ データ削除に成功しました
+ エラーが発生しました
+ サーバーに接続できません。後でもう一度お試しください。
+ 削除するデータがありません
+ https://tpe.seemoo.tu-darmstadt.de/articles/airguard_articles_en.json
+ 読了時間: %d分
+ インターネット接続なし
+ 現在インターネットに接続されていません。接続時にアプリの使い方を説明する記事が表示されます。
+ 注意:\nAppleデバイスは識別子を定期的に変更します。\nAirTag、AirPods、FindMyなどは新しいデバイスとして認識され複数回表示される場合があります。
+ 注意:\nSamsungデバイスは識別子を定期的に変更します。\nSmartTagなどは新しいデバイスとして認識され複数回表示される場合があります。
+ 注意:\nTileデバイスは識別子を変更しません。\n誤警告の可能性があります。警告時は周囲の人(友人、家族、同僚、隣人など)が持っていないか確認してください。
+ 注意:\nChipoloデバイスは識別子を変更しません。\n誤警告の可能性があります。警告時は周囲の人(友人、家族、同僚、隣人など)が持っていないか確認してください。
+ 注意:\nPebblebeeデバイスは識別子を変更しません。\n誤警告の可能性があります。警告時は周囲の人(友人、家族、同僚、隣人など)が持っていないか確認してください。
+ 注意:\n一部のデバイスは識別子を定期的に変更します。\nこれらは新しいデバイスとして認識され複数回表示される場合があります。
+ 安全なトラッカーは、現在所有者と接続されているか、直近で接続されていたものです。これらは脅威となる可能性が低いです。\n無視されたトラッカーも安全とみなされます。
+ デバイス名は空にできません
+ 著作権
+ 開発者: %s
+ メンテナー: %s
+ 潜在的なトラッカー
+ 安全なトラッカー
+ 凡例
+ 潜在的なトラッカーが見つかった場所
+ 過去30分以内に所有者と接続されていました
+ 特定のトラッカータイプを判定する
+ 正確なデバイスタイプを判定できませんでした
+ 所有者情報を表示
+ 所有者情報を取得できませんでした
+ 所有者情報の取得
+ 所有者情報を得るには、物理的にトラッカーを発見する必要があります。発見後、特定のモードにする必要があります。例えばボタンを3回押す、または30秒間長押しするなどです。成功すると音で確認できます。その後「次へ」を押してください。
+ 所有者情報を得るには、物理的にトラッカーを発見し、ボタンを3回押してください。音で確認できます。その後「次へ」を押してください。
+ 所有者情報を得るには、物理的にトラッカーを発見し、ボタンを30秒間押し、2回目のビープ音の直後に離してください。成功すると音で確認できます。その後「次へ」を押してください。
+ 所有者情報を得るには、物理的にトラッカーを発見し、ボタンを10秒間長押ししてビープ音が鳴ったら離し、すぐにもう一度押して離してください。その後「次へ」を押してください。
+ 次へ
+ 閉じる
+ バックグラウンドスキャン無効化の確認
+ 本当にバックグラウンドスキャンを無効化しますか?無効化すると追跡通知が届かなくなります。
+ はい、無効化する
+ いいえ、保護を維持する
+ トラッカーの最終検知場所:
+ 最終検知日時はありません
+ トラッカーかスマートフォンか判定する
+ 左
+ 右
+ トラッカーをエクスポート
+ 下のボタンをクリックすると、長期間あなたを追跡していたトラッカーのPDF文書を作成できます。
+ 文書を作成
+ このトラッカーはあなたを追跡しています
+ このトラッカーはあなたを追跡していません
+ このトラッカーは無視されました
+ 場所: 不明
+ トラッカータイプ: %s
+ MACアドレス: %s
+ 最終検知: %s
+ 初回検知: %s
+ 検知回数: %d
+ ユニークな場所: %d
+ 時間: %s
+ 場所: 緯度: %s, 経度: %s
+ マップ: %s
+ マップを読み込み中…
+ 信号強度: %d
+ AirGuard %s for Androidによって作成
+ 情報:
+ マップ:
+ このトラッカーは以下で検知されました:
+ このトラッカーは以下でも検知されました(続き):
+ ページ %d/%d
+ 警告: このエントリは推定値に基づいています。誤検知の可能性があります。
+ マップを読み込めません。インターネット接続を確認してください。
+ Bluetoothエラー検知
+ このデバイスはAndroid 15以上で発生するBluetooth Low Energyスキャンの不具合の影響を受けています。すべてのアプリがBluetoothスキャンを利用できません。スマートフォンを再起動してください。
+ Bluetoothスキャン中に問題が発生しました。AirGuardに必要な権限があり、Bluetoothがオンになっていることを確認してください。問題が解決しない場合はスマートフォンを再起動してください。
+ コメントを追加
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b03802b0..735264bd 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -465,7 +465,7 @@
Time: %s"Location: Latitude: %s, Longitude: %s"Map: %s
- Map still loading...
+ Map still loading…Signal Strength: %d"Created by AirGuard %s for AndroidInformation:
From f3f3184357872cf3efe9f62582284a697da2f488 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Thu, 4 Sep 2025 13:59:49 +0200
Subject: [PATCH 53/71] reduce throttle window in ScanOrchestrator to ensure
bluetooth scan access for high priority scans in edge cases
---
.../util/ble/ScanOrchestrator.kt | 73 +++++++++++++------
1 file changed, 51 insertions(+), 22 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt
index 164b5deb..5527327f 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/ble/ScanOrchestrator.kt
@@ -21,8 +21,10 @@ object ScanOrchestrator {
enum class Priority { HIGH, MEDIUM, LOW }
private const val STOP_START_GRACE_MS = 400L
+ // Increase throttle window and decrease max starts to free up slots for high priority
private const val THROTTLE_WINDOW_MS = 30_000L
- private const val THROTTLE_MAX_STARTS = 5
+ private const val THROTTLE_MAX_STARTS = 2 // This is actually 5, but we bypass front end scans, so we leave 3 as buffer
+ private var lastLowScanTime: Long = 0L // Ensures max. 1 Low scan per throttle window
private val appContext: Context
get() = ATTrackingDetectionApplication.getAppContext()
@@ -92,22 +94,38 @@ object ScanOrchestrator {
return try { adapter.bluetoothLeScanner } catch (_: Throwable) { null }
}
- private fun canStartNow(): Boolean {
+ private fun canStartNow(priority: Priority): Boolean {
val now = System.currentTimeMillis()
- while (true) {
- val head = recentStarts.peekFirst() ?: break
- if (now - head > THROTTLE_WINDOW_MS) {
- recentStarts.removeFirst()
- } else break
+ when (priority) {
+ Priority.MEDIUM -> {
+ while (true) {
+ val head = recentStarts.peekFirst() ?: break
+ if (now - head > THROTTLE_WINDOW_MS) {
+ recentStarts.removeFirst()
+ } else break
+ }
+ return recentStarts.size < THROTTLE_MAX_STARTS
+ }
+ Priority.LOW -> {
+ return (now - lastLowScanTime) > THROTTLE_WINDOW_MS
+ }
+ Priority.HIGH -> return true
}
- return recentStarts.size < THROTTLE_MAX_STARTS
}
- private fun recordStart() {
+ private fun recordStart(priority: Priority) {
val now = System.currentTimeMillis()
- recentStarts.addLast(now)
- while (recentStarts.size > THROTTLE_MAX_STARTS) {
- recentStarts.removeFirst()
+ when (priority) {
+ Priority.MEDIUM -> {
+ recentStarts.addLast(now)
+ while (recentStarts.size > THROTTLE_MAX_STARTS) {
+ recentStarts.removeFirst()
+ }
+ }
+ Priority.LOW -> {
+ lastLowScanTime = now
+ }
+ Priority.HIGH -> { /* no throttling */ }
}
}
@@ -134,10 +152,23 @@ object ScanOrchestrator {
return@post
}
- // Preemption logic
+ // Always process High Priority scan requests immediately, no exceptions
+ if (priority == Priority.HIGH) {
+ if (isStartingOrRunning || isStopping) {
+ Timber.d("Force stopping current scan for HIGH priority request from $callerTag")
+ internalStop(scanner, currentCallback)
+ handler.postDelayed({
+ internalStart(scanner, filters, settings, callback, priority, callerTag)
+ }, STOP_START_GRACE_MS)
+ } else {
+ internalStart(scanner, filters, settings, callback, priority, callerTag)
+ }
+ return@post
+ }
+
+ // For Medium/Low, use preemption and throttling logic
if (isStartingOrRunning || isStopping) {
val runningPrio = currentPriority
- // If current is lower priority than requested, preempt
val shouldPreempt = runningPrio != null && priorityHigherThan(priority, runningPrio)
if (shouldPreempt) {
Timber.d("Preempting $currentOwnerTag ($runningPrio) with $callerTag ($priority)")
@@ -148,12 +179,10 @@ object ScanOrchestrator {
return@post
}
- // If not preempting, only replace if allowed and same or lower priority caller requests it
if (!allowReplaceExisting) {
Timber.d("Scan already running or stopping; ignoring request from $callerTag")
return@post
}
- // If same caller and same or higher priority, we can replace (e.g., change mode)
if (callerTag == currentOwnerTag) {
Timber.d("Replacing running scan with new request from same owner $callerTag")
internalStop(scanner, currentCallback)
@@ -166,9 +195,9 @@ object ScanOrchestrator {
return@post
}
- // High priority scans bypass throttling. This ensures front end scans always work
- if (priority != Priority.HIGH && !canStartNow()) {
- Timber.w("Scan start throttled for $callerTag: too many starts within window")
+ // Throttling for Medium/Low only
+ if (!canStartNow(priority)) {
+ Timber.w("Scan start throttled for $callerTag: too many starts within window or too soon")
return@post
}
@@ -211,7 +240,7 @@ object ScanOrchestrator {
return@post
}
- if (!canStartNow()) {
+ if (!canStartNow(Priority.LOW)) {
Timber.w("ensureRunningLease: throttled; will not start now")
return@post
}
@@ -268,7 +297,7 @@ object ScanOrchestrator {
currentSettings = settings
currentPriority = priority
currentOwnerTag = ownerTag
- recordStart()
+ recordStart(priority)
scanner.startScan(filters, settings, callback)
Timber.d("BLE scan started owner=$ownerTag mode=${settings.scanMode} priority=$priority")
} catch (sec: SecurityException) {
@@ -328,7 +357,7 @@ object ScanOrchestrator {
val lCallback = leaseCallback ?: return
if (!hasScanPermission()) return
val scanner = getLeScanner() ?: return
- if (!canStartNow()) return
+ if (!canStartNow(Priority.LOW)) return
internalStart(scanner, lFilters, lSettings, lCallback, Priority.LOW, lTag)
}
From 7ed71f4bcfcdd0acd7f6b12ac0b3952350643140 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Thu, 4 Sep 2025 15:38:03 +0200
Subject: [PATCH 54/71] upgrade gradle and some dependencies
---
app/build.gradle | 5 +++--
build.gradle | 2 +-
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index 07dc95d3..9a5abed0 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -89,7 +89,7 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation 'androidx.core:core-ktx:1.17.0'
implementation 'androidx.appcompat:appcompat:1.7.1'
- implementation 'com.google.android.material:material:1.12.0'
+ 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.3'
@@ -101,6 +101,7 @@ dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.4.0'
implementation "androidx.profileinstaller:profileinstaller:1.4.1"
+ implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation "androidx.activity:activity-ktx:1.10.1"
implementation 'com.squareup.retrofit2:retrofit:3.0.0'
@@ -127,7 +128,7 @@ dependencies {
implementation 'com.github.mukeshsolanki:MarkdownView-Android:2.0.0'
- implementation 'com.github.bumptech.glide:glide:4.16.0'
+ implementation 'com.github.bumptech.glide:glide:5.0.4'
ksp "com.google.dagger:hilt-compiler:$hilt_compiler_version"
ksp "androidx.hilt:hilt-compiler:$hilt_version"
diff --git a/build.gradle b/build.gradle
index bc8b745e..0144e1bb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -19,7 +19,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.12.2'
+ 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.3"
From dbacf89ec39aafd220906c6603db97b6256c5fdc Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 8 Sep 2025 15:50:56 +0200
Subject: [PATCH 55/71] update proguard rules for ScanOrchestrator
---
app/proguard-rules.pro | 25 ++++++++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 065b0a6d..c9dbb4d3 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -124,4 +124,27 @@
-keepattributes Signature,RuntimeVisibleAnnotations
# Keep all data classes with Gson annotations
--keep class de.seemoo.at_tracking_detection.database.relations.** { *; }
\ No newline at end of file
+-keep class de.seemoo.at_tracking_detection.database.relations.** { *; }
+
+# --- BLE ScanOrchestrator and BLE-related keep rules ---
+# Keep the ScanOrchestrator singleton and all its methods/fields
+-keep class de.seemoo.at_tracking_detection.util.ble.ScanOrchestrator { *; }
+
+# Keep all classes that extend or implement android.bluetooth.le.ScanCallback
+-keep class * extends android.bluetooth.le.ScanCallback { *; }
+
+# Keep all classes that extend or implement android.bluetooth.le.ScanFilter
+-keep class * extends android.bluetooth.le.ScanFilter { *; }
+
+# Keep all classes that extend or implement android.bluetooth.le.ScanSettings
+-keep class * extends android.bluetooth.le.ScanSettings { *; }
+
+# Keep all classes in the BLE util package (if you have more helpers)
+-keep class de.seemoo.at_tracking_detection.util.ble.** { *; }
+
+# Keep all ScanCallback implementations, including anonymous and inner classes
+-keep class * extends android.bluetooth.le.ScanCallback { *; }
+-keepclassmembers class * {
+ void onScanResult(...);
+ void onScanFailed(...);
+}
\ No newline at end of file
From dd95266372cb0dea2a6399536478c2d725473603 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 8 Sep 2025 15:55:54 +0200
Subject: [PATCH 56/71] fix default value for permanent bluetooth scanner
---
app/src/main/res/xml/fragment_settings.xml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/src/main/res/xml/fragment_settings.xml b/app/src/main/res/xml/fragment_settings.xml
index 5653b549..a0ca9669 100644
--- a/app/src/main/res/xml/fragment_settings.xml
+++ b/app/src/main/res/xml/fragment_settings.xml
@@ -27,7 +27,8 @@
android:icon="@drawable/ic_scan_icon"
android:key="use_permanent_bluetooth_scanner"
android:summary="@string/settings_permanent_scanning_summary"
- app:title="@string/settings_permanent_scanning_title" />
+ app:title="@string/settings_permanent_scanning_title"
+ app:defaultValue="true" />
Date: Tue, 9 Sep 2025 12:11:32 +0200
Subject: [PATCH 57/71] set permanentscanner default to disabled
---
.../java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt | 2 +-
app/src/main/res/xml/fragment_settings.xml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
index 8b7bbc73..5c64652f 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
@@ -289,7 +289,7 @@ object SharedPrefs {
var usePermanentBluetoothScanner: Boolean
get() {
- return sharedPreferences.getBoolean("use_permanent_bluetooth_scanner", true)
+ return sharedPreferences.getBoolean("use_permanent_bluetooth_scanner", false)
}
set(value) {
sharedPreferences.edit { putBoolean("use_permanent_bluetooth_scanner", value) }
diff --git a/app/src/main/res/xml/fragment_settings.xml b/app/src/main/res/xml/fragment_settings.xml
index a0ca9669..fb970a75 100644
--- a/app/src/main/res/xml/fragment_settings.xml
+++ b/app/src/main/res/xml/fragment_settings.xml
@@ -28,7 +28,7 @@
android:key="use_permanent_bluetooth_scanner"
android:summary="@string/settings_permanent_scanning_summary"
app:title="@string/settings_permanent_scanning_title"
- app:defaultValue="true" />
+ app:defaultValue="false" />
Date: Tue, 9 Sep 2025 12:28:04 +0200
Subject: [PATCH 58/71] F-Droid Release 2.6.0
---
api.properties | 2 +-
fastlane/metadata/android/de-DE/changelogs/54.txt | 6 ++++++
fastlane/metadata/android/en-US/changelogs/54.txt | 6 ++++++
3 files changed, 13 insertions(+), 1 deletion(-)
create mode 100644 fastlane/metadata/android/de-DE/changelogs/54.txt
create mode 100644 fastlane/metadata/android/en-US/changelogs/54.txt
diff --git a/api.properties b/api.properties
index 371683d8..cf3f4b34 100644
--- a/api.properties
+++ b/api.properties
@@ -1,2 +1,2 @@
-API_KEY="g3T1wgsH.bb40FsxMYET7HC12I8CH83jtQ1FT0a9A"
+API_KEY="0pGhFp5V.3Me6ZeM4hqFnu8FDMrqZwpaau1fdE98Y"
API_BASE_ADDRESS="https://tpe.seemoo.tu-darmstadt.de/api/"
\ No newline at end of file
diff --git a/fastlane/metadata/android/de-DE/changelogs/54.txt b/fastlane/metadata/android/de-DE/changelogs/54.txt
new file mode 100644
index 00000000..d96ceca8
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/54.txt
@@ -0,0 +1,6 @@
+NEU: PDF-Export mit Übersicht aller gefundenen Standorte
+NEU: Jeder gefundene Tracker kann mit einem Kommentar versehen werden
+VERBESSERT: Hintergrund-Scan verbessert
+VERBESSERT: Kompatibilität mit Android 15 und 16
+VERBESSERT: Japanische Übersetzung
+VERBESSERT: Fehlerbehebungen
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/54.txt b/fastlane/metadata/android/en-US/changelogs/54.txt
new file mode 100644
index 00000000..30c0c2e3
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/54.txt
@@ -0,0 +1,6 @@
+NEW: PDF Export with an overview of all found locations
+NEW: Each found tracker can be annotated with a comment
+IMPROVED: Background Scanning improved
+IMPROVED: Compatibility with Android 15 and 16
+IMPROVED: Japanese Translation
+IMPROVED: Bugfixes
\ No newline at end of file
From 1337f5822ee3a5ff14fece9b5534f88ed80376f0 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Tue, 9 Sep 2025 15:59:34 +0200
Subject: [PATCH 59/71] update dependencies
---
app/build.gradle | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/build.gradle b/app/build.gradle
index 418a50d9..d2323c87 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -175,7 +175,7 @@ dependencies {
// 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.7.0")
+ implementation("com.google.android.gms:play-services-base:18.7.2")
}
ksp {
From 32cca2c3271dce58584ff4042b80ccd2790c3f8e Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Tue, 9 Sep 2025 17:14:11 +0200
Subject: [PATCH 60/71] fix screenshots in ReadMe
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 11db355b..e6e40e01 100644
--- a/README.md
+++ b/README.md
@@ -15,10 +15,10 @@
-
+
+
-
-
+
**Protect yourself from Find My Tracking**
From ee7785f8da1ad51c9cfa3d3476f92b89df1652e3 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 15 Sep 2025 12:54:05 +0200
Subject: [PATCH 61/71] Notification permission can now be denied, if disabled
a warning will be shown on the home screen
---
.../ATTrackingDetectionApplication.kt | 7 ++++++-
.../ui/OnboardingActivity.kt | 4 +++-
.../ui/dashboard/DashboardRiskFragment.kt | 17 +++++++++++++++++
.../at_tracking_detection/util/SharedPrefs.kt | 7 +++++++
app/src/main/res/values-de/strings.xml | 2 ++
app/src/main/res/values-ja/strings.xml | 2 ++
app/src/main/res/values/strings.xml | 2 ++
7 files changed, 39 insertions(+), 2 deletions(-)
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 2b8566a3..fa49c370 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
@@ -170,7 +170,12 @@ class ATTrackingDetectionApplication : Application(), Configuration.Provider {
requiredPermissions.add(Manifest.permission.BLUETOOTH_CONNECT)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- requiredPermissions.add(Manifest.permission.POST_NOTIFICATIONS)
+ // requiredPermissions.add(Manifest.permission.POST_NOTIFICATIONS)
+ SharedPrefs.showMissingNotificationPermissionWarning =
+ ContextCompat.checkSelfPermission(
+ applicationContext,
+ Manifest.permission.POST_NOTIFICATIONS
+ ) != PackageManager.PERMISSION_GRANTED
}
for (permission in requiredPermissions) {
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 02c11946..7a38e45a 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
@@ -140,7 +140,7 @@ class OnboardingActivity : AppIntro() {
askForPermissions(
permissions = arrayOf(Manifest.permission.BLUETOOTH_SCAN),
slideNumber = slideNumber,
- required = false
+ required = true
)
return true
}
@@ -234,6 +234,8 @@ class OnboardingActivity : AppIntro() {
private fun handleRequiredPermission(permissionName: String) {
if (permissionName == Manifest.permission.ACCESS_BACKGROUND_LOCATION) {
SharedPrefs.useLocationInTrackingDetection = false
+ } else if (permissionName == Manifest.permission.POST_NOTIFICATIONS) {
+ goToNextSlide()
} else if (dialog?.isShowing != true) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.permission_required)
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 d72d98fb..d49842c6 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt
@@ -1,7 +1,10 @@
package de.seemoo.at_tracking_detection.ui.dashboard
import android.annotation.SuppressLint
+import android.content.Context
import android.os.Build
+import android.Manifest
+import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -11,6 +14,7 @@ import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@@ -112,6 +116,19 @@ class DashboardRiskFragment : Fragment() {
articles = listOf(bugArticle) + articles
}
+ if (SharedPrefs.showMissingNotificationPermissionWarning) {
+ val notificationArticle = Article(
+ title = getString(R.string.notification_permission_missing_title),
+ author = "System",
+ readingTime = 0,
+ previewText = getString(R.string.notification_permission_missing_text),
+ cardColor = "warning_light_red",
+ preview_image = "",
+ filename = ""
+ )
+ articles = listOf(notificationArticle) + articles
+ }
+
// Create a new LinearLayout to hold the ArticleCards
val articleCardsLinearLayout = LinearLayout(context)
articleCardsLinearLayout.orientation = LinearLayout.VERTICAL
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
index 6eb6a04b..19454a2f 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
@@ -208,6 +208,13 @@ object SharedPrefs {
sharedPreferences.edit { putBoolean("samsung_bug_notification", value) }
}
+ var showMissingNotificationPermissionWarning: Boolean
+ get() {
+ return sharedPreferences.getBoolean("missing_notification_permission_warning", false)
+ }set(value) {
+ sharedPreferences.edit { putBoolean("missing_notification_permission_warning", value) }
+ }
+
var showGenericBluetoothBugNotification: Boolean
get() {
return sharedPreferences.getBoolean("generic_bluetooth_bug_notification", false)
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index bf01d8ea..8cc7455d 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -439,4 +439,6 @@
Es gab Probleme beim Scannen von Bluetooth. Stelle sicher, dass AirGuard alle erforderlichen Berechtigungen hat und dass Bluetooth eingeschaltet ist. Falls dieser Fehler weiterhin auftritt, versuche, dein Smartphone neu zu starten.Kommentar zu diesem Tracker
+ Benachrichtigungen sind deaktiviert
+ Benachrichtigungen sind für diese App deaktiviert. Du wirst nicht gewarnt, wenn dir ein Tracker folgen sollte.
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index ee0ed00b..4c04203f 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -405,4 +405,6 @@
このデバイスはAndroid 15以上で発生するBluetooth Low Energyスキャンの不具合の影響を受けています。すべてのアプリがBluetoothスキャンを利用できません。スマートフォンを再起動してください。Bluetoothスキャン中に問題が発生しました。AirGuardに必要な権限があり、Bluetoothがオンになっていることを確認してください。問題が解決しない場合はスマートフォンを再起動してください。コメントを追加
+ 通知が無効になっています
+ このデバイスでは通知が無効になっています。トラッカーがあなたを追跡している場合、警告は表示されません。
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 735264bd..b5db56bf 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -481,4 +481,6 @@
There have been issues while scanning bluetooth. Make sure that AirGuard has all required permissions and that Bluetooth is turned on. If this error still persists, try to restart your phone.Add a comment
+ Notifications disabled
+ Notifications are disabled for this app. You will not be warned when a tracker has been following you.
From 6217e79397326d6ee9db7364febac2c700b35e76 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Tue, 16 Sep 2025 15:07:46 +0200
Subject: [PATCH 62/71] Background Location can now be denied, if disabled a
warning will be shown on the home screen
---
.../ATTrackingDetectionApplication.kt | 13 ++++++++++++-
.../ui/OnboardingActivity.kt | 1 +
.../ui/dashboard/DashboardRiskFragment.kt | 18 +++++++++++++-----
.../at_tracking_detection/util/SharedPrefs.kt | 18 ++++++++++++------
app/src/main/res/values-de/strings.xml | 4 +++-
app/src/main/res/values-ja/strings.xml | 4 +++-
app/src/main/res/values/strings.xml | 6 ++++--
7 files changed, 48 insertions(+), 16 deletions(-)
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 fa49c370..50f53943 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
@@ -177,6 +177,17 @@ class ATTrackingDetectionApplication : Application(), Configuration.Provider {
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val backgroundLocationPermission =
+ ContextCompat.checkSelfPermission(
+ applicationContext,
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION
+ ) == PackageManager.PERMISSION_GRANTED
+ SharedPrefs.showMissingBackgroundLocationPermissionWarning = !backgroundLocationPermission
+ if (backgroundLocationPermission) {
+ SharedPrefs.useLocationInTrackingDetection = true
+ }
+ }
for (permission in requiredPermissions) {
val granted = ContextCompat.checkSelfPermission(
@@ -222,7 +233,7 @@ class ATTrackingDetectionApplication : Application(), Configuration.Provider {
fun getCurrentApp(): ATTrackingDetectionApplication {
return instance
}
- //TODO: Add real survey URL
+
const val SURVEY_URL = "https://survey.seemoo.tu-darmstadt.de/index.php/117478?G06Q39=AirGuardAppAndroid&newtest=Y&lang=en"
const val SURVEY_IS_RUNNING = false
}
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 7a38e45a..56da9818 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
@@ -234,6 +234,7 @@ class OnboardingActivity : AppIntro() {
private fun handleRequiredPermission(permissionName: String) {
if (permissionName == Manifest.permission.ACCESS_BACKGROUND_LOCATION) {
SharedPrefs.useLocationInTrackingDetection = false
+ goToNextSlide()
} else if (permissionName == Manifest.permission.POST_NOTIFICATIONS) {
goToNextSlide()
} else if (dialog?.isShowing != true) {
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 d49842c6..740d9caf 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/DashboardRiskFragment.kt
@@ -1,10 +1,6 @@
package de.seemoo.at_tracking_detection.ui.dashboard
import android.annotation.SuppressLint
-import android.content.Context
-import android.os.Build
-import android.Manifest
-import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -14,7 +10,6 @@ import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@@ -129,6 +124,19 @@ class DashboardRiskFragment : Fragment() {
articles = listOf(notificationArticle) + articles
}
+ if (SharedPrefs.showMissingBackgroundLocationPermissionWarning) {
+ val locationArticle = Article(
+ title = getString(R.string.background_location_permission_missing_title),
+ author = "System",
+ readingTime = 0,
+ previewText = getString(R.string.background_location_permission_missing_text),
+ cardColor = "warning_light_red",
+ preview_image = "",
+ filename = ""
+ )
+ articles = listOf(locationArticle) + articles
+ }
+
// Create a new LinearLayout to hold the ArticleCards
val articleCardsLinearLayout = LinearLayout(context)
articleCardsLinearLayout.orientation = LinearLayout.VERTICAL
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
index 19454a2f..9aa7821b 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/SharedPrefs.kt
@@ -197,28 +197,35 @@ object SharedPrefs {
var dismissSurveyInformation: Boolean
get() {
return sharedPreferences.getBoolean("dismiss_survey_information", false)
- }set(value) {
+ } set(value) {
sharedPreferences.edit { putBoolean("dismiss_survey_information", value) }
}
var showSamsungAndroid15BugNotification: Boolean
get() {
return sharedPreferences.getBoolean("samsung_bug_notification", false)
- }set(value) {
+ } set(value) {
sharedPreferences.edit { putBoolean("samsung_bug_notification", value) }
}
var showMissingNotificationPermissionWarning: Boolean
get() {
return sharedPreferences.getBoolean("missing_notification_permission_warning", false)
- }set(value) {
+ } set(value) {
sharedPreferences.edit { putBoolean("missing_notification_permission_warning", value) }
}
+ var showMissingBackgroundLocationPermissionWarning: Boolean
+ get() {
+ return sharedPreferences.getBoolean("missing_background_permission_warning", false)
+ } set(value) {
+ sharedPreferences.edit { putBoolean("missing_background_permission_warning", value) }
+ }
+
var showGenericBluetoothBugNotification: Boolean
get() {
return sharedPreferences.getBoolean("generic_bluetooth_bug_notification", false)
- }set(value) {
+ } set(value) {
sharedPreferences.edit { putBoolean("generic_bluetooth_bug_notification", value) }
}
@@ -233,8 +240,7 @@ object SharedPrefs {
}
}
return null
- }
- set(value) {
+ } set(value) {
sharedPreferences.edit {
putString(
"survey_notification_date",
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 8cc7455d..9df18a39 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -440,5 +440,7 @@
Kommentar zu diesem TrackerBenachrichtigungen sind deaktiviert
- Benachrichtigungen sind für diese App deaktiviert. Du wirst nicht gewarnt, wenn dir ein Tracker folgen sollte.
+ Benachrichtigungen sind für diese App deaktiviert. Du wirst nicht gewarnt, wenn dir ein Tracker folgen sollte. Um dies zu ändern, aktiviere Benachrichtigungen für diese App in deinen Systemeinstellungen.
+ Hintergrund-Standortberechtigung deaktiviert
+ Die Berechtigung für den Standort im Hintergrund wurde dieser App nicht erteilt. Du wirst nicht gewarnt, wenn dir ein Tracker folgt, während die App geschlossen ist. Um dies zu ändern, aktiviere die Hintergrund-Standortberechtigung für diese App in deinen Systemeinstellungen.
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 4c04203f..1cfd1dd0 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -406,5 +406,7 @@
Bluetoothスキャン中に問題が発生しました。AirGuardに必要な権限があり、Bluetoothがオンになっていることを確認してください。問題が解決しない場合はスマートフォンを再起動してください。コメントを追加通知が無効になっています
- このデバイスでは通知が無効になっています。トラッカーがあなたを追跡している場合、警告は表示されません。
+ このアプリでは通知が無効になっています。トラッカーがあなたを追跡している場合、警告は表示されません。通知を有効にするには、システム設定でこのアプリの通知を有効にしてください。
+ バックグラウンドでの位置情報取得が無効です
+ このアプリにはバックグラウンドでの位置情報取得の許可がありません。アプリが閉じている間にトラッカーがあなたを追跡しても警告されません。システム設定でバックグラウンド位置情報を有効にしてください。
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b5db56bf..e0af794c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -481,6 +481,8 @@
There have been issues while scanning bluetooth. Make sure that AirGuard has all required permissions and that Bluetooth is turned on. If this error still persists, try to restart your phone.Add a comment
- Notifications disabled
- Notifications are disabled for this app. You will not be warned when a tracker has been following you.
+ Notification Permission disabled
+ Notifications are disabled for this app. You will not be warned when a tracker has been following you. To change this enable notifications for this app in your system settings.
+ Background Location Permission disabled
+ The background locations permission has not been granted for this app. You will not be warned when a tracker has been following you while this app is closed. To change this enable background locations for this app in your system settings.
From 708b53052dee24e4b1d842ceb7dafd240733ca87 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Tue, 16 Sep 2025 15:27:55 +0200
Subject: [PATCH 63/71] fix a bug where false alarm toggle was not displayed
correctly
---
.../at_tracking_detection/database/daos/NotificationDao.kt | 3 +++
.../database/repository/NotificationRepository.kt | 2 ++
.../at_tracking_detection/ui/tracking/TrackingViewModel.kt | 1 +
3 files changed, 6 insertions(+)
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 168d8744..28c81022 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
@@ -62,6 +62,9 @@ interface NotificationDao {
@Query("SELECT COUNT(*) FROM notification WHERE deviceAddress == :deviceAddress AND createdAt >= :since AND falseAlarm = 1")
fun getFalseAlarmForDeviceSinceCount(deviceAddress: String, since: LocalDateTime): Int
+ @Query("SELECT COUNT(*) FROM notification WHERE deviceAddress == :deviceAddress AND falseAlarm = 1")
+ fun getFalseAlarmForDeviceCount(deviceAddress: String): Int
+
@Transaction
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM notification")
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 0aeee895..ef7f6a80 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 getFalseAlarmForDeviceCount(deviceAddress: String): Int = notificationDao.getFalseAlarmForDeviceCount(deviceAddress)
+
fun existsNotificationForDevice(deviceAddress: String): Boolean = notificationDao.existsNotificationForDevice(deviceAddress)
fun getAllNotifications(): List = notificationDao.getAllNotifications()
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 746bb274..3ac69fa5 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
@@ -89,6 +89,7 @@ class TrackingViewModel @Inject constructor(
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 {
From c0d57d6d945422f5fd9d6386965d5c792d10928e Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Thu, 18 Sep 2025 17:15:07 +0200
Subject: [PATCH 64/71] fix issue where notification buttons where not properly
processed and dismissed
---
.../NotificationActionReceiver.kt | 18 +++++++++++----
.../notifications/NotificationBuilder.kt | 21 +++++++++++++-----
.../notifications/NotificationService.kt | 2 +-
.../notifications/worker/FalseAlarmWorker.kt | 14 ++++++++----
.../worker/IgnoreDeviceWorker.kt | 11 ++++++++--
.../worker/BackgroundWorkBuilder.kt | 12 ++++++----
.../worker/BackgroundWorkScheduler.kt | 22 +++++++++----------
7 files changed, 69 insertions(+), 31 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationActionReceiver.kt b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationActionReceiver.kt
index 3e504878..7ff50449 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationActionReceiver.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationActionReceiver.kt
@@ -38,6 +38,15 @@ class NotificationActionReceiver : BroadcastReceiver() {
Timber.e("Notification id missing!")
return
}
+ val notificationTag = intent.getStringExtra("notificationTag")
+
+ fun cancelNotification() {
+ if (notificationTag.isNullOrEmpty()) {
+ notificationManagerCompat.cancel(notificationId)
+ } else {
+ notificationManagerCompat.cancel(notificationTag, notificationId)
+ }
+ }
when (intent.action) {
NotificationConstants.FALSE_ALARM_ACTION -> {
@@ -46,7 +55,8 @@ class NotificationActionReceiver : BroadcastReceiver() {
notificationRepository.setFalseAlarm(notificationId, true)
}
// Dismiss the notification immediately for this action.
- notificationManagerCompat.cancel(notificationId)
+ cancelNotification()
+ backgroundWorkScheduler.scheduleFalseAlarm(notificationId, notificationTag)
}
NotificationConstants.IGNORE_DEVICE_ACTION -> {
val deviceAddress = intent.getStringExtra("deviceAddress")
@@ -59,13 +69,14 @@ class NotificationActionReceiver : BroadcastReceiver() {
deviceRepository.setIgnoreFlag(deviceAddress, true)
}
// Dismiss the notification immediately for this action.
- notificationManagerCompat.cancel(notificationId)
+ cancelNotification()
+ backgroundWorkScheduler.scheduleIgnoreDevice(deviceAddress, notificationId, notificationTag)
}
NotificationConstants.CLICKED_ACTION -> {
GlobalScope.launch(Dispatchers.IO) {
notificationRepository.setClicked(notificationId, true)
}
- notificationManagerCompat.cancel(notificationId)
+ cancelNotification()
}
NotificationConstants.DISMISSED_ACTION -> {
GlobalScope.launch(Dispatchers.IO) {
@@ -75,4 +86,3 @@ class NotificationActionReceiver : BroadcastReceiver() {
}
}
}
-
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 ca3f21e5..a1a58010 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
@@ -29,7 +29,7 @@ import javax.inject.Singleton
@Singleton
class NotificationBuilder @Inject constructor(
- @ApplicationContext private val context: Context
+ @param:ApplicationContext private val context: Context
) {
private fun pendingNotificationIntent(bundle: Bundle, notificationId: Int): PendingIntent {
@@ -118,7 +118,10 @@ class NotificationBuilder @Inject constructor(
}
?: "UNKNOWN"
- val bundle: Bundle = packBundle(deviceAddress, deviceTypeString, notificationId)
+ val bundle: Bundle = packBundle(deviceAddress, deviceTypeString, notificationId).apply {
+ // Provide the tag so the receiver can cancel using tag+id
+ putString("notificationTag", NotificationService.TRACKING_NOTIFICATION_TAG)
+ }
val device = baseDevice.device
val notificationText: String
val notificationTitle: String
@@ -200,7 +203,9 @@ class NotificationBuilder @Inject constructor(
observationPositive: Boolean
): Notification {
Timber.d("Notification with id $notificationId for device $deviceAddress has been build!")
- val bundle: Bundle = packBundle(deviceAddress, deviceTypeString, notificationId)
+ val bundle: Bundle = packBundle(deviceAddress, deviceTypeString, notificationId).apply {
+ putString("notificationTag", NotificationService.OBSERVE_TRACKER_NOTIFICATION_TAG)
+ }
val notifyText = if (observationPositive) {
context.resources.getQuantityString(
@@ -236,7 +241,10 @@ class NotificationBuilder @Inject constructor(
}
fun buildObserveTrackerFailedNotification(notificationId: Int): Notification {
- val bundle: Bundle = Bundle().apply { putInt("notificationId", notificationId) }
+ val bundle: Bundle = Bundle().apply {
+ putInt("notificationId", notificationId)
+ putString("notificationTag", NotificationService.OBSERVE_TRACKER_NOTIFICATION_TAG)
+ }
return NotificationCompat.Builder(context, NotificationConstants.CHANNEL_ID)
.setContentTitle(context.getString(R.string.notification_observe_tracker_title_base))
@@ -251,7 +259,10 @@ class NotificationBuilder @Inject constructor(
fun buildBluetoothErrorNotification(): Notification {
val notificationId = -100
- val bundle: Bundle = Bundle().apply { putInt("notificationId", notificationId) }
+ val bundle: Bundle = Bundle().apply {
+ putInt("notificationId", notificationId)
+ putString("notificationTag", NotificationService.BLE_SCAN_ERROR_TAG)
+ }
return NotificationCompat.Builder(context, NotificationConstants.CHANNEL_ID)
.setContentTitle(context.getString(R.string.notification_title_ble_error))
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 6ffc5a31..2a8bed77 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
@@ -175,7 +175,7 @@ class NotificationService @Inject constructor(
"de.seemoo.at_tracking_detection.ble_scan_error_notification"
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"
+ const val SURVEY_INFO_TAG = "de.seemoo.at_tracking_detection.survey_info"
fun generateUniqueNotificationId(): Int {
return Random.nextInt()
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/worker/FalseAlarmWorker.kt b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/worker/FalseAlarmWorker.kt
index b0ab1d23..6f1e7f62 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/worker/FalseAlarmWorker.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/worker/FalseAlarmWorker.kt
@@ -17,17 +17,23 @@ class FalseAlarmWorker @AssistedInject constructor(
private val notificationViewModel: NotificationViewModel,
private val notificationManagerCompat: NotificationManagerCompat
) : CoroutineWorker(appContext, workerParams) {
-
override suspend fun doWork(): Result {
val notificationId = inputData.getInt("notificationId", -1)
if (notificationId == -1) {
Timber.e("No notification id passed!")
return Result.failure()
}
+ val notificationTag = inputData.getString("notificationTag")
+
notificationViewModel.setFalseAlarm(notificationId, true)
- //TODO: cancel specific notification by calling cancel(notificationId) which somehow doesn't work...
- notificationManagerCompat.cancelAll()
- Timber.d("Marked notification $notificationId as false alarm!")
+
+ if (notificationTag.isNullOrEmpty()) {
+ notificationManagerCompat.cancel(notificationId)
+ } else {
+ notificationManagerCompat.cancel(notificationTag, notificationId)
+ }
+
+ Timber.d("Marked notification $notificationId as false alarm and canceled it!")
return Result.success()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/worker/IgnoreDeviceWorker.kt b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/worker/IgnoreDeviceWorker.kt
index 7e241581..9b0066cb 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/worker/IgnoreDeviceWorker.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/worker/IgnoreDeviceWorker.kt
@@ -29,9 +29,16 @@ class IgnoreDeviceWorker @AssistedInject constructor(
Timber.e("No notification id passed!")
return Result.failure()
}
+ val notificationTag = inputData.getString("notificationTag")
+
deviceViewModel.setIgnoreFlag(deviceAddress, true)
- //TODO: cancel specific notification by calling cancel(notificationId) which somehow doesn't work...
- notificationManagerCompat.cancelAll()
+
+ if (notificationTag.isNullOrEmpty()) {
+ notificationManagerCompat.cancel(notificationId)
+ } else {
+ notificationManagerCompat.cancel(notificationTag, notificationId)
+ }
+
Timber.d("Added device $deviceAddress to the ignored devices list!")
return Result.success()
}
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/worker/BackgroundWorkBuilder.kt b/app/src/main/java/de/seemoo/at_tracking_detection/worker/BackgroundWorkBuilder.kt
index df037d0d..98745a61 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/worker/BackgroundWorkBuilder.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/worker/BackgroundWorkBuilder.kt
@@ -45,19 +45,23 @@ class BackgroundWorkBuilder @Inject constructor() {
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkerConstants.KIND_DELAY, TimeUnit.MINUTES)
.build()
- fun buildIgnoreDeviceWorker(deviceAddress: String, notificationId: Int): OneTimeWorkRequest =
+ fun buildIgnoreDeviceWorker(deviceAddress: String, notificationId: Int, notificationTag: String?): OneTimeWorkRequest =
OneTimeWorkRequestBuilder().addTag(WorkerConstants.IGNORE_DEVICE_WORKER)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkerConstants.KIND_DELAY, TimeUnit.MINUTES)
.setInputData(
Data.Builder().putString("deviceAddress", deviceAddress)
- .putInt("notificationId", notificationId).build()
+ .putInt("notificationId", notificationId)
+ .putString("notificationTag", notificationTag)
+ .build()
)
.build()
- fun buildFalseAlarmWorker(notificationId: Int): OneTimeWorkRequest =
+ fun buildFalseAlarmWorker(notificationId: Int, notificationTag: String?): OneTimeWorkRequest =
OneTimeWorkRequestBuilder().addTag(WorkerConstants.FALSE_ALARM_WORKER)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkerConstants.KIND_DELAY, TimeUnit.MINUTES)
- .setInputData(Data.Builder().putInt("notificationId", notificationId).build())
+ .setInputData(Data.Builder().putInt("notificationId", notificationId)
+ .putString("notificationTag", notificationTag)
+ .build())
.build()
private fun buildConstraints(): Constraints =
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/worker/BackgroundWorkScheduler.kt b/app/src/main/java/de/seemoo/at_tracking_detection/worker/BackgroundWorkScheduler.kt
index 04a1f598..7bf2d5b2 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/worker/BackgroundWorkScheduler.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/worker/BackgroundWorkScheduler.kt
@@ -7,19 +7,19 @@ import android.content.Intent
import android.os.Build
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
-import androidx.work.*
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.ExistingWorkPolicy
+import androidx.work.Operation
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
import de.seemoo.at_tracking_detection.BuildConfig
-import de.seemoo.at_tracking_detection.util.Utility
+import de.seemoo.at_tracking_detection.util.SharedPrefs
import timber.log.Timber
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
import javax.inject.Inject
import javax.inject.Singleton
-import android.Manifest
-import de.seemoo.at_tracking_detection.util.SharedPrefs
-import java.time.Instant
-import java.util.TimeZone
@Singleton
class BackgroundWorkScheduler @Inject constructor(
@@ -78,17 +78,17 @@ class BackgroundWorkScheduler @Inject constructor(
fun removeShareData() =
workManager.cancelUniqueWork(WorkerConstants.PERIODIC_SEND_STATISTICS_WORKER)
- fun scheduleIgnoreDevice(deviceAddress: String, notificationId: Int) =
+ fun scheduleIgnoreDevice(deviceAddress: String, notificationId: Int, notificationTag: String?) =
workManager.enqueueUniqueWork(
WorkerConstants.IGNORE_DEVICE_WORKER,
ExistingWorkPolicy.APPEND_OR_REPLACE,
- backgroundWorkBuilder.buildIgnoreDeviceWorker(deviceAddress, notificationId)
+ backgroundWorkBuilder.buildIgnoreDeviceWorker(deviceAddress, notificationId, notificationTag)
).also { it.logOperationSchedule(WorkerConstants.IGNORE_DEVICE_WORKER) }
- fun scheduleFalseAlarm(notificationId: Int) = workManager.enqueueUniqueWork(
- WorkerConstants.IGNORE_DEVICE_WORKER,
+ fun scheduleFalseAlarm(notificationId: Int, notificationTag: String?) = workManager.enqueueUniqueWork(
+ WorkerConstants.FALSE_ALARM_WORKER,
ExistingWorkPolicy.APPEND_OR_REPLACE,
- backgroundWorkBuilder.buildFalseAlarmWorker(notificationId)
+ backgroundWorkBuilder.buildFalseAlarmWorker(notificationId, notificationTag)
).also { it.logOperationSchedule(WorkerConstants.FALSE_ALARM_WORKER) }
private fun Operation.logOperationSchedule(uniqueWorker: String) =
From 60554d06c8afbdf6574a81129f714420d4eda027 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Thu, 18 Sep 2025 17:24:10 +0200
Subject: [PATCH 65/71] fix issue where notification dismissal was not
processed properly
---
.../notifications/NotificationBuilder.kt | 12 +++++++++++-
.../notifications/NotificationService.kt | 3 +--
.../ui/TrackingNotificationActivity.kt | 15 +++++++++++++++
3 files changed, 27 insertions(+), 3 deletions(-)
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 a1a58010..0a168d04 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
@@ -9,6 +9,7 @@ import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import androidx.core.app.NotificationCompat
+import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext
import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
import de.seemoo.at_tracking_detection.R
@@ -83,11 +84,14 @@ class NotificationBuilder @Inject constructor(
private fun buildPendingIntent(
bundle: Bundle,
notificationAction: String,
+ notificationId: Int,
code: Int
): PendingIntent {
val intent = Intent(context, NotificationActionReceiver::class.java).apply {
action = notificationAction
putExtras(bundle)
+ val tag = bundle.getString("notificationTag") ?: "none"
+ data = "airguard://notif/$tag/$notificationId/$notificationAction".toUri()
}
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -96,9 +100,11 @@ class NotificationBuilder @Inject constructor(
PendingIntent.FLAG_UPDATE_CURRENT
}
+ val uniqueRequestCode = (notificationId and 0x7FFFFFFF) xor (code shl 16)
+
return PendingIntent.getBroadcast(
context,
- code,
+ uniqueRequestCode,
intent,
flags
)
@@ -168,6 +174,7 @@ class NotificationBuilder @Inject constructor(
buildPendingIntent(
bundle,
NotificationConstants.FALSE_ALARM_ACTION,
+ notificationId,
NotificationConstants.FALSE_ALARM_CODE
)
).setAutoCancel(true)
@@ -179,6 +186,7 @@ class NotificationBuilder @Inject constructor(
buildPendingIntent(
bundle,
NotificationConstants.IGNORE_DEVICE_ACTION,
+ notificationId,
NotificationConstants.IGNORE_DEVICE_CODE
)
).setAutoCancel(true)
@@ -188,6 +196,7 @@ class NotificationBuilder @Inject constructor(
buildPendingIntent(
bundle,
NotificationConstants.DISMISSED_ACTION,
+ notificationId,
NotificationConstants.DISMISSED_CODE
)
).setAutoCancel(true)
@@ -232,6 +241,7 @@ class NotificationBuilder @Inject constructor(
buildPendingIntent(
bundle,
NotificationConstants.DISMISSED_ACTION,
+ notificationId,
NotificationConstants.DISMISSED_CODE
)
).setAutoCancel(true)
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 2a8bed77..ee5883e9 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
@@ -29,8 +29,7 @@ class NotificationService @Inject constructor(
) {
@SuppressLint("MissingPermission")
suspend fun sendTrackingNotification(baseDevice: BaseDevice) {
- val notificationId = generateUniqueNotificationId()
- notificationViewModel.insertToDb(deviceAddress = baseDevice.address)
+ val notificationId = notificationViewModel.insertToDb(deviceAddress = baseDevice.address)
with(notificationManagerCompat) {
if (this.areNotificationsEnabled() && !SharedPrefs.deactivateBackgroundScanning) {
notify(
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt
index 6689304d..d3a6a344 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt
@@ -4,18 +4,26 @@ import android.content.Intent
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import androidx.navigation.fragment.NavHostFragment
import dagger.hilt.android.AndroidEntryPoint
import de.seemoo.at_tracking_detection.R
+import de.seemoo.at_tracking_detection.database.repository.NotificationRepository
import de.seemoo.at_tracking_detection.ui.tracking.TrackingFragment
import de.seemoo.at_tracking_detection.ui.tracking.TrackingFragmentArgs
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import timber.log.Timber
+import javax.inject.Inject
@AndroidEntryPoint
class TrackingNotificationActivity : AppCompatActivity() {
+ @Inject
+ lateinit var notificationRepository: NotificationRepository
+
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
@@ -77,6 +85,13 @@ class TrackingNotificationActivity : AppCompatActivity() {
val notificationId = intent.getIntExtra("notificationId", -1)
Timber.d("Tracking Activity with device $deviceAddress and notification $notificationId started!")
+ // Mark as clicked if we have a valid notification id
+ if (notificationId != -1) {
+ lifecycleScope.launch(Dispatchers.IO) {
+ notificationRepository.setClicked(notificationId, true)
+ }
+ }
+
if (deviceAddress == null) {
Timber.e("Device address is needed! Going home...")
this.onSupportNavigateUp()
From 6ccbd996dab91abc2a0d4d7dea7f5356bca9c5af Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sat, 20 Sep 2025 16:18:58 +0200
Subject: [PATCH 66/71] fix map render issue under certain memory conditions
when accessing the TrackingFragment through a notification
---
.../at_tracking_detection/util/Utility.kt | 29 +++++++++++++++++--
1 file changed, 26 insertions(+), 3 deletions(-)
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 5b71a313..bf760e2b 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
@@ -19,6 +19,7 @@ import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.View
+import android.view.ViewTreeObserver
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityCompat.requestPermissions
@@ -153,6 +154,9 @@ object Utility {
)
+ // Remove previously added clusterer overlays to avoid performance issues
+ map.overlays.removeAll { it is RadiusMarkerClusterer }
+
val clusterer = RadiusMarkerClusterer(context)
val clusterIcon = BonusPackHelper.getBitmapFromVectorDrawable(context, icon)
clusterer.setIcon(clusterIcon)
@@ -186,7 +190,7 @@ object Utility {
if (geoPointList.isEmpty()) {
mapController.setZoom(MAX_ZOOM_LEVEL)
- map.post { map.invalidate() }
+ runWhenMapReady(map) { map.invalidate() }
return false
}
@@ -195,20 +199,39 @@ object Utility {
myLocationOverlay?.disableFollowLocation()
val boundingBox = BoundingBox.fromGeoPointsSafe(geoPointList)
- map.post {
+ // Ensure the map has been loaded before attempting to zoom.
+ runWhenMapReady(map) {
try {
- Timber.d("Zoom in to bounds -> $boundingBox")
+ Timber.d("Zoom in to bounds -> $boundingBox (w=${map.width}, h=${map.height})")
map.zoomToBoundingBox(boundingBox, true, 100, MAX_ZOOM_LEVEL, 1)
} catch (e: IllegalArgumentException) {
mapController.setCenter(boundingBox.centerWithDateLine)
mapController.setZoom(10.0)
Timber.e("Failed to zoom to bounding box! ${e.message}")
+ } finally {
+ map.invalidate()
}
}
return true
}
+ // Helper to run actions only after the MapView is loaded (has non-zero size)
+ private fun runWhenMapReady(map: MapView, action: () -> Unit) {
+ if (map.width > 0 && map.height > 0) {
+ map.post { action() }
+ return
+ }
+ map.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
+ override fun onGlobalLayout() {
+ if (map.width > 0 && map.height > 0) {
+ map.viewTreeObserver.removeOnGlobalLayoutListener(this)
+ map.post { action() }
+ }
+ }
+ })
+ }
+
fun fetchLocationListFromBeaconList(locations: List): List {
val uniqueLocations = locations
.distinctBy { it.locationId } // Filter out duplicates based on locationId
From 6545c72a152469cf8b262a56e6e5ebfbc86ca3d0 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sat, 20 Sep 2025 17:09:54 +0200
Subject: [PATCH 67/71] fix edge to edge problems with menus accessed from a
notification
---
.../at_tracking_detection/ui/MainActivity.kt | 60 ++++++++++++-------
.../ui/TrackingNotificationActivity.kt | 23 +++----
2 files changed, 45 insertions(+), 38 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/MainActivity.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/MainActivity.kt
index 490fd782..c7db1639 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/MainActivity.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/MainActivity.kt
@@ -4,9 +4,11 @@ import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.os.StrictMode
-import androidx.activity.enableEdgeToEdge
+import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.navigation.NavOptions
import androidx.navigation.findNavController
@@ -51,9 +53,9 @@ class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceCh
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
- enableEdgeToEdge()
+
+ configureSystemBars(this, edgeToEdge = true, applyRootPadding = false)
sharedPreferences.registerOnSharedPreferenceChangeListener(this)
- configureSystemBars()
val configuration = Configuration.getInstance()
configuration.load(this, PreferenceManager.getDefaultSharedPreferences(this))
@@ -111,25 +113,6 @@ class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceCh
}
}
- private fun configureSystemBars() {
- val isDarkTheme = Utility.isActualThemeDark(context = this)
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
- windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
-
- if (isDarkTheme) {
- windowInsetsController.isAppearanceLightStatusBars = false
- windowInsetsController.isAppearanceLightNavigationBars = false
- } else {
- windowInsetsController.isAppearanceLightStatusBars = true
- windowInsetsController.isAppearanceLightNavigationBars = true
- }
- } else {
- WindowCompat.setDecorFitsSystemWindows(window, false)
- }
- }
-
override fun onResume() {
super.onResume()
Timber.d("MainActivity onResume called")
@@ -186,6 +169,39 @@ class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceCh
companion object {
private val dateTime = LocalDateTime.now(ZoneOffset.UTC)
+
+ fun configureSystemBars(
+ activity: AppCompatActivity,
+ edgeToEdge: Boolean = true,
+ applyRootPadding: Boolean = true,
+ ) {
+ if (edgeToEdge) {
+ WindowCompat.setDecorFitsSystemWindows(activity.window, false)
+ } else {
+ WindowCompat.setDecorFitsSystemWindows(activity.window, true)
+ }
+
+ val isDarkTheme = Utility.isActualThemeDark(activity)
+ val controller = WindowCompat.getInsetsController(activity.window, activity.window.decorView)
+ controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+ controller.isAppearanceLightStatusBars = !isDarkTheme
+ controller.isAppearanceLightNavigationBars = !isDarkTheme
+
+ if (applyRootPadding) {
+ applySystemBarPadding(activity)
+ }
+ }
+
+ fun applySystemBarPadding(activity: AppCompatActivity) {
+ val content = activity.findViewById(android.R.id.content)
+ val root = content.getChildAt(0) ?: return
+ ViewCompat.setOnApplyWindowInsetsListener(root) { v, insets ->
+ val sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+ v.setPadding(sysBars.left, sysBars.top, sysBars.right, sysBars.bottom)
+ insets
+ }
+ ViewCompat.requestApplyInsets(root)
+ }
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt
index d3a6a344..a7aeeffe 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt
@@ -29,6 +29,10 @@ class TrackingNotificationActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tracking)
+
+ // For notification entry: disable edge-to-edge so content area automatically respects system bars
+ MainActivity.configureSystemBars(this, edgeToEdge = true, applyRootPadding = true)
+
val navHostFragment = supportFragmentManager.findFragmentById(R.id.tracking_host_fragment) as NavHostFragment
navController = navHostFragment.navController
@@ -41,17 +45,9 @@ class TrackingNotificationActivity : AppCompatActivity() {
})
}
- override fun onSupportNavigateUp(): Boolean {
- return if (navController.currentDestination?.id == R.id.trackingFragment) {
- finish()
- true
- } else {
- navController.navigateUp()
- }
- }
-
override fun onResume() {
super.onResume()
+ MainActivity.configureSystemBars(this, edgeToEdge = false, applyRootPadding = false)
val fragment = supportFragmentManager.findFragmentById(R.id.tracking_host_fragment)
if (fragment is NavHostFragment) {
val trackingFragment = fragment.childFragmentManager.primaryNavigationFragment
@@ -75,7 +71,6 @@ class TrackingNotificationActivity : AppCompatActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
-
navigateToTrackingFragment()
}
@@ -85,7 +80,6 @@ class TrackingNotificationActivity : AppCompatActivity() {
val notificationId = intent.getIntExtra("notificationId", -1)
Timber.d("Tracking Activity with device $deviceAddress and notification $notificationId started!")
- // Mark as clicked if we have a valid notification id
if (notificationId != -1) {
lifecycleScope.launch(Dispatchers.IO) {
notificationRepository.setClicked(notificationId, true)
@@ -96,9 +90,8 @@ class TrackingNotificationActivity : AppCompatActivity() {
Timber.e("Device address is needed! Going home...")
this.onSupportNavigateUp()
} else {
- // Workaround: Somehow not possible to use getString with deviceAddress as an Argument
var getTitle = getString(R.string.title_devices_tracking)
- getTitle = getTitle.replace("{deviceAddress}", deviceAddress.toString())
+ getTitle = getTitle.replace("{deviceAddress}", deviceAddress)
supportActionBar?.title = getTitle
val args = TrackingFragmentArgs(
@@ -107,9 +100,7 @@ class TrackingNotificationActivity : AppCompatActivity() {
notificationId = notificationId
).toBundle()
navController.setGraph(R.navigation.main_navigation)
- val navOptions = NavOptions.Builder()
- // .setPopUpTo(R.id.navigation_dashboard, true)
- .build()
+ val navOptions = NavOptions.Builder().build()
navController.navigate(R.id.trackingFragment, args, navOptions)
}
}
From f7b7b4ab517d76cff0689d07a2180e5c498b3c9e Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sun, 21 Sep 2025 12:58:17 +0200
Subject: [PATCH 68/71] fix old tracker deletion calculating with outdated
times
---
.../util/risk/RiskLevelEvaluator.kt | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/util/risk/RiskLevelEvaluator.kt b/app/src/main/java/de/seemoo/at_tracking_detection/util/risk/RiskLevelEvaluator.kt
index 8964f9a6..0493e619 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/util/risk/RiskLevelEvaluator.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/util/risk/RiskLevelEvaluator.kt
@@ -98,12 +98,18 @@ class RiskLevelEvaluator(
private const val NUMBER_OF_BEACONS_BEFORE_ALARM: Int = 3 // Number of total beacons before notification is created
private const val MAX_ACCURACY_FOR_LOCATIONS: Float = 100.0F // Minimum Location accuracy for high risk
const val MAX_NUMBER_MEDIUM_RISK: Long = 3 // Maximum number of devices with MEDIUM risk until the total risk level is set to high
- val relevantTrackingDateForRiskCalculation: LocalDateTime = LocalDateTime.now().minusDays(
- RELEVANT_DAYS_RISK_LEVEL) // Fallback Option, if possible use getRelevantTrackingDate() Function
- val deleteBeforeDate: LocalDateTime = LocalDateTime.now().minusDays(DELETE_SAFE_DEVICES_OLDER_THAN_DAYS)
- val deleteSafeGoogleTrackersBeforeDate: LocalDateTime = LocalDateTime.now().minusHours(DELETE_SAFE_GOOGLE_DEVICES_OLDER_THAN_HOURS)
- val matchNotConnectableGoogleTrackersBeforeDate: LocalDateTime = LocalDateTime.now().minusHours(MATCH_NOT_CONNECTABLE_GOOGLE_DEVICES_OLDER_THAN_HOURS)
- private val relevantNotificationDate: LocalDateTime = LocalDateTime.now().minusDays(RELEVANT_DAYS_NOTIFICATIONS)
+
+ // Dates used for queries. They will calculate a new date when accessed again
+ val relevantTrackingDateForRiskCalculation: LocalDateTime
+ get() = LocalDateTime.now().minusDays(RELEVANT_DAYS_RISK_LEVEL)
+ val deleteBeforeDate: LocalDateTime
+ get() = LocalDateTime.now().minusDays(DELETE_SAFE_DEVICES_OLDER_THAN_DAYS)
+ val deleteSafeGoogleTrackersBeforeDate: LocalDateTime
+ get() = LocalDateTime.now().minusHours(DELETE_SAFE_GOOGLE_DEVICES_OLDER_THAN_HOURS)
+ val matchNotConnectableGoogleTrackersBeforeDate: LocalDateTime
+ get() = LocalDateTime.now().minusHours(MATCH_NOT_CONNECTABLE_GOOGLE_DEVICES_OLDER_THAN_HOURS)
+ private val relevantNotificationDate: LocalDateTime
+ get() = LocalDateTime.now().minusDays(RELEVANT_DAYS_NOTIFICATIONS)
// Default Values: A single tracker gets tracked at least for x minutes until notification is created
private const val MINUTES_AT_LEAST_TRACKED_BEFORE_ALARM_HIGH: Long = 30
From 3a464e5682480de61fea873aee393bcf722d758c Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Sun, 21 Sep 2025 13:42:43 +0200
Subject: [PATCH 69/71] fix back navigation not working when application is
accessed through a notification
---
.../ui/TrackingNotificationActivity.kt | 70 ++++++++++++++-----
1 file changed, 53 insertions(+), 17 deletions(-)
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt
index a7aeeffe..232f0123 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/TrackingNotificationActivity.kt
@@ -36,11 +36,16 @@ class TrackingNotificationActivity : AppCompatActivity() {
val navHostFragment = supportFragmentManager.findFragmentById(R.id.tracking_host_fragment) as NavHostFragment
navController = navHostFragment.navController
- navigateToTrackingFragment()
+ // Only navigate on fresh creation or when a new intent with different device arrives
+ if (savedInstanceState == null) {
+ navigateToTrackingFragment(firstTime = true)
+ } else {
+ updateActionBarTitle()
+ }
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
- onSupportNavigateUp()
+ handleBackNavigation()
}
})
}
@@ -71,10 +76,10 @@ class TrackingNotificationActivity : AppCompatActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
- navigateToTrackingFragment()
+ navigateToTrackingFragment(replaceGraph = true, firstTime = true)
}
- private fun navigateToTrackingFragment() {
+ private fun navigateToTrackingFragment(replaceGraph: Boolean = false, firstTime: Boolean = false) {
val deviceAddress = intent.getStringExtra("deviceAddress")
val deviceTypeAsString = intent.getStringExtra("deviceTypeAsString") ?: "UNKNOWN"
val notificationId = intent.getIntExtra("notificationId", -1)
@@ -87,21 +92,52 @@ class TrackingNotificationActivity : AppCompatActivity() {
}
if (deviceAddress == null) {
- Timber.e("Device address is needed! Going home...")
- this.onSupportNavigateUp()
- } else {
- var getTitle = getString(R.string.title_devices_tracking)
- getTitle = getTitle.replace("{deviceAddress}", deviceAddress)
- supportActionBar?.title = getTitle
-
- val args = TrackingFragmentArgs(
- deviceAddress = deviceAddress,
- deviceTypeAsString = deviceTypeAsString,
- notificationId = notificationId
- ).toBundle()
+ Timber.e("Device was not provided! Finishing TrackingNotificationActivity.")
+ finish()
+ return
+ }
+
+ updateActionBarTitle(deviceAddress)
+ val args = TrackingFragmentArgs(deviceAddress, deviceTypeAsString, notificationId).toBundle()
+
+ if (replaceGraph || (firstTime && navController.currentDestination == null)) {
navController.setGraph(R.navigation.main_navigation)
- val navOptions = NavOptions.Builder().build()
+ // When accessing through Notification: trackingFragment is the root
+ val startDest = navController.graph.startDestinationId
+ val navOptions = NavOptions.Builder()
+ .setPopUpTo(startDest, inclusive = true)
+ .setLaunchSingleTop(true)
+ .build()
navController.navigate(R.id.trackingFragment, args, navOptions)
+ return
+ }
+
+ if (navController.currentDestination?.id != R.id.trackingFragment) {
+ navController.navigate(
+ R.id.trackingFragment,
+ args,
+ NavOptions.Builder().setLaunchSingleTop(true).build()
+ )
}
}
+
+ private fun updateActionBarTitle(deviceAddress: String? = intent.getStringExtra("deviceAddress")) {
+ deviceAddress ?: return
+ var getTitle = getString(R.string.title_devices_tracking)
+ getTitle = getTitle.replace("{deviceAddress}", deviceAddress)
+ supportActionBar?.title = getTitle
+ }
+
+ private fun handleBackNavigation() {
+ if (navController.currentDestination?.id != R.id.trackingFragment && navController.popBackStack()) {
+ return
+ }
+ // When at root --> finish
+ finish()
+ }
+
+ override fun onSupportNavigateUp(): Boolean {
+ handleBackNavigation()
+ return true
+ }
}
\ No newline at end of file
From 30a60c0ca8c843ac282fcefadf51441ba3b87745 Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 22 Sep 2025 11:33:11 +0200
Subject: [PATCH 70/71] update dependencies, increase Version number to 2.6.1
---
app/build.gradle | 30 +++++++++++++++---------------
build.gradle | 8 ++++----
2 files changed, 19 insertions(+), 19 deletions(-)
diff --git a/app/build.gradle b/app/build.gradle
index d2323c87..9f804f0a 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 54
- versionName "2.6.0"
+ versionCode 55
+ versionName "2.6.1"
buildConfigField "String", "API_KEY", apiProperties["API_KEY"]
buildConfigField "String", "API_BASE_ADDRESS", apiProperties["API_BASE_ADDRESS"]
@@ -92,10 +92,10 @@ 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.3'
- implementation 'androidx.navigation:navigation-ui-ktx:2.9.3'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.9.3'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.3'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.9.4'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.9.4'
+ 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'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.cardview:cardview:1.0.0'
@@ -103,12 +103,12 @@ dependencies {
implementation "androidx.profileinstaller:profileinstaller:1.4.1"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
- implementation "androidx.activity:activity-ktx:1.10.1"
+ implementation "androidx.activity:activity-ktx:1.11.0"
implementation 'com.squareup.retrofit2:retrofit:3.0.0'
implementation 'com.squareup.retrofit2:converter-gson:3.0.0'
implementation 'com.squareup.okhttp3:okhttp:5.1.0'
implementation 'com.squareup.okhttp3:logging-interceptor:5.1.0'
- implementation 'com.google.code.gson:gson:2.13.1'
+ implementation 'com.google.code.gson:gson:2.13.2'
implementation "androidx.work:work-testing:$work_version"
implementation 'androidx.core:core-ktx:1.17.0'
debugImplementation 'com.squareup.okhttp3:logging-interceptor:5.1.0'
@@ -128,7 +128,7 @@ dependencies {
implementation 'com.github.mukeshsolanki:MarkdownView-Android:2.0.0'
- implementation 'com.github.bumptech.glide:glide:5.0.4'
+ implementation 'com.github.bumptech.glide:glide:5.0.5'
ksp "com.google.dagger:hilt-compiler:$hilt_compiler_version"
ksp "androidx.hilt:hilt-compiler:$hilt_version"
@@ -157,25 +157,25 @@ dependencies {
//Compose
// Integration with activities
- implementation 'androidx.activity:activity-compose:1.10.1'
+ 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'
// Animations
- implementation 'androidx.compose.animation:animation:1.9.0'
+ implementation 'androidx.compose.animation:animation:1.9.1'
// Tooling support (Previews, etc.)
- implementation 'androidx.compose.ui:ui-tooling:1.9.0'
+ implementation 'androidx.compose.ui:ui-tooling:1.9.1'
// Integration with ViewModels
- implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.3'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4'
// UI Tests
- androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.9.0'
+ androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.9.1'
// 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.7.2")
+ implementation("com.google.android.gms:play-services-base:18.8.0")
}
ksp {
diff --git a/build.gradle b/build.gradle
index 0144e1bb..d891b82a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,11 +2,11 @@
buildscript {
ext.kotlin_version = '2.2.10'
ext.hilt_compiler_version = '2.57.1'
- ext.hilt_version = '1.2.0'
- ext.room_version = '2.7.2'
+ ext.hilt_version = '1.3.0'
+ ext.room_version = '2.8.0'
ext.compose_version = '1.7.3'
ext.about_libraries_version = '12.2.4'
- ext.work_version = '2.10.3'
+ ext.work_version = '2.10.4'
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.3"
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.9.4"
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"
From 2a055258708b5f8a4514675e2ccbd76407de734c Mon Sep 17 00:00:00 2001
From: Dennis Arndt
Date: Mon, 29 Sep 2025 12:04:07 +0200
Subject: [PATCH 71/71] F-Droid Release 2.6.1
---
api.properties | 2 +-
.../metadata/android/de-DE/changelogs/55.txt | 3 ++
.../android/de-DE/full_description.txt | 38 +++++++++----------
.../android/de-DE/short_description.txt | 2 +-
.../metadata/android/en-US/changelogs/55.txt | 3 ++
.../android/en-US/full_description.txt | 33 ++++++++++++----
.../android/en-US/short_description.txt | 2 +-
7 files changed, 53 insertions(+), 30 deletions(-)
create mode 100644 fastlane/metadata/android/de-DE/changelogs/55.txt
create mode 100644 fastlane/metadata/android/en-US/changelogs/55.txt
diff --git a/api.properties b/api.properties
index cf3f4b34..bb91fe29 100644
--- a/api.properties
+++ b/api.properties
@@ -1,2 +1,2 @@
-API_KEY="0pGhFp5V.3Me6ZeM4hqFnu8FDMrqZwpaau1fdE98Y"
+API_KEY="3acPl2TP.lAyeGkWWlqPrgfWI9WbzqKKHejOmahJ3"
API_BASE_ADDRESS="https://tpe.seemoo.tu-darmstadt.de/api/"
\ No newline at end of file
diff --git a/fastlane/metadata/android/de-DE/changelogs/55.txt b/fastlane/metadata/android/de-DE/changelogs/55.txt
new file mode 100644
index 00000000..0c8e9262
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/55.txt
@@ -0,0 +1,3 @@
+VERBESSERT: Die App funktioniert mit weniger Berechtigungen (Funktionsumfang abhängig von den gewährten Berechtigungen).
+VERBESSERT: Bessere Kompatibilität bei ungewöhnlichen Bildschirmformaten.
+VERBESSERT: Fehlerbehebungen
\ No newline at end of file
diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt
index ea464aae..506c41cb 100644
--- a/fastlane/metadata/android/de-DE/full_description.txt
+++ b/fastlane/metadata/android/de-DE/full_description.txt
@@ -1,30 +1,30 @@
-With AirGuard, you get the anti-stalking protection you deserve!
-The app scans your surroundings in the background to detect trackers like AirTags, Samsung SmartTags, or Google Find My Device trackers. If a tracker is following you, you will receive an instant notification.
+Mit AirGuard bekommst du den Anti-Stalking-Schutz, den du verdienst!
+Die App scannt im Hintergrund deine Umgebung, um Tracker wie AirTags, Samsung SmartTags oder Google Find My Device-Tracker zu erkennen. Wenn ein Tracker dir folgt, erhältst du sofort eine Benachrichtigung.
-These trackers are often no larger than a coin and are unfortunately misused to secretly track people. Since each tracker works differently, you would typically need multiple apps to detect unwanted tracking.
-AirGuard combines the detection of various trackers into a single app – keeping you protected with ease.
+Diese Tracker sind oft nicht größer als eine Münze und werden leider missbraucht, um Menschen heimlich zu verfolgen. Da jeder Tracker anders funktioniert, bräuchtest du normalerweise mehrere Apps, um unerwünschtes Tracking zu erkennen.
+AirGuard vereint die Erkennung verschiedener Tracker in einer einzigen App – so bleibst du einfach geschützt.
-Once a tracker is detected, you can make it play a sound (for supported models) or perform a manual scan to locate it. If you find a tracker, we recommend disabling it to prevent further tracking of your location.
+Sobald ein Tracker entdeckt wurde, kannst du ihn einen Ton abspielen lassen (für unterstützte Modelle) oder einen manuellen Scan durchführen, um ihn zu lokalisieren. Wenn du einen Tracker findest, empfehlen wir, ihn zu deaktivieren, damit er deinen Standort nicht weiter verfolgen kann.
-The app stores location data exclusively on your device, allowing you to review where a tracker has followed you. Your personal data is never shared.
+Die App speichert Standortdaten ausschließlich auf deinem Gerät, sodass du später nachvollziehen kannst, wo dich ein Tracker verfolgt hat. Deine persönlichen Daten werden niemals weitergegeben.
-If no trackers are found, the app runs silently in the background and won’t bother you.
+Wenn keine Tracker gefunden werden, läuft die App im Hintergrund und stört dich nicht.
-
How does the app work?
-AirGuard uses Bluetooth to detect AirTags, Samsung SmartTags, and other trackers. All data is processed and stored locally on your device.
-If a tracker is detected in at least three different locations, you will receive a warning. You can adjust the security level in the settings to receive even faster alerts.
+
Wie funktioniert die App?
+AirGuard verwendet Bluetooth, um AirTags, Samsung SmartTags und andere Tracker zu erkennen. Alle Daten werden lokal auf deinem Gerät verarbeitet und gespeichert.
+Wenn ein Tracker an mindestens drei verschiedenen Orten festgestellt wird, erhältst du eine Warnung. Du kannst das Sicherheitsniveau in den Einstellungen anpassen, um noch schneller benachrichtigt zu werden.
-
Who are we?
-We are part of the Technical University of Darmstadt. This project is part of the scientific research conducted by the Secure Mobile Networking Lab.
-Our goal is to protect people’s privacy and investigate how widespread the issue of tracker-based stalking is.
+
Wer sind wir?
+Wir sind Teil der Technischen Universität Darmstadt. Dieses Projekt ist Teil der wissenschaftlichen Forschung des Secure Mobile Networking Lab.
+Unser Ziel ist es, die Privatsphäre von Menschen zu schützen und zu untersuchen, wie weit verbreitet das Problem des tracker-basierten Stalkings ist.
-You can voluntarily participate in an anonymous study to help us gain more insights into the usage and spread of these trackers.
+Du kannst freiwillig an einer anonymen Studie teilnehmen, um uns mehr Einblicke in die Nutzung und Verbreitung dieser Tracker zu geben.
-This app will never be monetized – there are no ads and no paid features. You will never be charged for using it.
+Diese App wird niemals monetarisiert – es gibt keine Werbung und keine kostenpflichtigen Funktionen. Für die Nutzung fallen keine Kosten an.
-Our privacy policy can be found here:
+Unsere Datenschutzerklärung findest du hier:
https://tpe.seemoo.tu-darmstadt.de/privacy-policy.html
-
Legal Notice
-AirTag, Find My, and iOS are registered trademarks of Apple Inc.
-This project is not affiliated with Apple Inc.
\ No newline at end of file
+
Rechtlicher Hinweis
+AirTag, Find My und iOS sind eingetragene Marken der Apple Inc.
+Dieses Projekt ist nicht mit Apple Inc. verbunden
diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt
index 94a42307..cbd0fb5f 100644
--- a/fastlane/metadata/android/de-DE/short_description.txt
+++ b/fastlane/metadata/android/de-DE/short_description.txt
@@ -1 +1 @@
-AirGuard erkennt Tracker, die zum stalking eingesetzt werden und warnt dich
\ No newline at end of file
+AirGuard erkennt Tracker, die zum Stalking eingesetzt werden und warnt dich.
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/55.txt b/fastlane/metadata/android/en-US/changelogs/55.txt
new file mode 100644
index 00000000..a558e8fe
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/55.txt
@@ -0,0 +1,3 @@
+IMPROVED: App works with less permissions (functionality limited depending on given permissions)
+IMPROVED: better compatibility on unusual Screen Formats
+IMPROVED: Bugfixes
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index 6c3a3292..6e8da807 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -1,13 +1,30 @@
-With AirGuard you get the anti-stalking protection you deserve!
-The app scans your surroundings in the background to find trackers such as AirTags, Samsung SmartTags or Google Find My Device trackers. If the app finds out that a tracker is following you, you will receive a notification.
+With AirGuard, you get the anti-stalking protection you deserve!
+The app scans your surroundings in the background to detect trackers like AirTags, Samsung SmartTags, or Google Find My Device trackers. If a tracker is following you, you will receive an instant notification.
-These trackers are only the size of a coin and are often used to track people. Unfortunately, each tracker works differently, so you often need multiple apps to detect tracking.
-AirGuard combines the detection of different trackers so you only need one app to keep you safe.
+These trackers are often no larger than a coin and are unfortunately misused to secretly track people. Since each tracker works differently, you would typically need multiple apps to detect unwanted tracking.
+AirGuard combines the detection of various trackers into a single app – keeping you protected with ease.
-Once a tracker has been found, you can make it ring (doesn't work on all models) or go searching with a manual scan.
-If you find a tracker, we advise you to deactivate it so that it no longer tracks your location.
+Once a tracker is detected, you can make it play a sound (for supported models) or perform a manual scan to locate it. If you find a tracker, we recommend disabling it to prevent further tracking of your location.
-The app only saves your location locally to show you later where a tracker has followed you. This app never shares personal data.
+The app stores location data exclusively on your device, allowing you to review where a tracker has followed you. Your personal data is never shared.
-If the app does not find any trackers, then the app will not bother you.
+If no trackers are found, the app runs silently in the background and won’t bother you.
+
How does the app work?
+AirGuard uses Bluetooth to detect AirTags, Samsung SmartTags, and other trackers. All data is processed and stored locally on your device.
+If a tracker is detected in at least three different locations, you will receive a warning. You can adjust the security level in the settings to receive even faster alerts.
+
+
Who are we?
+We are part of the Technical University of Darmstadt. This project is part of the scientific research conducted by the Secure Mobile Networking Lab.
+Our goal is to protect people’s privacy and investigate how widespread the issue of tracker-based stalking is.
+
+You can voluntarily participate in an anonymous study to help us gain more insights into the usage and spread of these trackers.
+
+This app will never be monetized – there are no ads and no paid features. You will never be charged for using it.
+
+Our privacy policy can be found here:
+https://tpe.seemoo.tu-darmstadt.de/privacy-policy.html
+
+
Legal Notice
+AirTag, Find My, and iOS are registered trademarks of Apple Inc.
+This project is not affiliated with Apple Inc.
diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt
index 060ec166..65124113 100644
--- a/fastlane/metadata/android/en-US/short_description.txt
+++ b/fastlane/metadata/android/en-US/short_description.txt
@@ -1 +1 @@
-Protect yourself from Apple's Find My Tracking
\ No newline at end of file
+AirGuard dectects trackers that are used for stalking and warns you.
\ No newline at end of file