diff --git a/app/build.gradle b/app/build.gradle
index d9bf360..dd88b8a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -99,11 +99,11 @@ dependencies {
implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.cardview:cardview:1.0.0'
- implementation 'androidx.recyclerview:recyclerview:1.3.1'
+ implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
@@ -126,7 +126,9 @@ dependencies {
implementation "com.mikepenz:aboutlibraries:$about_libraries_version"
- implementation 'io.noties.markwon:core:4.6.2'
+ implementation 'com.github.mukeshsolanki:MarkdownView-Android:2.0.0'
+
+ implementation 'com.github.bumptech.glide:glide:4.16.0'
kapt "com.google.dagger:hilt-compiler:$hilt_version"
@@ -146,7 +148,7 @@ dependencies {
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
//Finds memory leaks while running the app in Debug mode
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
@@ -161,7 +163,7 @@ dependencies {
// Tooling support (Previews, etc.)
implementation 'androidx.compose.ui:ui-tooling:1.4.3'
// Integration with ViewModels
- implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'
// UI Tests
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.4.3'
// When using a MDC theme
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0ea01a8..505363c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -20,6 +20,7 @@
+
@@ -29,24 +30,25 @@
-
+ tools:targetApi="tiramisu"
+ android:dataExtractionRules="@xml/data_extraction_rules"
+ android:fullBackupContent="false">
+ android:parentActivityName=".ui.MainActivity"
+ android:exported="false" />
-
+
-
+
-
+
+ android:enabled="true"
+ android:exported="false"/>
()
- open fun getLastLocation(checkRequirements: Boolean = true): Location? {
- if (ContextCompat.checkSelfPermission(ATTrackingDetectionApplication.getAppContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ fun getLastLocation(checkRequirements: Boolean = true): Location? {
+ if (ContextCompat.checkSelfPermission(
+ ATTrackingDetectionApplication.getAppContext(),
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED &&
+ ContextCompat.checkSelfPermission(
+ ATTrackingDetectionApplication.getAppContext(),
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
return null
}
@@ -33,12 +40,19 @@ open class LocationProvider @Inject constructor(
}
/**
- * Fetches the most recent location from network and gps and returns the one that has been recveived more recently
+ * Fetches the most recent location from network and gps and returns the one that has been received more recently
* @return the most recent location across multiple providers
*/
- @SuppressLint("InlinedApi") // Suppressed, because we use a custom version provider which is injectable for testing
private fun getLastLocationFromAnyProvider(checkRequirements: Boolean): Location? {
- if (ContextCompat.checkSelfPermission(ATTrackingDetectionApplication.getAppContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ if (ContextCompat.checkSelfPermission(
+ ATTrackingDetectionApplication.getAppContext(),
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED &&
+ ContextCompat.checkSelfPermission(
+ ATTrackingDetectionApplication.getAppContext(),
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
return null
}
@@ -61,53 +75,46 @@ open class LocationProvider @Inject constructor(
}
private fun legacyGetLastLocationFromAnyProvider(checkRequirements: Boolean): Location? {
- if (ContextCompat.checkSelfPermission(ATTrackingDetectionApplication.getAppContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ // Check for location permission
+ if (ContextCompat.checkSelfPermission(
+ ATTrackingDetectionApplication.getAppContext(),
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED &&
+ ContextCompat.checkSelfPermission(
+ ATTrackingDetectionApplication.getAppContext(),
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
return null
}
- // On older versions we use both providers to get the best location signal
+ // Get the last known locations from both providers
val networkLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
+ val gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
- if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
- val gpsLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
-
- if (gpsLocation != null && networkLocation != null) {
- // Got to past locations, lets check which passes our requirements
- val gpsRequirements = locationMatchesMinimumRequirements(gpsLocation)
- val networkRequirements = locationMatchesMinimumRequirements(networkLocation)
- if (gpsRequirements && networkRequirements) {
- // Check which one is more current
- if (gpsLocation.time > networkLocation.time) {
- return gpsLocation
- }else {
- return networkLocation
- }
- }else if (gpsRequirements) {
- // Only GPS satisfies the requirements. Return it
- return gpsLocation
- }else if (networkRequirements) {
- // Only network satisfies. Return it
- return networkLocation
- }else if (!checkRequirements) {
- if (gpsLocation.time > networkLocation.time) {
- return gpsLocation
- }
- return networkLocation
- }
- }else if (gpsLocation != null && locationMatchesMinimumRequirements(gpsLocation)) {
- // Only gps satisfies and network does not exist
- return gpsLocation
+ // If both locations are available, return the one that is more current and meets the minimum requirements
+ if (networkLocation != null && gpsLocation != null) {
+ val bestLocation = if (gpsLocation.time > networkLocation.time) gpsLocation else networkLocation
+ if (locationMatchesMinimumRequirements(bestLocation)) {
+ return bestLocation
}
}
+ // If only one location is available, return it if it meets the minimum requirements
if (networkLocation != null && locationMatchesMinimumRequirements(networkLocation)) {
return networkLocation
- }else if (!checkRequirements) {
- return networkLocation
+ }
+ if (gpsLocation != null && locationMatchesMinimumRequirements(gpsLocation)) {
+ return gpsLocation
}
- Timber.d("No last know location matched the requirements")
- return null
+ // If neither location meets the minimum requirements, return null
+ if (checkRequirements) {
+ return null
+ }
+
+ // If no location requirements are specified, return the last known location from either provider, or null if none are available
+ return networkLocation ?: gpsLocation
}
private fun getSecondsSinceLocation(location: Location): Long {
@@ -130,27 +137,38 @@ open class LocationProvider @Inject constructor(
* @param timeoutMillis: After the timeout the last location will be returned no matter if it matches the requirements or not
* @return the last known location if this already satisfies our requirements
*/
- @SuppressLint("InlinedApi") // Suppressed, because we use a custom version provider which is injectable for testing
- open fun lastKnownOrRequestLocationUpdates(locationRequester: LocationRequester, timeoutMillis: Long?): Location? {
- if (ContextCompat.checkSelfPermission(ATTrackingDetectionApplication.getAppContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ open fun lastKnownOrRequestLocationUpdates(
+ locationRequester: LocationRequester,
+ timeoutMillis: Long? = null
+ ): Location? {
+ // Check for location permission
+ if (ContextCompat.checkSelfPermission(
+ ATTrackingDetectionApplication.getAppContext(),
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED) {
return null
}
+ // Get the last known location
val lastLocation = getLastLocation()
+
+ // If the last location is available and meets the minimum requirements, return it
if (lastLocation != null && locationMatchesMinimumRequirements(lastLocation)) {
return lastLocation
}
+ // Add the location requester to the list of active requesters
this.locationRequesters.add(locationRequester)
- // The fused location provider does not work reliably with Samsung + Android 12
- // We just stay with the legacy location, because this just works
+ // Request location updates from all enabled providers
requestLocationUpdatesFromAnyProvider()
+ // If a timeout is specified, set a timeout for the location update
if (timeoutMillis != null) {
- setTimeoutForLocationUpdate(requester = locationRequester, timeoutMillis= timeoutMillis)
+ setTimeoutForLocationUpdate(requester = locationRequester, timeoutMillis = timeoutMillis)
}
+ // Return null, since we don't have a location immediately available
return null
}
@@ -162,45 +180,58 @@ open class LocationProvider @Inject constructor(
* @param timeoutMillis milliseconds after which the timeout will be executed
*/
private fun setTimeoutForLocationUpdate(requester: LocationRequester, timeoutMillis: Long) {
- val handler = Handler(Looper.getMainLooper())
-
- val runnable = kotlinx.coroutines.Runnable {
- if (this@LocationProvider.locationRequesters.size == 0) {
- // The location was already returned
+ // Create a runnable to handle the timeout
+ val runnable = Runnable {
+ // If the location requester list is empty, the location has already been returned
+ if (this@LocationProvider.locationRequesters.isEmpty()) {
return@Runnable
}
+ // Log the timeout and get the last known location, regardless of whether it meets the requirements
Timber.d("Location request timed out")
val lastLocation = this@LocationProvider.getLastLocation(checkRequirements = false)
+
+ // If the last location is available, notify the requester
lastLocation?.let {
- requester.receivedAccurateLocationUpdate(location = lastLocation)
+ requester.receivedAccurateLocationUpdate(location = it)
}
+
+ // If there is only one requester left, stop location updates and clear the list
if (this@LocationProvider.locationRequesters.size == 1) {
this@LocationProvider.stopLocationUpdates()
this@LocationProvider.locationRequesters.clear()
- }else {
+ } else {
+ // Otherwise, remove the requester from the list
this@LocationProvider.locationRequesters.remove(requester)
}
}
+ // Schedule the runnable to be executed after the timeout period
+ val handler = Handler(Looper.getMainLooper())
handler.postDelayed(runnable, timeoutMillis)
+
+ // Log the timeout settings
Timber.d("Location request timeout set to $timeoutMillis")
}
-
private fun requestLocationUpdatesFromAnyProvider() {
- if (ContextCompat.checkSelfPermission(ATTrackingDetectionApplication.getAppContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+ // Check for location permission
+ if (ContextCompat.checkSelfPermission(
+ ATTrackingDetectionApplication.getAppContext(),
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
return
}
- Timber.d("Requesting location updates")
- val gpsProviderEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
- val networkProviderEnabled =
- locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
+ // Get the list of enabled location providers
+ val enabledProviders = locationManager.allProviders
+ .filter { locationManager.isProviderEnabled(it) }
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && locationManager.isProviderEnabled(LocationManager.FUSED_PROVIDER)) {
+ // Request location updates from all enabled providers
+ enabledProviders.forEach {
locationManager.requestLocationUpdates(
- LocationManager.FUSED_PROVIDER,
+ it,
MIN_UPDATE_TIME_MS,
MIN_DISTANCE_METER,
this,
@@ -208,43 +239,14 @@ open class LocationProvider @Inject constructor(
)
}
- if (networkProviderEnabled) {
- locationManager.requestLocationUpdates(
- LocationManager.NETWORK_PROVIDER,
- MIN_UPDATE_TIME_MS,
- MIN_DISTANCE_METER,
- this,
- handler.looper
- )
- }
-
- if (gpsProviderEnabled) {
- // Using GPS and Network provider, because the GPS provider does notwork indoors (it will never call the callback)
- locationManager.requestLocationUpdates(
- LocationManager.GPS_PROVIDER,
- MIN_UPDATE_TIME_MS,
- MIN_DISTANCE_METER,
- this,
- handler.looper
- )
- }
-
- if (!networkProviderEnabled && !gpsProviderEnabled) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- if (!locationManager.isProviderEnabled(LocationManager.FUSED_PROVIDER)) {
- // Error
- Timber.e("ERROR: No location provider available")
- stopLocationUpdates()
- }
- }else {
- //Error
- Timber.e("ERROR: No location provider available")
- stopLocationUpdates()
- }
+ // If no location providers are enabled, log an error and stop location updates
+ if (enabledProviders.isEmpty()) {
+ Timber.e("ERROR: No location provider available")
+ stopLocationUpdates()
}
}
- fun stopLocationUpdates() {
+ private fun stopLocationUpdates() {
locationManager.removeUpdates(this)
}
@@ -274,9 +276,6 @@ open class LocationProvider @Inject constructor(
}
}
- @Deprecated("Deprecated in Java")
- override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
-
// Android Phones with SDK < 30 need these methods
override fun onProviderEnabled(provider: String) {}
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 e037938..7871459 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationBuilder.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationBuilder.kt
@@ -90,11 +90,7 @@ class NotificationBuilder @Inject constructor(
context,
code,
intent,
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.FLAG_IMMUTABLE
- } else {
- PendingIntent.FLAG_UPDATE_CURRENT
- }
+ PendingIntent.FLAG_IMMUTABLE
)
}
@@ -270,6 +266,20 @@ class NotificationBuilder @Inject constructor(
}
+ fun buildObserveTrackerFailedNotification(notificationId: Int): Notification {
+ val bundle: Bundle = Bundle().apply { putInt("notificationId", notificationId) }
+
+ return NotificationCompat.Builder(context, NotificationConstants.CHANNEL_ID)
+ .setContentTitle(context.getString(R.string.notification_observe_tracker_title_base))
+ .setContentText(context.getString(R.string.notification_observe_tracker_error))
+ .setPriority(getNotificationPriority())
+ .setContentIntent(pendingNotificationIntent(bundle, notificationId))
+ .setCategory(Notification.CATEGORY_ERROR)
+ .setSmallIcon(R.drawable.ic_scan_icon)
+ .setAutoCancel(true)
+ .build()
+ }
+
fun buildBluetoothErrorNotification(): Notification {
val notificationId = -100
val bundle: Bundle = Bundle().apply { putInt("notificationId", notificationId) }
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationService.kt b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationService.kt
index c34ebbe..d9749dc 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationService.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/notifications/NotificationService.kt
@@ -70,6 +70,20 @@ class NotificationService @Inject constructor(
}
}
+ @SuppressLint("MissingPermission")
+ fun sendObserveTrackerFailedNotification() {
+ val notificationId = generateNotificationId()
+ with(notificationManagerCompat) {
+ if (this.areNotificationsEnabled()) {
+ notify(
+ OBSERVE_TRACKER_NOTIFICATION_TAG,
+ notificationId,
+ notificationBuilder.buildObserveTrackerFailedNotification(notificationId)
+ )
+ }
+ }
+ }
+
/*
@SuppressLint("MissingPermission")
suspend fun sendObserveTrackerNotification(baseDevice: BaseDevice) {
@@ -162,28 +176,26 @@ class NotificationService @Inject constructor(
}
fun setup() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- Timber.d("Setting up NotificationManager")
- // Register the channel with the system
- val channel = NotificationChannelCompat.Builder(
- NotificationConstants.CHANNEL_ID,
- NotificationManagerCompat.IMPORTANCE_HIGH
- )
- .setName(NotificationConstants.NOTIFICATION_CHANNEL_NAME)
- .build()
-
- notificationManagerCompat.createNotificationChannel(channel)
-
- //Register the info channel
- val infoChannel = NotificationChannelCompat.Builder(
- NotificationConstants.INFO_CHANNEL_ID,
- NotificationManagerCompat.IMPORTANCE_LOW
- )
- .setName(NotificationConstants.NOTIFICATION_CHANNEL_INFO)
- .build()
-
- notificationManagerCompat.createNotificationChannel(infoChannel)
- }
+ Timber.d("Setting up NotificationManager")
+ // Register the channel with the system
+ val channel = NotificationChannelCompat.Builder(
+ NotificationConstants.CHANNEL_ID,
+ NotificationManagerCompat.IMPORTANCE_HIGH
+ )
+ .setName(NotificationConstants.NOTIFICATION_CHANNEL_NAME)
+ .build()
+
+ notificationManagerCompat.createNotificationChannel(channel)
+
+ //Register the info channel
+ val infoChannel = NotificationChannelCompat.Builder(
+ NotificationConstants.INFO_CHANNEL_ID,
+ NotificationManagerCompat.IMPORTANCE_LOW
+ )
+ .setName(NotificationConstants.NOTIFICATION_CHANNEL_INFO)
+ .build()
+
+ notificationManagerCompat.createNotificationChannel(infoChannel)
}
companion object {
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/statistics/api/Api.kt b/app/src/main/java/de/seemoo/at_tracking_detection/statistics/api/Api.kt
index 27afbb2..0e69c66 100644
--- a/app/src/main/java/de/seemoo/at_tracking_detection/statistics/api/Api.kt
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/statistics/api/Api.kt
@@ -28,6 +28,17 @@ interface Api {
@Header("User-Agent") userAgent: String = USER_AGENT
): Response
+ /**
+ * Deletes the study data related to the given token.
+ */
+ @Headers("Authorization: Api-Key $API_KEY")
+ @DELETE("delete_study_data")
+ suspend fun deleteStudyData(
+ @Header("token") token: String,
+ @Header("X-Timezone") timezone: String = TIME_ZONE,
+ @Header("User-Agent") userAgent: String = USER_AGENT
+ ): Response
+
@GET("ping")
suspend fun ping(
@Header("X-Timezone") timezone: String = TIME_ZONE,
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/MarkdownViewerActivity.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/MarkdownViewerActivity.kt
deleted file mode 100644
index 244dd9d..0000000
--- a/app/src/main/java/de/seemoo/at_tracking_detection/ui/MarkdownViewerActivity.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package de.seemoo.at_tracking_detection.ui
-
-import android.os.Bundle
-import android.widget.TextView
-import androidx.appcompat.app.AppCompatActivity
-import de.seemoo.at_tracking_detection.R
-import io.noties.markwon.Markwon
-
-class MarkdownViewerActivity : AppCompatActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_markdown_viewer)
-
- val markdown = """
- # Hello Markdown
-
- This is a sample Markdown file rendered using Markwon library in Kotlin.
-
- - List item 1
- - List item 2
- - List item 3
-
- **Bold Text**
-
- *Italic Text*
-
- ![Image](https://example.com/image.jpg)
-
- `Inline Code`
-
- ```kotlin
- fun main() {
- println("Hello, Markdown!")
- }
- ```
- """.trimIndent()
-
- val markwon = Markwon.builder(this)
- .build()
-
- val markdownTextView = findViewById(R.id.markdownTextView)
- markwon.setMarkdown(markdownTextView, markdown)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/OnboardingActivity.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/OnboardingActivity.kt
index 31af1aa..c352ed1 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
@@ -218,9 +218,7 @@ class OnboardingActivity : AppIntro() {
notificationSlide(slideNumber + 3)
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- addSlide(IgnoreBatteryOptimizationFragment.newInstance())
- }
+ addSlide(IgnoreBatteryOptimizationFragment.newInstance())
addSlide(ShareDataFragment.newInstance())
diff --git a/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/Article.kt b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/Article.kt
new file mode 100644
index 0000000..848aa18
--- /dev/null
+++ b/app/src/main/java/de/seemoo/at_tracking_detection/ui/dashboard/Article.kt
@@ -0,0 +1,96 @@
+package de.seemoo.at_tracking_detection.ui.dashboard
+
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import de.seemoo.at_tracking_detection.ATTrackingDetectionApplication
+import de.seemoo.at_tracking_detection.R
+import timber.log.Timber
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.net.HttpURLConnection
+import java.net.URL
+
+data class Article(
+ val title: String,
+ val author: String,
+ val readingTime: Int,
+ val previewText: String,
+ val cardColor: String,
+ val preview_image: String, // TODO: Rename when in production to PreviewImage, also in JSON
+ val filename: String
+)
+
+fun parseArticles(jsonString: String): List {
+ val gson = Gson()
+ val listType = object : TypeToken