diff --git a/CHANGELOG.md b/CHANGELOG.md index f5acc4c..348f3c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,34 @@ All notable changes to RootADB Pro will be documented in this file. +## [Unreleased] + +### Changed +- Target and compile SDK raised from 34 to 35 (Android 15); Android 15's + enforced edge-to-edge is handled by the explicit inset listeners added in + this release + +### Added +- Accent color selection now actually themes the app: each accent applies a + Material 3 tonal overlay (light + dark palettes) at activity creation +- "Dynamic" accent option using Material You wallpaper colors (Android 12+) +- Adaptive launcher icon with monochrome layer for Android 13+ themed icons +- Predictive back gesture opt-in (Android 13+) + +### Fixed +- Android 12+ light mode no longer renders a near-black background: removed + the broken `values-v31` dynamic-color theme that hardcoded + `system_neutral1_900` into a DayNight theme +- Edge-to-edge insets are now handled explicitly (status bar padding on the + app bar, navigation-bar padding on scrolling content) instead of relying on + `fitsSystemWindows`, surviving Android 15's enforced edge-to-edge and + display cutouts +- Fragment content no longer rests hidden behind the gesture navigation bar +- Android 8.0 (API 26): navigation bar now uses a translucent scrim since the + platform cannot render dark nav-bar icons, keeping buttons visible on light + content +- Orange accent uses a dark on-primary color for WCAG-compliant contrast + ## [1.3.0] - 2026-07-15 ### Security diff --git a/README.md b/README.md index f26a975..c4db45a 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ A powerful Android utility that enables wireless ADB debugging with multiple rem ### Theme Customization - Light / Dark / System modes - 6 accent colors: Blue, Teal, Purple, Orange, Pink, Green +- Dynamic (Material You) wallpaper colors on Android 12+ +- Adaptive + themed launcher icon (Android 13+ monochrome) - Material 3 design language ## Requirements @@ -214,7 +216,7 @@ app/src/main/java/com/phenix/wirelessadb/ ### v1.2.0 (Current) - **Dashboard Tab** - Unified control center -- **Theme System** - Light/Dark modes + 6 accent colors +- **Theme System** - Light/Dark modes + 6 accent colors + Material You dynamic color (Android 12+) - **P2P Token** - Device-to-device connections - **ADB Auto-Pairing** - Android 11+ wireless pairing - **Shizuku Enhancement** - Proper UserService integration diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3f106a6..1727a42 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,12 +22,12 @@ val ciKeyPassword: String? by project android { namespace = "com.phenix.wirelessadb" - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "com.phenix.wirelessadb" minSdk = 26 - targetSdk = 34 + targetSdk = 35 versionCode = 6 versionName = "1.3.0" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5b13168..29b6bc1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,12 +33,13 @@ + tools:targetApi="35"> = Build.VERSION_CODES.S && DynamicColors.isDynamicColorAvailable() + /** * Get current accent color. */ diff --git a/app/src/main/java/com/phenix/wirelessadb/ui/ControlFragment.kt b/app/src/main/java/com/phenix/wirelessadb/ui/ControlFragment.kt index 60794bb..c3ef36a 100644 --- a/app/src/main/java/com/phenix/wirelessadb/ui/ControlFragment.kt +++ b/app/src/main/java/com/phenix/wirelessadb/ui/ControlFragment.kt @@ -24,6 +24,7 @@ import com.phenix.wirelessadb.util.AccessibilityHelper.setStateDescription import com.phenix.wirelessadb.relay.TailscaleHelper import com.phenix.wirelessadb.viewmodel.AdbViewModel import com.phenix.wirelessadb.warpgate.WarpgateConfig +import com.phenix.wirelessadb.util.applyBottomSystemBarInsets /** * TAB 1: CONTROL (v1.3.0 - Mode-Based UI) @@ -74,6 +75,8 @@ class ControlFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // Edge-to-edge: keep scrollable content clear of the gesture/navigation bar + view.applyBottomSystemBarInsets() setupViews() observeViewModel() } diff --git a/app/src/main/java/com/phenix/wirelessadb/ui/DevicesFragment.kt b/app/src/main/java/com/phenix/wirelessadb/ui/DevicesFragment.kt index 4b4a7f7..469f8ab 100644 --- a/app/src/main/java/com/phenix/wirelessadb/ui/DevicesFragment.kt +++ b/app/src/main/java/com/phenix/wirelessadb/ui/DevicesFragment.kt @@ -14,6 +14,7 @@ import com.phenix.wirelessadb.model.TrustedDevice import com.phenix.wirelessadb.relay.DeviceAuthManager import com.phenix.wirelessadb.util.AccessibilityHelper.announceForAccessibility import com.phenix.wirelessadb.viewmodel.AdbViewModel +import com.phenix.wirelessadb.util.applyBottomSystemBarInsets /** * TAB 2: DEVICES (v1.3.0) @@ -42,6 +43,8 @@ class DevicesFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // Edge-to-edge: keep scrollable content clear of the gesture/navigation bar + view.applyBottomSystemBarInsets() authManager = DeviceAuthManager(requireContext()) setupViews() observeViewModel() diff --git a/app/src/main/java/com/phenix/wirelessadb/ui/HelpFragment.kt b/app/src/main/java/com/phenix/wirelessadb/ui/HelpFragment.kt index e6b8dff..a9967aa 100644 --- a/app/src/main/java/com/phenix/wirelessadb/ui/HelpFragment.kt +++ b/app/src/main/java/com/phenix/wirelessadb/ui/HelpFragment.kt @@ -16,6 +16,7 @@ import com.phenix.wirelessadb.databinding.FragmentHelpBinding import com.phenix.wirelessadb.theme.AccentColor import com.phenix.wirelessadb.theme.ThemeManager import com.phenix.wirelessadb.theme.ThemeMode +import com.phenix.wirelessadb.util.applyBottomSystemBarInsets class HelpFragment : Fragment() { @@ -33,6 +34,8 @@ class HelpFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // Edge-to-edge: keep scrollable content clear of the gesture/navigation bar + view.applyBottomSystemBarInsets() setupViews() } @@ -105,7 +108,7 @@ class HelpFragment : Fragment() { private fun setupAccentColorButtons() { binding.apply { - val accentButtons = listOf( + val accentButtons = mutableListOf( accentBlue to AccentColor.BLUE, accentTeal to AccentColor.TEAL, accentPurple to AccentColor.PURPLE, @@ -114,19 +117,28 @@ class HelpFragment : Fragment() { accentGreen to AccentColor.GREEN ) + // Material You option only exists where the system provides dynamic color + if (ThemeManager.isDynamicColorAvailable()) { + accentDynamic.visibility = View.VISIBLE + accentDynamic.backgroundTintList = ContextCompat.getColorStateList( + requireContext(), android.R.color.system_accent1_500 + ) + accentButtons.add(accentDynamic to AccentColor.DYNAMIC) + } + // Set up click handlers and initial selection indicator val currentAccent = ThemeManager.getAccentColor(requireContext()) accentButtons.forEach { (button, color) -> // Add check icon to currently selected color - updateAccentButtonSelection(button, color == currentAccent) + updateAccentButtonSelection(button, color, color == currentAccent) button.setOnClickListener { ThemeManager.setAccentColor(requireContext(), color) // Update selection indicators accentButtons.forEach { (btn, clr) -> - updateAccentButtonSelection(btn, clr == color) + updateAccentButtonSelection(btn, clr, clr == color) } // Recreate activity to apply new accent color immediately @@ -136,11 +148,15 @@ class HelpFragment : Fragment() { } } - private fun updateAccentButtonSelection(button: MaterialButton, isSelected: Boolean) { + private fun updateAccentButtonSelection( + button: MaterialButton, + accent: AccentColor, + isSelected: Boolean + ) { if (isSelected) { button.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_check) button.iconSize = resources.getDimensionPixelSize(R.dimen.accent_check_size) - button.iconTint = ContextCompat.getColorStateList(requireContext(), R.color.accent_on_primary) + button.iconTint = ContextCompat.getColorStateList(requireContext(), accent.onPrimary) } else { button.icon = null button.iconSize = 0 diff --git a/app/src/main/java/com/phenix/wirelessadb/ui/StatisticsFragment.kt b/app/src/main/java/com/phenix/wirelessadb/ui/StatisticsFragment.kt index fde66c7..e79f7c2 100644 --- a/app/src/main/java/com/phenix/wirelessadb/ui/StatisticsFragment.kt +++ b/app/src/main/java/com/phenix/wirelessadb/ui/StatisticsFragment.kt @@ -10,6 +10,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.phenix.wirelessadb.R import com.phenix.wirelessadb.databinding.FragmentStatisticsBinding import com.phenix.wirelessadb.viewmodel.StatisticsViewModel +import com.phenix.wirelessadb.util.applyBottomSystemBarInsets /** * TAB 3: STATISTICS (v1.3.0) @@ -39,6 +40,8 @@ class StatisticsFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // Edge-to-edge: keep scrollable content clear of the gesture/navigation bar + view.applyBottomSystemBarInsets() setupViews() observeViewModel() } diff --git a/app/src/main/java/com/phenix/wirelessadb/util/InsetsExtensions.kt b/app/src/main/java/com/phenix/wirelessadb/util/InsetsExtensions.kt new file mode 100644 index 0000000..5618d52 --- /dev/null +++ b/app/src/main/java/com/phenix/wirelessadb/util/InsetsExtensions.kt @@ -0,0 +1,54 @@ +package com.phenix.wirelessadb.util + +import android.view.View +import androidx.core.graphics.Insets +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat + +/** + * Edge-to-edge window inset helpers. + * + * The app disables decor inset fitting (WindowCompat.setDecorFitsSystemWindows(window, false)) + * and consumes insets explicitly via these helpers instead of android:fitsSystemWindows. + * This keeps layouts correct on every version, including Android 15's enforced + * edge-to-edge, and on devices with display cutouts. + */ + +private val systemBarTypes = + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + +/** + * Pads this view by the status bar (top) and cutout/system-bar side insets, + * preserving the padding declared in the layout. Use on top app bars. + */ +fun View.applyTopSystemBarInsets() { + applySystemBarInsets(top = true) +} + +/** + * Pads this view by the navigation bar (bottom) and cutout/system-bar side insets, + * preserving the padding declared in the layout. Use on scrolling content roots + * together with clipToPadding=false so content scrolls under the gesture bar + * but never rests hidden behind it. + */ +fun View.applyBottomSystemBarInsets() { + applySystemBarInsets(bottom = true) +} + +private fun View.applySystemBarInsets(top: Boolean = false, bottom: Boolean = false) { + val initialLeft = paddingLeft + val initialTop = paddingTop + val initialRight = paddingRight + val initialBottom = paddingBottom + + ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets -> + val insets: Insets = windowInsets.getInsets(systemBarTypes) + view.setPadding( + initialLeft + insets.left, + initialTop + if (top) insets.top else 0, + initialRight + insets.right, + initialBottom + if (bottom) insets.bottom else 0 + ) + windowInsets + } +} diff --git a/app/src/main/java/com/phenix/wirelessadb/util/QrCodeGenerator.kt b/app/src/main/java/com/phenix/wirelessadb/util/QrCodeGenerator.kt index 2b75584..cdf8797 100644 --- a/app/src/main/java/com/phenix/wirelessadb/util/QrCodeGenerator.kt +++ b/app/src/main/java/com/phenix/wirelessadb/util/QrCodeGenerator.kt @@ -129,7 +129,8 @@ object QrCodeGenerator { canvas.drawRoundRect(bgRect, 8f, 8f, paint) // Draw logo with rounded corners - val roundedLogo = Bitmap.createBitmap(logoSize, logoSize, logo.config) + // getConfig() is nullable (hardware bitmaps have no config); ARGB_8888 is the safe default + val roundedLogo = Bitmap.createBitmap(logoSize, logoSize, logo.config ?: Bitmap.Config.ARGB_8888) val canvas2 = Canvas(roundedLogo) val paint2 = Paint().apply { isAntiAlias = true diff --git a/app/src/main/res/drawable/ic_launcher.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml similarity index 81% rename from app/src/main/res/drawable/ic_launcher.xml rename to app/src/main/res/drawable/ic_launcher_foreground.xml index ff12480..63beec4 100644 --- a/app/src/main/res/drawable/ic_launcher.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,4 +1,6 @@ + - diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index c6e64de..f02340f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,14 +6,12 @@ android:id="@+id/rootLayout" android:layout_width="match_parent" android:layout_height="match_parent" - android:fitsSystemWindows="true" android:importantForAccessibility="yes"> + + + diff --git a/app/src/main/res/layout/fragment_statistics.xml b/app/src/main/res/layout/fragment_statistics.xml index 83af77e..7fec800 100644 --- a/app/src/main/res/layout/fragment_statistics.xml +++ b/app/src/main/res/layout/fragment_statistics.xml @@ -13,6 +13,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" + android:clipToPadding="false" android:fillViewport="true"> + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..370b2a4 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/values-night/colors_accents.xml b/app/src/main/res/values-night/colors_accents.xml new file mode 100644 index 0000000..67e0e43 --- /dev/null +++ b/app/src/main/res/values-night/colors_accents.xml @@ -0,0 +1,46 @@ + + + + + + #A6C8FF + #84AFFF + #003060 + #0E4A8E + #D5E3FF + + + #7ED9CB + #5EC2B3 + #003731 + #00504A + #9CF2E2 + + + #E5B3FF + #D897FF + #470076 + #641E8C + #F5D9FF + + + #FFB77C + #FFA05C + #4D2600 + #6D3800 + #FFDCC2 + + + #FFB1C4 + #FF8FAC + #66002B + #8F0A45 + #FFD9E1 + + + #9BD693 + #7FC178 + #0A3910 + #205426 + #B6F2AF + diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml deleted file mode 100644 index 11ca93e..0000000 --- a/app/src/main/res/values-v31/themes.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ba7861f..be11174 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,5 +1,8 @@ + + #6200EE + diff --git a/app/src/main/res/values/colors_accents.xml b/app/src/main/res/values/colors_accents.xml index 286fdee..d09d710 100644 --- a/app/src/main/res/values/colors_accents.xml +++ b/app/src/main/res/values/colors_accents.xml @@ -1,31 +1,49 @@ - + #1976D2 #1565C0 + #FFFFFF + #D5E3FF + #001B3D #00897B #00796B + #FFFFFF + #9CF2E2 + #00201B #7B1FA2 #6A1B9A + #FFFFFF + #F5D9FF + #2D0050 - + #F57C00 #EF6C00 + #331200 + #FFDCC2 + #2E1500 #D81B60 #C2185B + #FFFFFF + #FFD9E1 + #3F0018 #388E3C #2E7D32 + #FFFFFF + #B6F2AF + #002204 - + #FFFFFF diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 17eddc6..0b24755 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -193,6 +193,7 @@ Orange Pink Green + Dynamic (Material You wallpaper colors) ADB Pairing diff --git a/app/src/main/res/values/themes_accent_overlays.xml b/app/src/main/res/values/themes_accent_overlays.xml new file mode 100644 index 0000000..ff1e117 --- /dev/null +++ b/app/src/main/res/values/themes_accent_overlays.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/test/java/com/phenix/wirelessadb/theme/AccentColorTest.kt b/app/src/test/java/com/phenix/wirelessadb/theme/AccentColorTest.kt index 6224de8..1f26be4 100644 --- a/app/src/test/java/com/phenix/wirelessadb/theme/AccentColorTest.kt +++ b/app/src/test/java/com/phenix/wirelessadb/theme/AccentColorTest.kt @@ -35,6 +35,11 @@ class AccentColorTest { assertEquals(AccentColor.GREEN, AccentColor.fromOrdinal(5)) } + @Test + fun `fromOrdinal returns DYNAMIC for ordinal 6`() { + assertEquals(AccentColor.DYNAMIC, AccentColor.fromOrdinal(6)) + } + @Test fun `fromOrdinal returns DEFAULT for negative ordinal`() { assertEquals(AccentColor.DEFAULT, AccentColor.fromOrdinal(-1)) @@ -42,7 +47,7 @@ class AccentColorTest { @Test fun `fromOrdinal returns DEFAULT for out of bounds ordinal`() { - assertEquals(AccentColor.DEFAULT, AccentColor.fromOrdinal(6)) + assertEquals(AccentColor.DEFAULT, AccentColor.fromOrdinal(7)) assertEquals(AccentColor.DEFAULT, AccentColor.fromOrdinal(100)) } @@ -59,12 +64,14 @@ class AccentColorTest { assertEquals(3, AccentColor.ORANGE.ordinal) assertEquals(4, AccentColor.PINK.ordinal) assertEquals(5, AccentColor.GREEN.ordinal) + // DYNAMIC must stay last: prefs persist the ordinal + assertEquals(6, AccentColor.DYNAMIC.ordinal) } @Test - fun `entries contains all 6 colors`() { + fun `entries contains all 7 accents`() { val entries = AccentColor.entries - assertEquals(6, entries.size) + assertEquals(7, entries.size) } @Test @@ -83,6 +90,7 @@ class AccentColorTest { assertEquals("Orange", AccentColor.ORANGE.displayName) assertEquals("Pink", AccentColor.PINK.displayName) assertEquals("Green", AccentColor.GREEN.displayName) + assertEquals("Dynamic", AccentColor.DYNAMIC.displayName) } @Test diff --git a/plans/260715-0450-cross-version-ui-ux/plan.md b/plans/260715-0450-cross-version-ui-ux/plan.md new file mode 100644 index 0000000..0fba984 --- /dev/null +++ b/plans/260715-0450-cross-version-ui-ux/plan.md @@ -0,0 +1,41 @@ +# Cross-Version UI/UX Improvements (Android 8.0 → 15+) + +**Status:** Complete — all phases implemented; `assembleDebug`, `testDebugUnitTest`, `lintDebug` pass (changes uncommitted) +**Branch:** PhenixStar/improve-UI-UX-against-all-android-versions + +## Problems found + +1. `values-v31/themes.xml` hardcodes `system_neutral1_900` (near-black) background into a DayNight theme + `windowLightStatusBar=true` → broken light mode on Android 12+; also kills accent feature on 31+. +2. Accent color feature is a no-op: pref saved, `recreate()` called, but no theme overlay ever applied. +3. Edge-to-edge half-wired: decor fitting disabled + transparent bars, but no inset listeners; relies on `fitsSystemWindows` (breaks under Android 15 enforced edge-to-edge); fragment scroll bottoms hidden under gesture nav bar. +4. API 26: transparent nav bar with no light-nav-icon support → invisible nav buttons in light mode. +5. No adaptive launcher icon (legacy letterboxed icon on all launchers), no monochrome layer for Android 13+ themed icons. +6. No predictive back opt-in (Android 13/14+). + +## Phases + +### Phase 1 — Theme correctness +- Delete broken `values-v31/themes.xml` base override. +- Add per-accent `ThemeOverlay` styles (values + values-night, M3 tonal primary/container). +- `ThemeManager.applyAccent(activity)` applies overlay before `setContentView`; new `DYNAMIC` accent uses `DynamicColors.applyToActivityIfAvailable` (Material You, API 31+, restores intent of deleted v31 file, correctly for both modes). +- HelpFragment: wire Dynamic button (hidden below API 31). +- API 26 nav bar scrim set programmatically. + +### Phase 2 — Edge-to-edge insets (8.0 → 15+ proof) +- `util/InsetsExtensions.kt`: status-bar top padding, nav-bar/cutout bottom padding helpers via `ViewCompat`. +- Remove `fitsSystemWindows` from `activity_main.xml`; pad AppBarLayout top via listener. +- Fragments: `clipToPadding=false` + bottom inset padding on scroll roots. + +### Phase 3 — Launcher icon +- Adaptive icon (`mipmap-anydpi-v26`) with foreground/background split + monochrome (13+ themed icons). Manifest → `@mipmap/ic_launcher`. + +### Phase 4 — Platform behaviors +- `android:enableOnBackInvokedCallback="true"` (no custom back handling exists — safe). + +### Phase 5 — Verify +- `lintDebug`, `testDebugUnitTest`, `assembleDebug`. Update CHANGELOG. + +### Phase 6 — targetSdk 35 (added on user request) +- compileSdk/targetSdk 34 → 35, manifest tools:targetApi 35. +- Fixed SDK 35 nullability change: `Bitmap.getConfig()` now nullable (QrCodeGenerator). +- Behavior review: enforced edge-to-edge covered by Phase 2 insets; FGS specialUse subtype property already present; CONNECTIVITY_ACTION is an exempt system broadcast.