From 4c21f208b24bc31d9695623e1271a6ead36585e9 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Fri, 25 Oct 2024 22:36:58 +0530 Subject: [PATCH 1/8] Set up versions --- build.gradle | 2 +- gradle.properties | 2 +- gradle/android.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 861627e92..796180a8c 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ allprojects { version = "$project_version" group = 'org.radarbase' - ext.versionCode = 52 + ext.versionCode = 54 } subprojects { diff --git a/gradle.properties b/gradle.properties index c4173c197..45f93edfe 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,7 +24,7 @@ android.defaults.buildfeatures.buildconfig=true # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -project_version=1.2.5 +project_version=1.4.1-SNAPSHOT java_version=17 kotlin_version=1.9.23 diff --git a/gradle/android.gradle b/gradle/android.gradle index 9a8ec4cc9..abec6a31a 100644 --- a/gradle/android.gradle +++ b/gradle/android.gradle @@ -4,7 +4,7 @@ android { defaultConfig { minSdkVersion 26 - targetSdkVersion 33 + targetSdkVersion 34 versionCode versionCode versionName version } From e4cf6eaa9527563b9431862e8ffd4b50de10b262 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Fri, 25 Oct 2024 22:38:37 +0530 Subject: [PATCH 2/8] Updated startForeground to include flags on higher API levels --- .../org/radarbase/android/RadarService.kt | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt b/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt index 4eac4080c..11d5e3b44 100644 --- a/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt +++ b/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt @@ -20,14 +20,25 @@ import android.Manifest.permission.* import android.app.Notification import android.app.PendingIntent import android.content.ComponentName -import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE import android.os.* +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES +import android.os.Build.VERSION_CODES.Q +import android.os.Build.VERSION_CODES.S +import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE import android.os.Process.THREAD_PRIORITY_BACKGROUND import android.widget.Toast import androidx.annotation.CallSuper +import androidx.annotation.RequiresApi import androidx.lifecycle.LifecycleService import androidx.localbroadcastmanager.content.LocalBroadcastManager import org.apache.avro.specific.SpecificRecord @@ -101,10 +112,10 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis protected open val servicePermissions: List = buildList(4) { add(ACCESS_NETWORK_STATE) add(INTERNET) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + if (SDK_INT >= VERSION_CODES.P) { add(FOREGROUND_SERVICE) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (SDK_INT >= VERSION_CODES.TIRAMISU) { add(POST_NOTIFICATIONS) } } @@ -208,11 +219,28 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis super.onStartCommand(intent, flags, startId) configure(configuration.latestConfig) checkPermissions() - startForeground(1, createForegroundNotification()) + + if (SDK_INT < UPSIDE_DOWN_CAKE) { + // Below API 34: Start foreground without service types + startForeground(1, createForegroundNotification()) + } else { + + /** + * API 34+ (Android 14+): Adding HEALTH and SPECIAL_USE types + * Currently this is not explicitly checking for android 14+ version. + * This need to be modified it in future when setting new targetSdkVersion + */ + startForeground(1, createForegroundNotification(), + FOREGROUND_SERVICE_TYPE_SPECIAL_USE or + FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } return START_STICKY } + + protected open fun createForegroundNotification(): Notification { val mainIntent = Intent(this, radarApp.mainActivity) return notificationHandler.create( @@ -319,6 +347,7 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis if (grantedPermissions.isNotEmpty()) { mHandler.execute { logger.info("Granted permissions {}", grantedPermissions) +// startForegroundIfNeeded(grantedPermissions) // Permission granted. needsPermissions -= grantedPermissions startScanning() @@ -683,7 +712,7 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis private const val BLUETOOTH_NOTIFICATION = 521290 - val ACCESS_BACKGROUND_LOCATION_COMPAT = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + val ACCESS_BACKGROUND_LOCATION_COMPAT = if (SDK_INT >= VERSION_CODES.Q) ACCESS_BACKGROUND_LOCATION else "android.permission.ACCESS_BACKGROUND_LOCATION" private const val BACKGROUND_REQUEST_CODE = 9559 From 32e1642c6baf33a48f5ed22a18cc497392179b10 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Fri, 25 Oct 2024 22:40:39 +0530 Subject: [PATCH 3/8] RadarService will call startForeground when fgs types permissions are accepted --- .../org/radarbase/android/RadarService.kt | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt b/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt index 11d5e3b44..b28fa5789 100644 --- a/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt +++ b/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt @@ -130,6 +130,17 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis private var bluetoothNotification: NotificationHandler.NotificationRegistration? = null + @RequiresApi(Q) + val fgsHealthPermissions: Set = setOf(BODY_SENSORS, ACTIVITY_RECOGNITION) + @RequiresApi(S) + val fgsConnectDevicePermissions: Set = + setOf(BLUETOOTH_CONNECT, BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE, UWB_RANGING) + private val fgsLocationPermissions: Set = + setOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION) + private val fgsMicrophonePermissions: Set = + setOf(RECORD_AUDIO) + + /** Defines callbacks for service binding, passed to bindService() */ private lateinit var bluetoothReceiver: BluetoothStateReceiver @@ -239,7 +250,48 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis return START_STICKY } + private fun startForegroundIfNeeded(grantedPermissions: Set) { + if (SDK_INT < Q) return + + val fgsTypePermissions: MutableSet = mutableSetOf() + + if (SDK_INT >= S) { + if (grantedPermissions.intersect(fgsConnectDevicePermissions).isNotEmpty()) { + fgsTypePermissions.add(FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE) + } + } + + if (grantedPermissions.intersect(fgsHealthPermissions) + .isNotEmpty() && (SDK_INT >= UPSIDE_DOWN_CAKE) + ) { + fgsTypePermissions.add(FOREGROUND_SERVICE_TYPE_HEALTH) + } + + if (grantedPermissions.intersect(fgsLocationPermissions).isNotEmpty()) { + fgsTypePermissions.add(FOREGROUND_SERVICE_TYPE_LOCATION) + } + if (grantedPermissions.intersect(fgsMicrophonePermissions) + .isNotEmpty() && (SDK_INT >= VERSION_CODES.R) + ) { + fgsTypePermissions.add(FOREGROUND_SERVICE_TYPE_MICROPHONE) + } + + if (fgsTypePermissions.isNotEmpty()) { + if (SDK_INT >= UPSIDE_DOWN_CAKE) { + fgsTypePermissions.add(FOREGROUND_SERVICE_TYPE_SPECIAL_USE) + } + + fgsTypePermissions.add(FOREGROUND_SERVICE_TYPE_DATA_SYNC) + + val combinedFgsType = fgsTypePermissions.reduce { acc, type -> acc or type } + + startForeground( + 1, createForegroundNotification(), + combinedFgsType + ) + } + } protected open fun createForegroundNotification(): Notification { val mainIntent = Intent(this, radarApp.mainActivity) @@ -345,9 +397,9 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis } if (grantedPermissions.isNotEmpty()) { + startForegroundIfNeeded(grantedPermissions) mHandler.execute { logger.info("Granted permissions {}", grantedPermissions) -// startForegroundIfNeeded(grantedPermissions) // Permission granted. needsPermissions -= grantedPermissions startScanning() From e794e447ddfb61ea02be800246012827cb9afdf6 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Fri, 25 Oct 2024 23:45:55 +0530 Subject: [PATCH 4/8] Updated extension function for PendingIntent --- .../src/main/java/org/radarbase/android/util/Extensions.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/radar-commons-android/src/main/java/org/radarbase/android/util/Extensions.kt b/radar-commons-android/src/main/java/org/radarbase/android/util/Extensions.kt index af211ca7a..de7cdcf08 100644 --- a/radar-commons-android/src/main/java/org/radarbase/android/util/Extensions.kt +++ b/radar-commons-android/src/main/java/org/radarbase/android/util/Extensions.kt @@ -8,6 +8,7 @@ fun String.takeTrimmedIfNotEmpty(): String? = trim { it <= ' ' } .takeUnless(String::isEmpty) fun Int.toPendingIntentFlag(mutable: Boolean = false) = this or when { + mutable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT mutable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> PendingIntent.FLAG_MUTABLE !mutable -> PendingIntent.FLAG_IMMUTABLE else -> 0 From cf7a111090a136f2646d6663b098434a458bb57f Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Fri, 25 Oct 2024 23:47:17 +0530 Subject: [PATCH 5/8] Added javadocs explaining security vulnerabilities of Mutable flag --- .../org/radarbase/android/util/Extensions.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/radar-commons-android/src/main/java/org/radarbase/android/util/Extensions.kt b/radar-commons-android/src/main/java/org/radarbase/android/util/Extensions.kt index de7cdcf08..2205b0427 100644 --- a/radar-commons-android/src/main/java/org/radarbase/android/util/Extensions.kt +++ b/radar-commons-android/src/main/java/org/radarbase/android/util/Extensions.kt @@ -7,6 +7,23 @@ import android.os.Build fun String.takeTrimmedIfNotEmpty(): String? = trim { it <= ' ' } .takeUnless(String::isEmpty) +/** + * Converts an integer to a PendingIntent flag with appropriate mutability settings. + * + * Android 14 (API level 34) introduces stricter security requirements for `PendingIntents`. + * - By default, `PendingIntents` should be immutable (`FLAG_IMMUTABLE`) unless explicitly required to be mutable. + * - Using the mutable flag without necessity may lead to security vulnerabilities, as mutable `PendingIntents` + * can be modified by other apps if granted. + * + * This function checks the Android version to set flags appropriately: + * - For API level 34 and above (`UPSIDE_DOWN_CAKE`), includes `FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT` to bypass the + * implicit intent restriction if `mutable` is true. + * - For API level 31 (Android 12, `S`) to API level 33, `FLAG_MUTABLE` is used when `mutable` is true. + * - For any other case or if `mutable` is false, the flag defaults to `FLAG_IMMUTABLE`. + * + * @param mutable Determines if the `PendingIntent` needs to be mutable (default: false). + * @return The calculated `PendingIntent` flag with the correct mutability based on API level. + */ fun Int.toPendingIntentFlag(mutable: Boolean = false) = this or when { mutable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT mutable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> PendingIntent.FLAG_MUTABLE @@ -14,7 +31,6 @@ fun Int.toPendingIntentFlag(mutable: Boolean = false) = this or when { else -> 0 } -@Suppress("UNCHECKED_CAST") inline fun Context.applySystemService(type: String, callback: (T) -> Boolean): Boolean? { return (getSystemService(type) as T?)?.let(callback) } From fb319f0406b84b54f65138d764419f5fe8c11e80 Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Sat, 26 Oct 2024 00:34:35 +0530 Subject: [PATCH 6/8] Updated app to use broadcast receiver safely --- .../passive/google/activity/GoogleActivityManager.kt | 4 +++- .../GoogleSleepManager.kt | 8 +++++++- .../src/main/java/org/radarbase/android/RadarService.kt | 4 ++-- .../java/org/radarbase/android/util/OfflineProcessor.kt | 9 ++++++++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/GoogleActivityManager.kt b/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/GoogleActivityManager.kt index 3878d9be3..7cb4cbb0f 100644 --- a/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/GoogleActivityManager.kt +++ b/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/GoogleActivityManager.kt @@ -69,7 +69,9 @@ class GoogleActivityManager(context: GoogleActivityService) : AbstractSourceMana private fun registerActivityTransitionReceiver() { val filter = IntentFilter(ACTION_ACTIVITY_UPDATE) - service.registerReceiver(activityTransitionReceiver, filter) + ContextCompat.registerReceiver(service, activityTransitionReceiver, filter, + ContextCompat.RECEIVER_EXPORTED + ) logger.info("Registered activity transition receiver.") } diff --git a/plugins/radar-android-google-sleep/src/main/java/org.radarbase.passive.google.sleep/GoogleSleepManager.kt b/plugins/radar-android-google-sleep/src/main/java/org.radarbase.passive.google.sleep/GoogleSleepManager.kt index d536859fd..8b257a628 100644 --- a/plugins/radar-android-google-sleep/src/main/java/org.radarbase.passive.google.sleep/GoogleSleepManager.kt +++ b/plugins/radar-android-google-sleep/src/main/java/org.radarbase.passive.google.sleep/GoogleSleepManager.kt @@ -19,9 +19,13 @@ package org.radarbase.passive.google.sleep import android.annotation.SuppressLint import android.app.PendingIntent import android.content.BroadcastReceiver +import android.content.Context.RECEIVER_NOT_EXPORTED import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager +import android.os.Build +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES import android.os.Process import androidx.core.content.ContextCompat import com.google.android.gms.location.ActivityRecognition @@ -69,7 +73,9 @@ class GoogleSleepManager(context: GoogleSleepService) : AbstractSourceManager acc or type } + val combinedFgsType: Int = fgsTypePermissions.reduce { acc, type -> acc or type } startForeground( 1, createForegroundNotification(), diff --git a/radar-commons-android/src/main/java/org/radarbase/android/util/OfflineProcessor.kt b/radar-commons-android/src/main/java/org/radarbase/android/util/OfflineProcessor.kt index 2112255ba..a615cb13f 100644 --- a/radar-commons-android/src/main/java/org/radarbase/android/util/OfflineProcessor.kt +++ b/radar-commons-android/src/main/java/org/radarbase/android/util/OfflineProcessor.kt @@ -29,6 +29,8 @@ import android.os.Debug import android.os.PowerManager import android.os.Process.THREAD_PRIORITY_BACKGROUND import android.os.SystemClock +import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.RECEIVER_EXPORTED import org.radarbase.util.CountedReference import org.slf4j.LoggerFactory import java.io.Closeable @@ -103,7 +105,12 @@ class OfflineProcessor( } handler.execute { didStart = true - context.registerReceiver(this.receiver, IntentFilter(requestName)) + ContextCompat.registerReceiver( + context, + this.receiver, + IntentFilter(requestName), + RECEIVER_EXPORTED + ) schedule() initializer?.let { it() } } From 77d84db0cea37103894e67241ec86aace5bcc50b Mon Sep 17 00:00:00 2001 From: this-Aditya Date: Sun, 27 Oct 2024 17:09:10 +0530 Subject: [PATCH 7/8] Addressed the issue regarding components not accurately receiving broadcast events --- .../passive/google/activity/GoogleActivityManager.kt | 6 ++++-- .../GoogleSleepManager.kt | 6 ++++-- .../java/org/radarbase/android/util/OfflineProcessor.kt | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/GoogleActivityManager.kt b/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/GoogleActivityManager.kt index 7cb4cbb0f..e32f3b016 100644 --- a/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/GoogleActivityManager.kt +++ b/plugins/radar-android-google-activity/src/main/java/org/radarbase/passive/google/activity/GoogleActivityManager.kt @@ -70,7 +70,7 @@ class GoogleActivityManager(context: GoogleActivityService) : AbstractSourceMana private fun registerActivityTransitionReceiver() { val filter = IntentFilter(ACTION_ACTIVITY_UPDATE) ContextCompat.registerReceiver(service, activityTransitionReceiver, filter, - ContextCompat.RECEIVER_EXPORTED + ContextCompat.RECEIVER_NOT_EXPORTED ) logger.info("Registered activity transition receiver.") } @@ -119,7 +119,9 @@ class GoogleActivityManager(context: GoogleActivityService) : AbstractSourceMana } private fun createActivityPendingIntent(): PendingIntent { - val intent = Intent(ACTION_ACTIVITY_UPDATE) + val intent = Intent(ACTION_ACTIVITY_UPDATE).apply { + `package` = service.packageName + } logger.info("Activity pending intent created") return PendingIntent.getBroadcast(service, ACTIVITY_UPDATE_REQUEST_CODE, intent, PendingIntent.FLAG_CANCEL_CURRENT.toPendingIntentFlag(true) diff --git a/plugins/radar-android-google-sleep/src/main/java/org.radarbase.passive.google.sleep/GoogleSleepManager.kt b/plugins/radar-android-google-sleep/src/main/java/org.radarbase.passive.google.sleep/GoogleSleepManager.kt index 8b257a628..dc0488934 100644 --- a/plugins/radar-android-google-sleep/src/main/java/org.radarbase.passive.google.sleep/GoogleSleepManager.kt +++ b/plugins/radar-android-google-sleep/src/main/java/org.radarbase.passive.google.sleep/GoogleSleepManager.kt @@ -74,7 +74,7 @@ class GoogleSleepManager(context: GoogleSleepService) : AbstractSourceManager Date: Mon, 28 Oct 2024 20:40:00 +0530 Subject: [PATCH 8/8] Removed specialUse permissions for FGS --- .../src/main/java/org/radarbase/android/RadarService.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt b/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt index a943434e8..ae78175f5 100644 --- a/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt +++ b/radar-commons-android/src/main/java/org/radarbase/android/RadarService.kt @@ -28,7 +28,6 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE -import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE import android.os.* import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES @@ -64,7 +63,6 @@ import org.radarcns.kafka.ObservationKey import org.slf4j.LoggerFactory import java.util.* import java.util.concurrent.atomic.AtomicBoolean -import kotlin.collections.HashSet abstract class RadarService : LifecycleService(), ServerStatusListener, LoginListener { private var configurationUpdateFuture: SafeHandler.HandlerFuture? = null @@ -237,12 +235,11 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis } else { /** - * API 34+ (Android 14+): Adding DATA_SYNC and SPECIAL_USE types + * API 34+ (Android 14+): Adding DATA_SYNC type * Currently this is not explicitly checking for android 14+ version. * This need to be modified it in future when setting new targetSdkVersion */ startForeground(1, createForegroundNotification(), - FOREGROUND_SERVICE_TYPE_SPECIAL_USE or FOREGROUND_SERVICE_TYPE_DATA_SYNC ) } @@ -278,9 +275,6 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis } if (fgsTypePermissions.isNotEmpty()) { - if (SDK_INT >= UPSIDE_DOWN_CAKE) { - fgsTypePermissions.add(FOREGROUND_SERVICE_TYPE_SPECIAL_USE) - } fgsTypePermissions.add(FOREGROUND_SERVICE_TYPE_DATA_SYNC)