From 5f22302f65a7aed8913b07922b91448d8f003b9e Mon Sep 17 00:00:00 2001 From: Phenix Date: Wed, 15 Jul 2026 07:45:03 +0800 Subject: [PATCH 1/5] fix(theme): apply accent colors for real and repair Android 12+ theming Accent selection previously saved a preference and recreated the activity without ever changing the theme. Each accent now applies a Material 3 tonal overlay (night-qualified palettes cover light and dark). Adds a Dynamic (Material You) accent on Android 12+ and removes the values-v31 theme that hardcoded system_neutral1_900 into a DayNight theme, which rendered a near-black background in light mode on Android 12+. Orange uses a dark on-primary color: white on #F57C00 fails WCAG contrast. --- .../phenix/wirelessadb/theme/AccentColor.kt | 38 +++++++-- .../phenix/wirelessadb/theme/ThemeManager.kt | 26 ++++++ .../com/phenix/wirelessadb/ui/HelpFragment.kt | 23 +++-- app/src/main/res/layout/fragment_help.xml | 12 +++ .../main/res/values-night/colors_accents.xml | 46 ++++++++++ app/src/main/res/values-v31/themes.xml | 83 ------------------- app/src/main/res/values/colors_accents.xml | 24 +++++- app/src/main/res/values/strings.xml | 1 + .../res/values/themes_accent_overlays.xml | 48 +++++++++++ .../wirelessadb/theme/AccentColorTest.kt | 14 +++- 10 files changed, 213 insertions(+), 102 deletions(-) create mode 100644 app/src/main/res/values-night/colors_accents.xml delete mode 100644 app/src/main/res/values-v31/themes.xml create mode 100644 app/src/main/res/values/themes_accent_overlays.xml diff --git a/app/src/main/java/com/phenix/wirelessadb/theme/AccentColor.kt b/app/src/main/java/com/phenix/wirelessadb/theme/AccentColor.kt index d2182de..8247ff8 100644 --- a/app/src/main/java/com/phenix/wirelessadb/theme/AccentColor.kt +++ b/app/src/main/java/com/phenix/wirelessadb/theme/AccentColor.kt @@ -1,59 +1,81 @@ package com.phenix.wirelessadb.theme import androidx.annotation.ColorRes +import androidx.annotation.StyleRes import com.phenix.wirelessadb.R /** * Accent color presets for the app theme (v1.2.0). * - * Each accent defines primary and secondary colors for consistent theming. + * Each accent defines a tonal palette (night-qualified color resources) and a + * theme overlay applied at Activity creation by [ThemeManager.applyAccent]. + * [DYNAMIC] uses Material You wallpaper colors on Android 12+ instead of an overlay. */ enum class AccentColor( val displayName: String, @ColorRes val primaryColor: Int, @ColorRes val primaryVariant: Int, - @ColorRes val onPrimary: Int + @ColorRes val onPrimary: Int, + @StyleRes val themeOverlay: Int ) { BLUE( displayName = "Blue", primaryColor = R.color.accent_blue_primary, primaryVariant = R.color.accent_blue_variant, - onPrimary = R.color.accent_on_primary + onPrimary = R.color.accent_blue_on_primary, + themeOverlay = R.style.ThemeOverlay_WirelessADB_Accent_Blue ), TEAL( displayName = "Teal", primaryColor = R.color.accent_teal_primary, primaryVariant = R.color.accent_teal_variant, - onPrimary = R.color.accent_on_primary + onPrimary = R.color.accent_teal_on_primary, + themeOverlay = R.style.ThemeOverlay_WirelessADB_Accent_Teal ), PURPLE( displayName = "Purple", primaryColor = R.color.accent_purple_primary, primaryVariant = R.color.accent_purple_variant, - onPrimary = R.color.accent_on_primary + onPrimary = R.color.accent_purple_on_primary, + themeOverlay = R.style.ThemeOverlay_WirelessADB_Accent_Purple ), ORANGE( displayName = "Orange", primaryColor = R.color.accent_orange_primary, primaryVariant = R.color.accent_orange_variant, - onPrimary = R.color.accent_on_primary + onPrimary = R.color.accent_orange_on_primary, + themeOverlay = R.style.ThemeOverlay_WirelessADB_Accent_Orange ), PINK( displayName = "Pink", primaryColor = R.color.accent_pink_primary, primaryVariant = R.color.accent_pink_variant, - onPrimary = R.color.accent_on_primary + onPrimary = R.color.accent_pink_on_primary, + themeOverlay = R.style.ThemeOverlay_WirelessADB_Accent_Pink ), GREEN( displayName = "Green", primaryColor = R.color.accent_green_primary, primaryVariant = R.color.accent_green_variant, - onPrimary = R.color.accent_on_primary + onPrimary = R.color.accent_green_on_primary, + themeOverlay = R.style.ThemeOverlay_WirelessADB_Accent_Green + ), + + /** + * Material You wallpaper-based colors (Android 12+ only). Falls back to + * [DEFAULT] on older devices. Keep last: prefs store the ordinal. + */ + DYNAMIC( + displayName = "Dynamic", + primaryColor = R.color.accent_teal_primary, + primaryVariant = R.color.accent_teal_variant, + onPrimary = R.color.accent_on_primary, + themeOverlay = 0 ); companion object { diff --git a/app/src/main/java/com/phenix/wirelessadb/theme/ThemeManager.kt b/app/src/main/java/com/phenix/wirelessadb/theme/ThemeManager.kt index fc2a9cc..0a6b526 100644 --- a/app/src/main/java/com/phenix/wirelessadb/theme/ThemeManager.kt +++ b/app/src/main/java/com/phenix/wirelessadb/theme/ThemeManager.kt @@ -1,8 +1,11 @@ package com.phenix.wirelessadb.theme +import android.app.Activity import android.content.Context import android.content.res.Configuration +import android.os.Build import androidx.appcompat.app.AppCompatDelegate +import com.google.android.material.color.DynamicColors import com.phenix.wirelessadb.PrefsManager /** @@ -48,6 +51,29 @@ object ThemeManager { return PrefsManager.getThemeMode(context) } + /** + * Apply the saved accent color to an Activity's theme. + * Call after super.onCreate() and before setContentView(). + * + * DYNAMIC uses Material You wallpaper colors (Android 12+); other accents + * apply a static tonal overlay that works on every supported version. + */ + fun applyAccent(activity: Activity) { + val accent = getAccentColor(activity) + if (accent == AccentColor.DYNAMIC && isDynamicColorAvailable()) { + DynamicColors.applyToActivityIfAvailable(activity) + } else { + val effective = if (accent == AccentColor.DYNAMIC) AccentColor.DEFAULT else accent + activity.theme.applyStyle(effective.themeOverlay, true) + } + } + + /** + * Whether Material You dynamic color is available on this device. + */ + fun isDynamicColorAvailable(): Boolean = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && DynamicColors.isDynamicColorAvailable() + /** * Get current accent color. */ 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..612ccf0 100644 --- a/app/src/main/java/com/phenix/wirelessadb/ui/HelpFragment.kt +++ b/app/src/main/java/com/phenix/wirelessadb/ui/HelpFragment.kt @@ -105,7 +105,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 +114,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 +145,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/res/layout/fragment_help.xml b/app/src/main/res/layout/fragment_help.xml index dc1af0c..036b584 100644 --- a/app/src/main/res/layout/fragment_help.xml +++ b/app/src/main/res/layout/fragment_help.xml @@ -4,6 +4,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/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_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 From f65241894149a0a47ab3ae764224a5af00d61620 Mon Sep 17 00:00:00 2001 From: Phenix Date: Wed, 15 Jul 2026 07:45:14 +0800 Subject: [PATCH 2/5] fix(ui): handle edge-to-edge insets explicitly across all Android versions The app disabled decor inset fitting and made system bars transparent but never consumed insets, relying on fitsSystemWindows - which Android 15's enforced edge-to-edge ignores. The app bar now pads by the status bar and cutout insets, and each fragment's scroll root pads by the navigation bar inset (clipToPadding=false), so content no longer rests behind the gesture bar. Insets are returned unconsumed so every ViewPager2 page receives them. Also wires ThemeManager.applyAccent into MainActivity and adds a translucent nav-bar scrim on API 26, where dark nav icons are unsupported and a transparent bar would leave white buttons invisible on light content. --- .../com/phenix/wirelessadb/MainActivity.kt | 17 ++++++ .../phenix/wirelessadb/ui/ControlFragment.kt | 3 ++ .../phenix/wirelessadb/ui/DevicesFragment.kt | 3 ++ .../com/phenix/wirelessadb/ui/HelpFragment.kt | 3 ++ .../wirelessadb/ui/StatisticsFragment.kt | 3 ++ .../wirelessadb/util/InsetsExtensions.kt | 54 +++++++++++++++++++ app/src/main/res/layout/activity_main.xml | 2 - app/src/main/res/layout/fragment_control.xml | 1 + .../main/res/layout/fragment_statistics.xml | 1 + 9 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/phenix/wirelessadb/util/InsetsExtensions.kt diff --git a/app/src/main/java/com/phenix/wirelessadb/MainActivity.kt b/app/src/main/java/com/phenix/wirelessadb/MainActivity.kt index 41c38c3..413c074 100644 --- a/app/src/main/java/com/phenix/wirelessadb/MainActivity.kt +++ b/app/src/main/java/com/phenix/wirelessadb/MainActivity.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager +import android.graphics.Color import android.net.ConnectivityManager import android.os.Build import android.os.Bundle @@ -21,6 +22,7 @@ import com.phenix.wirelessadb.databinding.ActivityMainBinding import com.phenix.wirelessadb.theme.ThemeManager import com.phenix.wirelessadb.ui.MainPagerAdapter import com.phenix.wirelessadb.util.AccessibilityHelper +import com.phenix.wirelessadb.util.applyTopSystemBarInsets import com.phenix.wirelessadb.util.isReducedMotionEnabled import com.phenix.wirelessadb.viewmodel.AdbViewModel @@ -50,11 +52,26 @@ class MainActivity : AppCompatActivity() { ThemeManager.applyTheme(this) super.onCreate(savedInstanceState) + + // Apply accent color overlay (or Material You dynamic color) before inflating views + ThemeManager.applyAccent(this) + WindowCompat.setDecorFitsSystemWindows(window, false) + // API 26 can't render dark nav-bar icons (windowLightNavigationBar is API 27+), + // so a fully transparent nav bar would leave white buttons invisible on light + // content. Use a translucent scrim there instead. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) { + @Suppress("DEPRECATION") + window.navigationBarColor = Color.argb(0x66, 0, 0, 0) + } + binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + // Edge-to-edge: app bar absorbs the status bar / cutout insets + binding.appBarLayout.applyTopSystemBarInsets() + setSupportActionBar(binding.toolbar) requestNotificationPermission() 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 612ccf0..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() } 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/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"> Date: Wed, 15 Jul 2026 07:45:27 +0800 Subject: [PATCH 3/5] feat(ui): adaptive launcher icon, themed icon, and predictive back Replaces the legacy vector icon (letterboxed by launcher masks since API 26) with an adaptive icon: white ADB glyph foreground over the brand purple background, plus a monochrome layer for Android 13+ themed icons. Opts into the predictive back gesture; no code overrides back handling. --- app/src/main/AndroidManifest.xml | 7 ++++--- .../{ic_launcher.xml => ic_launcher_foreground.xml} | 5 ++--- app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 6 ++++++ app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml | 6 ++++++ app/src/main/res/values/colors.xml | 3 +++ 5 files changed, 21 insertions(+), 6 deletions(-) rename app/src/main/res/drawable/{ic_launcher.xml => ic_launcher_foreground.xml} (81%) create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml 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"> + - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..370b2a4 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + 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/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 + From 03abbab7030e384474261b049a93471e5c17dcc2 Mon Sep 17 00:00:00 2001 From: Phenix Date: Wed, 15 Jul 2026 07:45:27 +0800 Subject: [PATCH 4/5] feat(build): target and compile against SDK 35 (Android 15) Enforced edge-to-edge is covered by the explicit inset listeners, the foreground service already declares its specialUse subtype, and the connectivity receiver is an exempt system broadcast. Bitmap.getConfig() is nullable in SDK 35 (hardware bitmaps); QR logo rendering falls back to ARGB_8888. --- app/build.gradle.kts | 4 ++-- .../main/java/com/phenix/wirelessadb/util/QrCodeGenerator.kt | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c68cf88..ae66712 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 = 5 versionName = "1.2.0" } 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 From 5890df2d6ef004a781a18c7401c99cb9f70d3248 Mon Sep 17 00:00:00 2001 From: Phenix Date: Wed, 15 Jul 2026 07:45:27 +0800 Subject: [PATCH 5/5] docs: record cross-version UI/UX fixes and SDK 35 bump --- CHANGELOG.md | 24 +++++++++++ README.md | 4 +- plans/260715-0450-cross-version-ui-ux/plan.md | 41 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 plans/260715-0450-cross-version-ui-ux/plan.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 067b0b1..df1abd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ 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+) + ### Security - Warpgate SSH password is now stored in `EncryptedSharedPreferences` (Android Keystore) instead of plaintext prefs; existing stored passwords @@ -14,6 +26,18 @@ All notable changes to RootADB Pro will be documented in this file. - Persistent P2P token lookup now uses constant-time comparison ### 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 - Legacy (v1.0) trusted-device records no longer deserialize with a null device ID (Gson bypasses Kotlin null-safety); records are now routed by JSON shape and old entries parse correctly instead of crashing on access 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/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.