From 4b5b88d65ba4316bdd7cb49f8ac2daa3852e4808 Mon Sep 17 00:00:00 2001 From: anod <171704+anod@users.noreply.github.com> Date: Thu, 23 Jul 2026 14:52:17 +0300 Subject: [PATCH 1/3] Update targetSdk to 37 and Robolectric to 4.17-beta-2 Bump targetSdk 35->37 (app) and 36->37 (baselineProfile) to match the project-wide compileSdk 37, and migrate the deprecation warnings that have clean, behavior-preserving AndroidX-compat replacements. Deprecation migrations: - IntentCompat.getParcelableExtra(..., Class) for typed parcelable extras (ShortcutInfoFactory, ModeDetector, ShortcutEditViewModel) - ServiceCompat.stopForeground(STOP_FOREGROUND_REMOVE) in ModeService - Activity setShowWhenLocked/setTurnScreenOn + requestDismissKeyguard instead of window keyguard flags in AcceptCallActivity - Platform Bundle instead of the deprecated androidx bundleOf in EditWidgetViewModel Robolectric: - Update 4.16.1 -> 4.17-beta-2 for native API 37 support - Add --add-opens=java.base/jdk.internal.access=ALL-UNNAMED to the forked unit-test JVM; JDK 21's module system otherwise blocks Robolectric's FileDescriptor shadow when running at API 37 Advance the lib submodule to the merged PackageInfoCompat version-code change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: deb19a46-a1a3-40c2-a9b3-a539e068631e --- app/build.gradle.kts | 2 +- .../com/anod/car/home/incar/AcceptCallActivity.kt | 14 +++++--------- .../java/com/anod/car/home/incar/ModeDetector.kt | 5 +++-- .../java/com/anod/car/home/incar/ModeService.kt | 3 ++- baselineProfile/build.gradle.kts | 2 +- build.gradle.kts | 8 ++++++++ .../carwidget/appwidget/EditWidgetViewModel.kt | 8 ++++---- .../carwidget/shortcut/ShortcutEditViewModel.kt | 3 ++- .../content/shortcuts/ShortcutInfoFactory.kt | 9 +++++---- gradle/libs.versions.toml | 2 +- lib | 2 +- 11 files changed, 33 insertions(+), 25 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 277ff612..9773c473 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,7 +55,7 @@ android { defaultConfig { minSdk = 31 - targetSdk = 35 // 29 wifi switch not working + targetSdk = 37 applicationId = "com.anod.car.home.free" versionCode = 3_41004 diff --git a/app/src/main/java/com/anod/car/home/incar/AcceptCallActivity.kt b/app/src/main/java/com/anod/car/home/incar/AcceptCallActivity.kt index 9feb950d..59db4331 100644 --- a/app/src/main/java/com/anod/car/home/incar/AcceptCallActivity.kt +++ b/app/src/main/java/com/anod/car/home/incar/AcceptCallActivity.kt @@ -11,7 +11,6 @@ import android.os.Build import android.os.Bundle import android.telephony.TelephonyManager import android.view.KeyEvent -import android.view.WindowManager import info.anodsplace.applog.AppLog import java.io.IOException @@ -68,15 +67,12 @@ class AcceptCallActivity : Activity() { private fun updateWindowFlags() { if (keyguardManager!!.isKeyguardLocked) { - window.addFlags( - WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) + setShowWhenLocked(true) + setTurnScreenOn(true) + keyguardManager!!.requestDismissKeyguard(this, null) } else { - window.clearFlags( - WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD or - WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) + setShowWhenLocked(false) + setTurnScreenOn(false) } } diff --git a/app/src/main/java/com/anod/car/home/incar/ModeDetector.kt b/app/src/main/java/com/anod/car/home/incar/ModeDetector.kt index 78fc0cb7..f517a91f 100644 --- a/app/src/main/java/com/anod/car/home/incar/ModeDetector.kt +++ b/app/src/main/java/com/anod/car/home/incar/ModeDetector.kt @@ -5,6 +5,7 @@ import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.content.Context import android.content.Intent +import androidx.core.content.IntentCompat import com.anod.car.home.BuildConfig import com.anod.car.home.getKoin import com.anod.car.home.utils.Power @@ -172,7 +173,7 @@ object ModeDetector { if (BluetoothDevice.ACTION_ACL_CONNECTED == action) { val devices = prefs.btDevices if (devices.isNotEmpty()) { - val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + val device: BluetoothDevice? = IntentCompat.getParcelableExtra(intent, BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) if (device != null && devices.containsKey(device.address)) { sEventState[FLAG_BLUETOOTH] = true return @@ -189,7 +190,7 @@ object ModeDetector { if (BluetoothDevice.ACTION_ACL_DISCONNECTED == action) { val devices = prefs.btDevices if (devices.isNotEmpty()) { - val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + val device: BluetoothDevice? = IntentCompat.getParcelableExtra(intent, BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java) if (device != null && devices.containsKey(device.address)) { sEventState[FLAG_BLUETOOTH] = false return diff --git a/app/src/main/java/com/anod/car/home/incar/ModeService.kt b/app/src/main/java/com/anod/car/home/incar/ModeService.kt index 42c7bc41..9c9670b0 100644 --- a/app/src/main/java/com/anod/car/home/incar/ModeService.kt +++ b/app/src/main/java/com/anod/car/home/incar/ModeService.kt @@ -8,6 +8,7 @@ import android.os.IBinder import android.os.PowerManager import android.telephony.PhoneStateListener import android.telephony.TelephonyManager +import androidx.core.app.ServiceCompat import com.anod.car.home.appwidget.Provider import com.anod.car.home.notifications.InCarModeNotificationFactory import info.anodsplace.applog.AppLog @@ -26,7 +27,7 @@ class ModeService : Service(), KoinComponent { private var forceState: Boolean = false override fun onDestroy() { - stopForeground(true) + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) val prefs = get() if (forceState) { diff --git a/baselineProfile/build.gradle.kts b/baselineProfile/build.gradle.kts index ddf8b872..f5ced163 100644 --- a/baselineProfile/build.gradle.kts +++ b/baselineProfile/build.gradle.kts @@ -13,7 +13,7 @@ android { defaultConfig { minSdk = 31 - targetSdk = 36 + targetSdk = 37 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/build.gradle.kts b/build.gradle.kts index 31419310..fd28a573 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,3 +10,11 @@ plugins { alias(libs.plugins.baselineprofile) apply false alias(libs.plugins.compose.compiler) apply false } + +subprojects { + // Robolectric 4.17 on JDK 17+ reflects into jdk.internal.access (FileDescriptor shadow at API 37+). + // The JPMS blocks this by default, so open the package to the forked unit-test JVM. + tasks.withType().configureEach { + jvmArgs("--add-opens=java.base/jdk.internal.access=ALL-UNNAMED") + } +} diff --git a/compose/src/androidMain/kotlin/info/anodsplace/carwidget/appwidget/EditWidgetViewModel.kt b/compose/src/androidMain/kotlin/info/anodsplace/carwidget/appwidget/EditWidgetViewModel.kt index 7a25a396..574c01fa 100644 --- a/compose/src/androidMain/kotlin/info/anodsplace/carwidget/appwidget/EditWidgetViewModel.kt +++ b/compose/src/androidMain/kotlin/info/anodsplace/carwidget/appwidget/EditWidgetViewModel.kt @@ -2,8 +2,8 @@ package info.anodsplace.carwidget.appwidget import android.app.Application import android.content.Context +import android.os.Bundle import androidx.compose.runtime.Immutable -import androidx.core.os.bundleOf import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import coil3.ImageLoader @@ -58,19 +58,19 @@ class EditWidgetViewModel( context = application, title = application.getString(R.string.pref_settings_transparent), iconRes = skinProperties.settingsButtonRes, - extras = bundleOf("button" to WidgetInterface.WIDGET_BUTTON_SETTINGS) + extras = Bundle().apply { putInt("button", WidgetInterface.WIDGET_BUTTON_SETTINGS) } ), ChooserEntry( context = application, title = application.getString(R.string.pref_incar_transparent), iconRes = skinProperties.inCarButtonEnterRes, - extras = bundleOf("button" to WidgetInterface.WIDGET_BUTTON_INCAR) + extras = Bundle().apply { putInt("button", WidgetInterface.WIDGET_BUTTON_INCAR) } ), ChooserEntry( context = application, title = application.getString(R.string.hidden), iconRes = skinProperties.buttonAlternativeHiddenResId, - extras = bundleOf("button" to WidgetInterface.WIDGET_BUTTON_HIDDEN) + extras = Bundle().apply { putInt("button", WidgetInterface.WIDGET_BUTTON_HIDDEN) } ) ) ) diff --git a/compose/src/androidMain/kotlin/info/anodsplace/carwidget/shortcut/ShortcutEditViewModel.kt b/compose/src/androidMain/kotlin/info/anodsplace/carwidget/shortcut/ShortcutEditViewModel.kt index fb6e8633..9b649739 100644 --- a/compose/src/androidMain/kotlin/info/anodsplace/carwidget/shortcut/ShortcutEditViewModel.kt +++ b/compose/src/androidMain/kotlin/info/anodsplace/carwidget/shortcut/ShortcutEditViewModel.kt @@ -6,6 +6,7 @@ import android.content.Intent.ShortcutIconResource import android.graphics.Bitmap import android.net.Uri import androidx.compose.runtime.Immutable +import androidx.core.content.IntentCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -215,7 +216,7 @@ class ShortcutEditViewModel( } if (intent.hasExtra("icon")) { - val bitmap: Bitmap? = intent.getParcelableExtra("icon") + val bitmap: Bitmap? = IntentCompat.getParcelableExtra(intent, "icon", Bitmap::class.java) if (bitmap != null) { val shortcutIcon = ShortcutIcon.forCustomIcon(viewState.shortcutId, bitmap) return shortcutIcon diff --git a/content/src/androidMain/kotlin/info/anodsplace/carwidget/content/shortcuts/ShortcutInfoFactory.kt b/content/src/androidMain/kotlin/info/anodsplace/carwidget/content/shortcuts/ShortcutInfoFactory.kt index 7ea1051c..d1469f46 100644 --- a/content/src/androidMain/kotlin/info/anodsplace/carwidget/content/shortcuts/ShortcutInfoFactory.kt +++ b/content/src/androidMain/kotlin/info/anodsplace/carwidget/content/shortcuts/ShortcutInfoFactory.kt @@ -12,6 +12,7 @@ import android.content.res.Resources import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.os.Parcelable +import androidx.core.content.IntentCompat import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.toDrawable import androidx.core.net.toUri @@ -48,17 +49,17 @@ class ShortcutIntent(val data: Intent, val isApp: Boolean) { val hasIntent: Boolean get() = data.hasExtra(Intent.EXTRA_SHORTCUT_INTENT) val intent: Intent? - get() = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT) + get() = IntentCompat.getParcelableExtra(data, Intent.EXTRA_SHORTCUT_INTENT, Intent::class.java) val name: String? get() = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME) val icon: Parcelable? - get() = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON) + get() = IntentCompat.getParcelableExtra(data, Intent.EXTRA_SHORTCUT_ICON, Parcelable::class.java) val iconResource: Parcelable? - get() = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE) + get() = IntentCompat.getParcelableExtra(data, Intent.EXTRA_SHORTCUT_ICON_RESOURCE, Parcelable::class.java) val preferIconResource: Boolean get() = data.getBooleanExtra(ShortcutExtra.EXTRA_PREFER_ICON_RESOURCE, false) val pinItemRequest: LauncherApps.PinItemRequest? - get() = data.getParcelableExtra(LauncherApps.EXTRA_PIN_ITEM_REQUEST) + get() = IntentCompat.getParcelableExtra(data, LauncherApps.EXTRA_PIN_ITEM_REQUEST, LauncherApps.PinItemRequest::class.java) } object ShortcutInfoFactory { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 839a23cc..97262165 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ profileinstaller = "1.4.1" window = "1.5.1" palette = "1.0.0" annotation = "1.10.0" -robolectric = "4.16.1" +robolectric = "4.17-beta-2" androidx-test-core = "1.7.0" nav3Core = "1.1.4" lifecycleViewmodelNav3 = "2.11.0" diff --git a/lib b/lib index 4601f8be..0bd0fdf3 160000 --- a/lib +++ b/lib @@ -1 +1 @@ -Subproject commit 4601f8be1e39be434c3ee487072433df90d820f8 +Subproject commit 0bd0fdf371e2fab4d672e7aa06b863992b5943df From 2bc7b8cd224a0bc3a9cdb286e14115f8b90b3c24 Mon Sep 17 00:00:00 2001 From: anod <171704+anod@users.noreply.github.com> Date: Sun, 26 Jul 2026 08:54:51 +0300 Subject: [PATCH 2/3] Fix stale shortcut icon preview after picking a custom icon The shortcut edit screen threads an iconVersion token into the Coil image request to bust the cache when the icon changes, but it stored the token in the request's generic extras bag. Coil ignores generic extras when computing the memory cache key, so the content-provider URI stayed identical and Coil kept serving the cached (stale) bitmap. The preview never refreshed after picking a custom icon, an icon-pack icon, or resetting to the default. Route the version into memoryCacheKeyExtra("version", ...) instead, which actually participates in the memory cache key. The custom fetcher returns a stream source, so Coil's disk cache is not involved and the memory cache was the sole source of staleness. Add ShortcutImageRequestUnitTest covering that the version lands in memoryCacheKeyExtras and that different versions yield different keys. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 81097fe6-0ab7-4177-bf55-70ee63e552aa --- .../db/ShortcutImageRequestUnitTest.kt | 52 +++++++++++++++++++ .../carwidget/content/db/Shortcut.kt | 5 +- 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/db/ShortcutImageRequestUnitTest.kt diff --git a/content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/db/ShortcutImageRequestUnitTest.kt b/content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/db/ShortcutImageRequestUnitTest.kt new file mode 100644 index 00000000..13c73227 --- /dev/null +++ b/content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/db/ShortcutImageRequestUnitTest.kt @@ -0,0 +1,52 @@ +package info.anodsplace.carwidget.content.db + +import android.content.Context +import android.content.Intent +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Verifies [toImageRequest] wires the icon version into Coil's memory cache key so a changed + * icon busts the cache and the shortcut preview refreshes. Generic request extras are ignored + * when Coil computes the cache key, so the version must live in [ImageRequest.memoryCacheKeyExtras]. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [31]) +class ShortcutImageRequestUnitTest { + + private val context: Context get() = ApplicationProvider.getApplicationContext() + + private fun shortcut(): Shortcut = Shortcut( + id = 42L, + position = 0, + itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION, + title = "Title", + isCustomIcon = true, + intent = Intent(Intent.ACTION_MAIN) + ) + + @Test + fun toImageRequest_omitsVersionWhenIconVersionIsDefault() { + val request = shortcut().toImageRequest(context, adaptiveIconStyle = "") + assertFalse(request.memoryCacheKeyExtras.containsKey("version")) + } + + @Test + fun toImageRequest_writesIconVersionIntoMemoryCacheKeyExtras() { + val request = shortcut().toImageRequest(context, adaptiveIconStyle = "", iconVersion = 123) + assertEquals("123", request.memoryCacheKeyExtras["version"]) + } + + @Test + fun toImageRequest_differentIconVersionsProduceDifferentCacheKeys() { + val first = shortcut().toImageRequest(context, adaptiveIconStyle = "", iconVersion = 1) + val second = shortcut().toImageRequest(context, adaptiveIconStyle = "", iconVersion = 2) + assertNotEquals(first.memoryCacheKeyExtras, second.memoryCacheKeyExtras) + } +} diff --git a/content/src/androidMain/kotlin/info/anodsplace/carwidget/content/db/Shortcut.kt b/content/src/androidMain/kotlin/info/anodsplace/carwidget/content/db/Shortcut.kt index bd697167..b94f97a3 100644 --- a/content/src/androidMain/kotlin/info/anodsplace/carwidget/content/db/Shortcut.kt +++ b/content/src/androidMain/kotlin/info/anodsplace/carwidget/content/db/Shortcut.kt @@ -5,7 +5,6 @@ import android.content.Context import android.content.Intent import android.net.Uri import androidx.compose.runtime.Immutable -import coil3.Extras import coil3.request.ImageRequest import info.anodsplace.carwidget.content.extentions.isDebugBuild import info.anodsplace.carwidget.content.preferences.WidgetInterface @@ -104,7 +103,9 @@ fun Shortcut.toImageRequest( ): ImageRequest = ImageRequest.Builder(context) .data(iconUri(context.isDebugBuild, adaptiveIconStyle, skinName)).apply { if (iconVersion != -1) { - extras[Extras.Key("version")] = iconVersion + // Route the version into the memory cache key so a changed icon busts + // Coil's cache. Generic request extras are ignored when computing the key. + memoryCacheKeyExtra("version", iconVersion.toString()) } } .build() \ No newline at end of file From c199fdb9f4b5c1e493edbc3e18795f57c4921a7a Mon Sep 17 00:00:00 2001 From: anod <171704+anod@users.noreply.github.com> Date: Sun, 26 Jul 2026 09:05:30 +0300 Subject: [PATCH 3/3] Address PR review: complete iconResource migration and tighten test - ShortcutEditViewModel: read EXTRA_SHORTCUT_ICON_RESOURCE via typed IntentCompat.getParcelableExtra for correct behavior on API 33+. Keep a narrowly-scoped @Suppress("DEPRECATION") because the platform constant itself is deprecated but is still what icon packs return. - ShortcutImageRequestUnitTest: rename the cache-key test to reflect that it asserts on memoryCacheKeyExtras (Coil does not expose the computed key) and assert on the specific version extra values. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 81097fe6-0ab7-4177-bf55-70ee63e552aa --- .../anodsplace/carwidget/shortcut/ShortcutEditViewModel.kt | 4 +++- .../carwidget/content/db/ShortcutImageRequestUnitTest.kt | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/compose/src/androidMain/kotlin/info/anodsplace/carwidget/shortcut/ShortcutEditViewModel.kt b/compose/src/androidMain/kotlin/info/anodsplace/carwidget/shortcut/ShortcutEditViewModel.kt index 9b649739..5aee1a96 100644 --- a/compose/src/androidMain/kotlin/info/anodsplace/carwidget/shortcut/ShortcutEditViewModel.kt +++ b/compose/src/androidMain/kotlin/info/anodsplace/carwidget/shortcut/ShortcutEditViewModel.kt @@ -206,8 +206,10 @@ class ShortcutEditViewModel( return null } + // EXTRA_SHORTCUT_ICON_RESOURCE is deprecated by the platform but is still what icon + // packs return; read it with the typed IntentCompat API for correct behavior on API 33+. @Suppress("DEPRECATION") - val iconResource = intent.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE) as? ShortcutIconResource + val iconResource = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_SHORTCUT_ICON_RESOURCE, ShortcutIconResource::class.java) if (iconResource != null) { val icon = ShortcutInfoFactory.resolveIconResource(viewState.shortcutId, iconResource, isCustom = true, context) if (icon != null) { diff --git a/content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/db/ShortcutImageRequestUnitTest.kt b/content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/db/ShortcutImageRequestUnitTest.kt index 13c73227..6be8be08 100644 --- a/content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/db/ShortcutImageRequestUnitTest.kt +++ b/content/src/androidHostTest/kotlin/info/anodsplace/carwidget/content/db/ShortcutImageRequestUnitTest.kt @@ -5,7 +5,6 @@ import android.content.Intent import androidx.test.core.app.ApplicationProvider import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotEquals import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @@ -44,9 +43,10 @@ class ShortcutImageRequestUnitTest { } @Test - fun toImageRequest_differentIconVersionsProduceDifferentCacheKeys() { + fun toImageRequest_differentIconVersionsProduceDifferentCacheKeyExtras() { val first = shortcut().toImageRequest(context, adaptiveIconStyle = "", iconVersion = 1) val second = shortcut().toImageRequest(context, adaptiveIconStyle = "", iconVersion = 2) - assertNotEquals(first.memoryCacheKeyExtras, second.memoryCacheKeyExtras) + assertEquals("1", first.memoryCacheKeyExtras["version"]) + assertEquals("2", second.memoryCacheKeyExtras["version"]) } }