diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 0231ad224f..ac21c24dd9 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -50,10 +50,10 @@ jobs: echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Run instrumented tests (API 23) + - name: Run instrumented tests (API 21) uses: ReactiveCircus/android-emulator-runner@v2 with: - api-level: 23 + api-level: 21 arch: x86_64 script: ./gradlew connected${{ matrix.flavor }}DebugAndroidTest - name: Run instrumented tests (API 35) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd499df24c..a78e92f524 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased - 159 -Android 5.0 and 5.1 are no longer supported starting with this release. If you want to use Catima on these versions, please use version 2.41.1. - - Fix change introduced in 2.41.0 that broke support for some scanners for non-UTF-8 barcodes ## v2.41.1 - 158 (2025-12-31) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ccd8159dfb..120b207bc8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,7 +17,7 @@ android { defaultConfig { applicationId = "me.hackerchick.catima" - minSdk = 23 + minSdk = 21 targetSdk = 36 versionCode = 158 versionName = "2.41.1" diff --git a/app/src/main/java/protect/card_locker/CatimaAppCompatActivity.java b/app/src/main/java/protect/card_locker/CatimaAppCompatActivity.java index 50f5dd852b..c44a19fc4a 100644 --- a/app/src/main/java/protect/card_locker/CatimaAppCompatActivity.java +++ b/app/src/main/java/protect/card_locker/CatimaAppCompatActivity.java @@ -38,10 +38,15 @@ protected void onPostCreate(@Nullable Bundle savedInstanceState) { Window window = getWindow(); if (window != null) { boolean darkMode = Utils.isDarkModeEnabled(this); - View decorView = window.getDecorView(); - WindowInsetsControllerCompat wic = new WindowInsetsControllerCompat(window, decorView); - wic.setAppearanceLightStatusBars(!darkMode); - window.setStatusBarColor(Color.TRANSPARENT); + if (Build.VERSION.SDK_INT >= 23) { + View decorView = window.getDecorView(); + WindowInsetsControllerCompat wic = new WindowInsetsControllerCompat(window, decorView); + wic.setAppearanceLightStatusBars(!darkMode); + window.setStatusBarColor(Color.TRANSPARENT); + } else { + // icons are always white back then + window.setStatusBarColor(darkMode ? Color.TRANSPARENT : Color.argb(127, 0, 0, 0)); + } } // XXX android 9 and below has a nasty rendering bug if the theme was patched earlier Utils.postPatchColors(this); @@ -61,4 +66,7 @@ protected void enableToolbarBackButton() { actionBar.setDisplayHomeAsUpEnabled(true); } } + + public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + } } diff --git a/app/src/main/java/protect/card_locker/ListWidget.kt b/app/src/main/java/protect/card_locker/ListWidget.kt index d6242eca7d..3a741fcb60 100644 --- a/app/src/main/java/protect/card_locker/ListWidget.kt +++ b/app/src/main/java/protect/card_locker/ListWidget.kt @@ -100,7 +100,8 @@ class ListWidget : AppWidgetProvider() { val foreground = if (Utils.needsDarkForeground(headerColor)) Color.BLACK else Color.WHITE setInt(R.id.item_container_foreground, "setBackgroundColor", headerColor) val icon = loyaltyCard.getImageThumbnail(context) - // FIXME: The icon flow causes a crash up to Android 12L, so force anything below 33 down this path + // setImageViewIcon is not supported on Android 5, so force Android 5 down the text path + // FIXME: The icon flow causes a crash up to Android 12L, so SDK_INT is forced up from 23 to 33 if (icon != null && Build.VERSION.SDK_INT >= 32) { setInt(R.id.item_container_foreground, "setBackgroundColor", foreground) setImageViewIcon(R.id.item_image, Icon.createWithBitmap(icon)) diff --git a/app/src/main/java/protect/card_locker/PermissionUtils.java b/app/src/main/java/protect/card_locker/PermissionUtils.java index 583da8e14d..71cf75f523 100644 --- a/app/src/main/java/protect/card_locker/PermissionUtils.java +++ b/app/src/main/java/protect/card_locker/PermissionUtils.java @@ -34,6 +34,11 @@ private static boolean needsStorageReadPermission(Activity activity) { * @return */ public static boolean needsCameraPermission(Activity activity) { + // Android only introduced the runtime permission system in Marshmallow (Android 6.0) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return false; + } + return ContextCompat.checkSelfPermission(activity, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED; } @@ -44,14 +49,21 @@ public static boolean needsCameraPermission(Activity activity) { * @param activity * @param requestCode */ - public static void requestStorageReadPermission(Activity activity, int requestCode) { + public static void requestStorageReadPermission(CatimaAppCompatActivity activity, int requestCode) { String[] permissions = new String[]{ android.Manifest.permission.READ_EXTERNAL_STORAGE }; int[] mockedResults = new int[]{ PackageManager.PERMISSION_GRANTED }; if (needsStorageReadPermission(activity)) { ActivityCompat.requestPermissions(activity, permissions, requestCode); } else { - activity.onRequestPermissionsResult(requestCode, permissions, mockedResults); + // FIXME: This points to onMockedRequestPermissionResult instead of to + // onRequestPermissionResult because onRequestPermissionResult was only introduced in + // Android 6.0 (SDK 23) and we and to support Android 5.0 (SDK 21) too. + // + // When minSdk becomes 23, this should point to onRequestPermissionResult directly and + // the activity input variable should be changed from CatimaAppCompatActivity to + // Activity. + activity.onMockedRequestPermissionsResult(requestCode, permissions, mockedResults); } } @@ -62,14 +74,21 @@ public static void requestStorageReadPermission(Activity activity, int requestCo * @param activity * @param requestCode */ - public static void requestCameraPermission(Activity activity, int requestCode) { + public static void requestCameraPermission(CatimaAppCompatActivity activity, int requestCode) { String[] permissions = new String[]{ Manifest.permission.CAMERA }; int[] mockedResults = new int[]{ PackageManager.PERMISSION_GRANTED }; if (needsCameraPermission(activity)) { ActivityCompat.requestPermissions(activity, permissions, requestCode); } else { - activity.onRequestPermissionsResult(requestCode, permissions, mockedResults); + // FIXME: This points to onMockedRequestPermissionResult instead of to + // onRequestPermissionResult because onRequestPermissionResult was only introduced in + // Android 6.0 (SDK 23) and we and to support Android 5.0 (SDK 21) too. + // + // When minSdk becomes 23, this should point to onRequestPermissionResult directly and + // the activity input variable should be changed from CatimaAppCompatActivity to + // Activity. + activity.onMockedRequestPermissionsResult(requestCode, permissions, mockedResults); } } } \ No newline at end of file diff --git a/app/src/main/java/protect/card_locker/ScanActivity.kt b/app/src/main/java/protect/card_locker/ScanActivity.kt index 952dac3c30..52beb68eaf 100644 --- a/app/src/main/java/protect/card_locker/ScanActivity.kt +++ b/app/src/main/java/protect/card_locker/ScanActivity.kt @@ -543,6 +543,14 @@ class ScanActivity : CatimaAppCompatActivity() { ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) + onMockedRequestPermissionsResult(requestCode, permissions, grantResults) + } + + override fun onMockedRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED diff --git a/app/src/main/java/protect/card_locker/UCropWrapper.kt b/app/src/main/java/protect/card_locker/UCropWrapper.kt index 352fb52665..caa326c763 100644 --- a/app/src/main/java/protect/card_locker/UCropWrapper.kt +++ b/app/src/main/java/protect/card_locker/UCropWrapper.kt @@ -40,9 +40,16 @@ class UCropWrapper : UCropActivity() { return } - val decorView = window.decorView - val wic = WindowInsetsControllerCompat(window, decorView) - wic.isAppearanceLightStatusBars = !darkMode + if (Build.VERSION.SDK_INT >= 23) { + val decorView = window.decorView + val wic = WindowInsetsControllerCompat(window, decorView) + wic.isAppearanceLightStatusBars = !darkMode + } else if (!darkMode) { + window.statusBarColor = ColorUtils.compositeColors( + Color.argb(127, 0, 0, 0), + window.statusBarColor + ) + } } private fun checkViews(darkMode: Boolean) { diff --git a/app/src/main/java/protect/card_locker/contentprovider/CardsContentProvider.java b/app/src/main/java/protect/card_locker/contentprovider/CardsContentProvider.java index 04807eb245..77c6ccb37a 100644 --- a/app/src/main/java/protect/card_locker/contentprovider/CardsContentProvider.java +++ b/app/src/main/java/protect/card_locker/contentprovider/CardsContentProvider.java @@ -77,6 +77,13 @@ public Cursor query(@NonNull final Uri uri, @Nullable final String selection, @Nullable final String[] selectionArgs, @Nullable final String sortOrder) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // Disable the content provider on SDK < 23 since it grants dangerous + // permissions at install-time + Log.w(TAG, "Content provider read is only available for SDK >= 23"); + return null; + } + final Settings settings = new Settings(getContext()); if (!settings.getAllowContentProviderRead()) { Log.w(TAG, "Content provider read is disabled"); diff --git a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java index 82af0f1dd3..a6567dd44e 100644 --- a/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java +++ b/app/src/main/java/protect/card_locker/importexport/CatimaImporter.java @@ -459,7 +459,6 @@ private LoyaltyCard importLoyaltyCard(CSVRecord record) throws FormatException { barcodeType = CatimaBarcode.fromName(unparsedBarcodeType); } - // This field did not exist in version 2.40.0 and before Charset barcodeEncoding = null; String unparsedBarcodeEncoding = CSVHelpers.extractString(DBHelper.LoyaltyCardDbIds.BARCODE_ENCODING, record, ""); if (!unparsedBarcodeEncoding.isEmpty()) { diff --git a/app/src/main/java/protect/card_locker/preferences/SettingsActivity.kt b/app/src/main/java/protect/card_locker/preferences/SettingsActivity.kt index 8d48320d22..06b7377ecb 100644 --- a/app/src/main/java/protect/card_locker/preferences/SettingsActivity.kt +++ b/app/src/main/java/protect/card_locker/preferences/SettingsActivity.kt @@ -157,6 +157,12 @@ class SettingsActivity : CatimaAppCompatActivity() { true } + // Disable content provider on SDK < 23 since dangerous permissions + // are granted at install-time + val contentProviderReadPreference = findPreference(getString(R.string.settings_key_allow_content_provider_read)) + contentProviderReadPreference!!.isVisible = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + // Hide crash reporter settings on builds it's not enabled on val crashReporterPreference = findPreference("acra.enable") crashReporterPreference!!.isVisible = BuildConfig.useAcraCrashReporter diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5792aad3cc..3c0ad6d38e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] # AndroidX -compose = "2025.12.01" +compose = "2025.11.01" # Third-party acra = "5.13.1" @@ -22,7 +22,7 @@ com-google-android-material-material = { group = "com.google.android.material", com-android-tools-desugar_jdk_libs = { group = "com.android.tools", name = "desugar_jdk_libs", version = "2.1.5" } # Compose -androidx-activity-activity-compose = { group = "androidx.activity", name = "activity-compose", version = "1.12.2" } +androidx-activity-activity-compose = { group = "androidx.activity", name = "activity-compose", version = "1.10.1" } androidx-compose-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose" } androidx-compose-foundation-foundation = { group = "androidx.compose.foundation", name = "foundation" } androidx-compose-material3-material3 = { group = "androidx.compose.material3", name = "material3"}