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 } 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..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 @@ -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_NOT_EXPORTED + ) logger.info("Registered activity transition receiver.") } @@ -117,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 d536859fd..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 @@ -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 = 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) } } @@ -119,6 +128,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 @@ -208,11 +228,65 @@ 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 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_DATA_SYNC + ) + } 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()) { + + fgsTypePermissions.add(FOREGROUND_SERVICE_TYPE_DATA_SYNC) + + val combinedFgsType: Int = fgsTypePermissions.reduce { acc, type -> acc or type } + + startForeground( + 1, createForegroundNotification(), + combinedFgsType + ) + } + } + protected open fun createForegroundNotification(): Notification { val mainIntent = Intent(this, radarApp.mainActivity) return notificationHandler.create( @@ -317,6 +391,7 @@ abstract class RadarService : LifecycleService(), ServerStatusListener, LoginLis } if (grantedPermissions.isNotEmpty()) { + startForegroundIfNeeded(grantedPermissions) mHandler.execute { logger.info("Granted permissions {}", grantedPermissions) // Permission granted. @@ -683,7 +758,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 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..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,13 +7,30 @@ 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 !mutable -> PendingIntent.FLAG_IMMUTABLE else -> 0 } -@Suppress("UNCHECKED_CAST") inline fun Context.applySystemService(type: String, callback: (T) -> Boolean): Boolean? { return (getSystemService(type) as T?)?.let(callback) } 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..f3628ee40 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 @@ -79,7 +81,9 @@ class OfflineProcessor( this.alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager handler = config.handlerReference.acquire() - val intent = Intent(config.requestName) + val intent = Intent(config.requestName).apply { + `package` = context.packageName + } pendingIntent = PendingIntent.getBroadcast( context, requireNotNull(config.requestCode) { "Cannot start processor without request code" }, @@ -103,7 +107,12 @@ class OfflineProcessor( } handler.execute { didStart = true - context.registerReceiver(this.receiver, IntentFilter(requestName)) + ContextCompat.registerReceiver( + context, + this.receiver, + IntentFilter(requestName), + ContextCompat.RECEIVER_NOT_EXPORTED + ) schedule() initializer?.let { it() } }