From c1b9efe48204f35fc0b8865877e6253aa755f844 Mon Sep 17 00:00:00 2001 From: pblundell Date: Fri, 27 Feb 2026 00:01:04 +0000 Subject: [PATCH] Update minimum SDK to API 21 (Android 5.0 Lollipop) This commit migrates the entire LeakCanary project from minimum SDK API 14 to API 21, enabling modern Android features and simplifying the codebase. API 21 corresponds to Android 5.0 (Lollipop), released in 2014, providing over a decade of Android compatibility while enabling Material Design and modern development practices. Changes included: ## Build Configuration - Update androidMinSdk from "14" to "21" in version catalog - Standardize all modules to use libs.versions.androidMinSdk reference - Update hardcoded minSdk values in sample and library modules to use TOML ## GitHub Actions / CI - Remove API 16 and API 19 from test matrix - Streamline CI to test only API 21+ (21, 23, 26, 31, 33, 34) - Improve CI efficiency with fewer test combinations ## Test Infrastructure - Upgrade androidx.test:orchestrator from 1.4.2 to 1.5.0 - Modernize test assertions to use direct assertThat(Throwable) calls - Remove API 16 workaround comments and code ## Code Modernization - Remove obsolete SDK version checks (9 lint issues resolved) - Simplify file provider methods for API 21+ only - Update notification builder usage for modern APIs - Remove legacy view tree observer workarounds - Modernize layout and rendering code ## Resources & UI - Migrate drawable resources from selectors to Material ripple effects - Update themes from Holo to Material Design (API 21+ requirement) - Merge and remove obsolete -v21 resource folders - Modernize button and touch feedback using ripples ## Benefits - Access to Material Design components and APIs - Simplified codebase with removed legacy compatibility code - Enhanced test infrastructure with latest orchestrator - Improved user experience with modern UI patterns - Faster CI builds with fewer test matrix combinations ## Compatibility All modules now consistently require Android 5.0 (API 21, 2014) or higher. This affects minSdk but does not change the public API surface. Co-Authored-By: Claude --- .github/workflows/main.yml | 9 ------ gradle/libs.versions.toml | 4 +-- .../internal/LeakCanaryFileProvider.kt | 15 ++-------- .../java/leakcanary/internal/Notifications.kt | 8 +----- .../internal/SquigglySpanRenderer.kt | 3 +- .../internal/activity/db/Cursors.kt | 2 +- .../activity/screen/RenderHeapDumpScreen.kt | 8 +----- .../internal/navigation/NavigatingActivity.kt | 11 +++----- .../drawable-v21/leak_canary_gray_fill.xml | 16 ----------- .../leak_canary_list_selector.xml | 9 ------ .../leak_canary_primary_button.xml | 17 ----------- .../leak_canary_secondary_button.xml | 17 ----------- .../leak_canary_tab_background.xml | 4 --- .../res/drawable/leak_canary_gray_fill.xml | 9 +++--- .../drawable/leak_canary_list_selector.xml | 2 +- .../drawable/leak_canary_primary_button.xml | 11 ++------ .../drawable/leak_canary_secondary_button.xml | 11 ++------ .../drawable/leak_canary_tab_background.xml | 8 +++--- .../leak_canary_tab_selector_ripple.xml | 2 +- .../res/values-v21/leak_canary_themes.xml | 28 ------------------- .../main/res/values/leak_canary_themes.xml | 6 ++-- .../leakcanary/TestDescriptionHolderTest.kt | 9 ++---- .../build.gradle.kts | 2 +- .../src/main/java/leakcanary/ProcessInfo.kt | 14 ++-------- .../build.gradle.kts | 2 +- .../build.gradle.kts | 2 +- 26 files changed, 41 insertions(+), 188 deletions(-) delete mode 100644 leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_gray_fill.xml delete mode 100644 leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_list_selector.xml delete mode 100644 leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_primary_button.xml delete mode 100644 leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_secondary_button.xml delete mode 100644 leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_tab_background.xml rename leakcanary/leakcanary-android-core/src/main/res/{drawable-v21 => drawable}/leak_canary_tab_selector_ripple.xml (95%) delete mode 100644 leakcanary/leakcanary-android-core/src/main/res/values-v21/leak_canary_themes.xml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2cca2460f..207086403 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,15 +42,6 @@ jobs: - 31 - 33 - 34 - include: - - arch: x86 - api-level: 16 - target: google_apis - channel: stable - - arch: x86 - api-level: 19 - target: google_apis - channel: stable steps: - name: Enable KVM group perms run: | diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5409363a7..fead10726 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,7 @@ androidXJunit = "1.1.5" workManager = "2.7.0" detekt = "1.23.8" hilt = "2.53" -androidMinSdk = "14" +androidMinSdk = "21" androidCompileSdk = "35" [libraries] @@ -60,7 +60,7 @@ androidX-test-monitor = { module = "androidx.test:monitor", version = "1.6.1" } androidX-test-rules = { module = "androidx.test:rules", version.ref = "androidXTest" } # Exposed transitively, avoid increasing androidX-test-runner = { module = "androidx.test:runner", version = "1.5.2" } -androidX-test-orchestrator = { module = "androidx.test:orchestrator", version = "1.4.2" } # 1.5.0+ requires API 21+, keeping at 1.4.2 for API 16 compatibility +androidX-test-orchestrator = { module = "androidx.test:orchestrator", version = "1.5.0" } androidX-test-espresso = { module = "androidx.test.espresso:espresso-core", version = "3.5.1" } androidX-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidXJunit" } androidX-test-junitKtx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidXJunit" } diff --git a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/LeakCanaryFileProvider.kt b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/LeakCanaryFileProvider.kt index c5f6ae13a..00156837a 100644 --- a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/LeakCanaryFileProvider.kt +++ b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/LeakCanaryFileProvider.kt @@ -24,7 +24,6 @@ import android.content.pm.ProviderInfo import android.database.Cursor import android.database.MatrixCursor import android.net.Uri -import android.os.Build import android.os.Environment import android.os.ParcelFileDescriptor import android.os.StrictMode @@ -499,7 +498,7 @@ internal class LeakCanaryFileProvider : ContentProvider() { if (externalCacheDirs.isNotEmpty()) { target = externalCacheDirs[0] } - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && TAG_EXTERNAL_MEDIA == tag) { + } else if (TAG_EXTERNAL_MEDIA == tag) { val externalMediaDirs = context.externalMediaDirs if (externalMediaDirs.isNotEmpty()) { target = externalMediaDirs[0] @@ -519,19 +518,11 @@ internal class LeakCanaryFileProvider : ContentProvider() { context: Context, type: String? ): Array { - return if (Build.VERSION.SDK_INT >= 19) { - context.getExternalFilesDirs(type) - } else { - arrayOf(context.getExternalFilesDir(type)!!) - } + return context.getExternalFilesDirs(type) } private fun getExternalCacheDirs(context: Context): Array { - return if (Build.VERSION.SDK_INT >= 19) { - context.externalCacheDirs - } else { - arrayOf(context.externalCacheDir!!) - } + return context.externalCacheDirs } /** diff --git a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/Notifications.kt b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/Notifications.kt index 5a41a4181..ac18e5aa1 100644 --- a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/Notifications.kt +++ b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/Notifications.kt @@ -22,7 +22,6 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.os.Build.VERSION.SDK_INT -import android.os.Build.VERSION_CODES.JELLY_BEAN import android.os.Build.VERSION_CODES.O import com.squareup.leakcanary.core.R import leakcanary.LeakCanary @@ -130,11 +129,6 @@ internal object Notifications { builder.setGroup(type.name) } - return if (SDK_INT < JELLY_BEAN) { - @Suppress("DEPRECATION") - builder.notification - } else { - builder.build() - } + return builder.build() } } diff --git a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/SquigglySpanRenderer.kt b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/SquigglySpanRenderer.kt index 36229b28d..ef17ee376 100644 --- a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/SquigglySpanRenderer.kt +++ b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/SquigglySpanRenderer.kt @@ -4,7 +4,6 @@ import android.content.Context import android.graphics.Canvas import android.graphics.Paint import android.graphics.Path -import android.os.Build import android.text.Layout import com.squareup.leakcanary.core.R import kotlin.math.max @@ -107,7 +106,7 @@ internal abstract class SquigglySpanRenderer(context: Context) { private fun Layout.getLineBottomWithoutSpacing(line: Int): Int { val lineBottom = getLineBottom(line) - val lastLineSpacingNotAdded = Build.VERSION.SDK_INT >= 19 + val lastLineSpacingNotAdded = true val isLastLine = line == lineCount - 1 val lineBottomWithoutSpacing: Int diff --git a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/activity/db/Cursors.kt b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/activity/db/Cursors.kt index 6cb1e2741..8e9277520 100644 --- a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/activity/db/Cursors.kt +++ b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/activity/db/Cursors.kt @@ -6,7 +6,7 @@ import kotlin.concurrent.getOrSet /** * Similar to the more generic use() for Closable. - * Cursor started implementing Closable in API 16. + * Cursor implements Closable on all supported API levels (21+). */ internal inline fun Cursor.use(block: (Cursor) -> R): R { var exception: Throwable? = null diff --git a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/activity/screen/RenderHeapDumpScreen.kt b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/activity/screen/RenderHeapDumpScreen.kt index 09a62d5ee..32d68d372 100644 --- a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/activity/screen/RenderHeapDumpScreen.kt +++ b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/activity/screen/RenderHeapDumpScreen.kt @@ -2,8 +2,6 @@ package leakcanary.internal.activity.screen import android.content.Intent import android.graphics.Bitmap -import android.os.Build.VERSION -import android.os.Build.VERSION_CODES import android.os.Environment import android.os.Environment.DIRECTORY_DOWNLOADS import android.view.View @@ -60,11 +58,7 @@ internal class RenderHeapDumpScreen( imageView.visibility = View.VISIBLE } } - if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { - viewTreeObserver.removeOnGlobalLayoutListener(this) - } else { - viewTreeObserver.removeGlobalOnLayoutListener(this) - } + viewTreeObserver.removeOnGlobalLayoutListener(this) } }) diff --git a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/navigation/NavigatingActivity.kt b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/navigation/NavigatingActivity.kt index 31e25c5d5..d376d93e6 100644 --- a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/navigation/NavigatingActivity.kt +++ b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/navigation/NavigatingActivity.kt @@ -2,7 +2,6 @@ package leakcanary.internal.navigation import android.app.Activity import android.content.Intent -import android.os.Build.VERSION.SDK_INT import android.os.Bundle import android.os.Parcelable import android.view.Menu @@ -155,12 +154,10 @@ internal abstract class NavigatingActivity : Activity() { private fun screenUpdated() { invalidateOptionsMenu() - if (SDK_INT >= 18) { - actionBar?.run { - val goBack = backstack.size > 0 - val indicator = if (goBack) 0 else android.R.drawable.ic_menu_close_clear_cancel - setHomeAsUpIndicator(indicator) - } + actionBar?.run { + val goBack = backstack.size > 0 + val indicator = if (goBack) 0 else android.R.drawable.ic_menu_close_clear_cancel + setHomeAsUpIndicator(indicator) } onNewScreen(currentScreen) } diff --git a/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_gray_fill.xml b/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_gray_fill.xml deleted file mode 100644 index 00d67ea3a..000000000 --- a/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_gray_fill.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_list_selector.xml b/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_list_selector.xml deleted file mode 100644 index 4bbb9c540..000000000 --- a/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_list_selector.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_primary_button.xml b/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_primary_button.xml deleted file mode 100644 index 67b9d2c63..000000000 --- a/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_primary_button.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_secondary_button.xml b/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_secondary_button.xml deleted file mode 100644 index 97cc859a0..000000000 --- a/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_secondary_button.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_tab_background.xml b/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_tab_background.xml deleted file mode 100644 index 2418076a5..000000000 --- a/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_tab_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_gray_fill.xml b/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_gray_fill.xml index 0685b99ef..00d67ea3a 100644 --- a/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_gray_fill.xml +++ b/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_gray_fill.xml @@ -1,8 +1,9 @@ - - + + - + @@ -12,4 +13,4 @@ - + diff --git a/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_list_selector.xml b/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_list_selector.xml index 3f6a244ac..4bbb9c540 100644 --- a/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_list_selector.xml +++ b/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_list_selector.xml @@ -1,7 +1,7 @@ - + diff --git a/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_primary_button.xml b/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_primary_button.xml index 5fe33eade..8682faf1c 100644 --- a/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_primary_button.xml +++ b/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_primary_button.xml @@ -1,16 +1,11 @@ - - - - - - - + - + diff --git a/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_secondary_button.xml b/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_secondary_button.xml index 8987d5f63..1329d0ff8 100644 --- a/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_secondary_button.xml +++ b/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_secondary_button.xml @@ -1,15 +1,10 @@ - - - - - - - + - + diff --git a/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_tab_background.xml b/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_tab_background.xml index 9d1173d49..015470a65 100644 --- a/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_tab_background.xml +++ b/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_tab_background.xml @@ -1,5 +1,5 @@ - - - - + + + diff --git a/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_tab_selector_ripple.xml b/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_tab_selector_ripple.xml similarity index 95% rename from leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_tab_selector_ripple.xml rename to leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_tab_selector_ripple.xml index a924d4e04..3c8726948 100644 --- a/leakcanary/leakcanary-android-core/src/main/res/drawable-v21/leak_canary_tab_selector_ripple.xml +++ b/leakcanary/leakcanary-android-core/src/main/res/drawable/leak_canary_tab_selector_ripple.xml @@ -2,4 +2,4 @@ + /> \ No newline at end of file diff --git a/leakcanary/leakcanary-android-core/src/main/res/values-v21/leak_canary_themes.xml b/leakcanary/leakcanary-android-core/src/main/res/values-v21/leak_canary_themes.xml deleted file mode 100644 index c18b95185..000000000 --- a/leakcanary/leakcanary-android-core/src/main/res/values-v21/leak_canary_themes.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/leakcanary/leakcanary-android-core/src/main/res/values/leak_canary_themes.xml b/leakcanary/leakcanary-android-core/src/main/res/values/leak_canary_themes.xml index bd8ddb82c..bfcdc4e3e 100644 --- a/leakcanary/leakcanary-android-core/src/main/res/values/leak_canary_themes.xml +++ b/leakcanary/leakcanary-android-core/src/main/res/values/leak_canary_themes.xml @@ -15,14 +15,14 @@ ~ limitations under the License. --> - -