From 640dd5de22a7e3eb297843ef14801ec0512f0d13 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Thu, 28 May 2026 18:21:38 +0500 Subject: [PATCH] theme customizations support --- .../java/sh/sar/basedbank/BasedBankApp.kt | 6 +- .../java/sh/sar/basedbank/LockActivity.kt | 5 + .../sh/sar/basedbank/ui/home/HomeActivity.kt | 5 + .../ui/home/SettingsAppearanceFragment.kt | 105 ++++++++++++++++++ .../sar/basedbank/ui/login/LoginActivity.kt | 5 + .../ui/onboarding/OnboardingActivity.kt | 5 + .../java/sh/sar/basedbank/util/ThemeHelper.kt | 78 +++++++++++++ .../layout/fragment_settings_appearance.xml | 81 ++++++++++++++ app/src/main/res/values/strings.xml | 9 ++ app/src/main/res/values/themes.xml | 28 +++++ 10 files changed, 326 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/sh/sar/basedbank/util/ThemeHelper.kt diff --git a/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt b/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt index 471385c..ac01955 100644 --- a/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt +++ b/app/src/main/java/sh/sar/basedbank/BasedBankApp.kt @@ -115,7 +115,11 @@ class BasedBankApp : Application() { override fun onCreate() { super.onCreate() - DynamicColors.applyToActivitiesIfAvailable(this) + // Only apply wallpaper-based dynamic colors in system theme mode. + // Light/dark modes use content-based accent colors applied per-activity via ThemeHelper. + DynamicColors.applyToActivitiesIfAvailable(this) { _, _ -> + getSharedPreferences("prefs", MODE_PRIVATE).getString("theme", "system") == "system" + } val theme = getSharedPreferences("prefs", MODE_PRIVATE).getString("theme", "system") AppCompatDelegate.setDefaultNightMode(when (theme) { diff --git a/app/src/main/java/sh/sar/basedbank/LockActivity.kt b/app/src/main/java/sh/sar/basedbank/LockActivity.kt index 98127d2..17aa162 100644 --- a/app/src/main/java/sh/sar/basedbank/LockActivity.kt +++ b/app/src/main/java/sh/sar/basedbank/LockActivity.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.withContext import sh.sar.basedbank.databinding.ActivityLockBinding import sh.sar.basedbank.ui.home.HomeActivity import sh.sar.basedbank.util.CredentialStore +import sh.sar.basedbank.util.ThemeHelper import sh.sar.basedbank.BasedBankApp import javax.crypto.SecretKeyFactory import javax.crypto.spec.PBEKeySpec @@ -46,6 +47,7 @@ class LockActivity : AppCompatActivity() { } override fun onCreate(savedInstanceState: Bundle?) { + ThemeHelper.applyAccent(this) super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) binding = ActivityLockBinding.inflate(layoutInflater) @@ -55,6 +57,9 @@ class LockActivity : AppCompatActivity() { isAppearanceLightStatusBars = isLight isAppearanceLightNavigationBars = isLight } + val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground)) + window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK) + ta.recycle() ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets -> val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) view.setPadding(bars.left, bars.top, bars.right, bars.bottom) diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt index 663efc7..c76c14b 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/HomeActivity.kt @@ -68,6 +68,7 @@ import sh.sar.basedbank.util.CredentialStore import sh.sar.basedbank.util.CardsCache import sh.sar.basedbank.util.FinancingCache import sh.sar.basedbank.util.ForeignLimitsCache +import sh.sar.basedbank.util.ThemeHelper class HomeActivity : AppCompatActivity() { @@ -106,6 +107,7 @@ class HomeActivity : AppCompatActivity() { } override fun onCreate(savedInstanceState: Bundle?) { + ThemeHelper.applyAccent(this) super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) binding = ActivityHomeBinding.inflate(layoutInflater) @@ -118,6 +120,9 @@ class HomeActivity : AppCompatActivity() { isAppearanceLightStatusBars = isLight isAppearanceLightNavigationBars = isLight } + val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground)) + window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK) + ta.recycle() // Auth guard: HomeActivity must only be reachable after LockActivity or fresh login. // Using loadSecurityHash() (EncryptedSharedPreferences) instead of plain prefs so // a rooted device cannot bypass this by editing security_method in plain prefs. diff --git a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsAppearanceFragment.kt b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsAppearanceFragment.kt index 0483371..4d2b23f 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/home/SettingsAppearanceFragment.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/home/SettingsAppearanceFragment.kt @@ -2,7 +2,9 @@ package sh.sar.basedbank.ui.home import android.content.Context import android.content.SharedPreferences +import android.graphics.Color import android.os.Bundle +import android.text.InputType import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -10,6 +12,7 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.ScrollView import android.widget.TextView +import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate.setApplicationLocales import androidx.core.os.LocaleListCompat @@ -18,8 +21,11 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout import sh.sar.basedbank.R import sh.sar.basedbank.databinding.FragmentSettingsAppearanceBinding +import sh.sar.basedbank.util.ThemeHelper import java.util.Collections class SettingsAppearanceFragment : Fragment() { @@ -103,8 +109,45 @@ class SettingsAppearanceFragment : Fragment() { } prefs.edit().putString("theme", key).apply() AppCompatDelegate.setDefaultNightMode(mode) + updateAccentState(key == "system") + updatePitchBlackState(key == "dark") } + // Pitch black + binding.switchPitchBlack.isChecked = prefs.getBoolean("pitch_black", false) + binding.switchPitchBlack.setOnCheckedChangeListener { _, checked -> + prefs.edit().putBoolean("pitch_black", checked).apply() + requireActivity().recreate() + } + val isDark = prefs.getString("theme", "system") == "dark" + updatePitchBlackState(isDark) + + // Accent color + val savedPreset = prefs.getString("accent_preset", ThemeHelper.PRESET_BLUE) + binding.accentToggle.check(when (savedPreset) { + ThemeHelper.PRESET_RED -> R.id.btnAccentOrange + ThemeHelper.PRESET_GREEN -> R.id.btnAccentGreen + ThemeHelper.PRESET_CUSTOM -> R.id.btnAccentCustom + else -> R.id.btnAccentBlue + }) + binding.accentToggle.addOnButtonCheckedListener { _, checkedId, isChecked -> + if (!isChecked) return@addOnButtonCheckedListener + val preset = when (checkedId) { + R.id.btnAccentOrange -> ThemeHelper.PRESET_RED + R.id.btnAccentGreen -> ThemeHelper.PRESET_GREEN + R.id.btnAccentCustom -> ThemeHelper.PRESET_CUSTOM + else -> ThemeHelper.PRESET_BLUE + } + if (preset == ThemeHelper.PRESET_CUSTOM) { + showCustomColorPicker() + } else { + prefs.edit().putString("accent_preset", preset).apply() + requireActivity().recreate() + } + } + val isSystem = prefs.getString("theme", "system") == "system" + updateAccentState(isSystem) + // Language val currentLocales = AppCompatDelegate.getApplicationLocales() val currentLang = if (currentLocales.isEmpty) "en" else currentLocales[0]?.language ?: "en" @@ -156,6 +199,68 @@ class SettingsAppearanceFragment : Fragment() { slotAdapter.notifyDataSetChanged() } + private fun updatePitchBlackState(isDark: Boolean) { + binding.rowPitchBlack.alpha = if (isDark) 1f else 0.38f + binding.switchPitchBlack.isEnabled = isDark + } + + private fun updateAccentState(isSystem: Boolean) { + binding.sectionAccentColor.alpha = if (isSystem) 0.38f else 1f + for (i in 0 until binding.accentToggle.childCount) { + binding.accentToggle.getChildAt(i)?.isEnabled = !isSystem + } + } + + private fun showCustomColorPicker() { + val ctx = requireContext() + val currentHex = prefs.getString("accent_custom_color", "") ?: "" + val inputLayout = TextInputLayout(ctx).apply { + hint = getString(R.string.accent_custom_hint) + val pad = (16 * resources.displayMetrics.density).toInt() + setPadding(pad, pad / 2, pad, 0) + } + val input = TextInputEditText(ctx).apply { + setText(currentHex) + inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS + setSingleLine(true) + } + inputLayout.addView(input) + + val dialog = MaterialAlertDialogBuilder(ctx) + .setTitle(R.string.accent_custom_pick) + .setView(inputLayout) + .setPositiveButton(android.R.string.ok, null) + .setNegativeButton(R.string.cancel) { _, _ -> revertAccentToggle() } + .setOnCancelListener { revertAccentToggle() } + .show() + + dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val raw = input.text.toString().trim() + val hex = if (raw.startsWith("#")) raw else "#$raw" + try { + Color.parseColor(hex) + prefs.edit() + .putString("accent_preset", ThemeHelper.PRESET_CUSTOM) + .putString("accent_custom_color", hex) + .apply() + dialog.dismiss() + requireActivity().recreate() + } catch (_: Exception) { + Toast.makeText(ctx, R.string.accent_invalid_color, Toast.LENGTH_SHORT).show() + } + } + } + + private fun revertAccentToggle() { + val saved = prefs.getString("accent_preset", ThemeHelper.PRESET_BLUE) + binding.accentToggle.check(when (saved) { + ThemeHelper.PRESET_RED -> R.id.btnAccentOrange + ThemeHelper.PRESET_GREEN -> R.id.btnAccentGreen + ThemeHelper.PRESET_CUSTOM -> R.id.btnAccentCustom + else -> R.id.btnAccentBlue + }) + } + private fun showItemPicker(items: MutableList, slotIndex: Int, adapter: NavItemAdapter) { val isBottom = prefs.getBoolean("bottom_nav", false) if (items === slots && !isBottom) return diff --git a/app/src/main/java/sh/sar/basedbank/ui/login/LoginActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/login/LoginActivity.kt index 4eb468e..660a6a7 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/login/LoginActivity.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/login/LoginActivity.kt @@ -9,12 +9,14 @@ import sh.sar.basedbank.BasedBankApp import sh.sar.basedbank.LockActivity import sh.sar.basedbank.databinding.ActivityLoginBinding import sh.sar.basedbank.util.CredentialStore +import sh.sar.basedbank.util.ThemeHelper class LoginActivity : AppCompatActivity() { private lateinit var binding: ActivityLoginBinding override fun onCreate(savedInstanceState: Bundle?) { + ThemeHelper.applyAccent(this) super.onCreate(savedInstanceState) // If security is configured and the user hasn't unlocked this session, @@ -34,5 +36,8 @@ class LoginActivity : AppCompatActivity() { isAppearanceLightStatusBars = isLight isAppearanceLightNavigationBars = isLight } + val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground)) + window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK) + ta.recycle() } } diff --git a/app/src/main/java/sh/sar/basedbank/ui/onboarding/OnboardingActivity.kt b/app/src/main/java/sh/sar/basedbank/ui/onboarding/OnboardingActivity.kt index 77254a5..a7261ab 100644 --- a/app/src/main/java/sh/sar/basedbank/ui/onboarding/OnboardingActivity.kt +++ b/app/src/main/java/sh/sar/basedbank/ui/onboarding/OnboardingActivity.kt @@ -19,6 +19,7 @@ import sh.sar.basedbank.R import sh.sar.basedbank.databinding.ActivityOnboardingBinding import sh.sar.basedbank.ui.login.LoginActivity import sh.sar.basedbank.util.CredentialStore +import sh.sar.basedbank.util.ThemeHelper class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback { @@ -27,6 +28,7 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback { private var countDownTimer: CountDownTimer? = null override fun onCreate(savedInstanceState: Bundle?) { + ThemeHelper.applyAccent(this) super.onCreate(savedInstanceState) // If security is already configured, onboarding is complete. Redirect to lock screen @@ -45,6 +47,9 @@ class OnboardingActivity : AppCompatActivity(), SecuritySetupFragment.Callback { isAppearanceLightStatusBars = isLight isAppearanceLightNavigationBars = isLight } + val ta = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground)) + window.statusBarColor = ta.getColor(0, if (isLight) android.graphics.Color.WHITE else android.graphics.Color.BLACK) + ta.recycle() prefs = getSharedPreferences("prefs", MODE_PRIVATE) val originalBottomPadding = binding.bottomBar.paddingBottom ViewCompat.setOnApplyWindowInsetsListener(binding.bottomBar) { view, insets -> diff --git a/app/src/main/java/sh/sar/basedbank/util/ThemeHelper.kt b/app/src/main/java/sh/sar/basedbank/util/ThemeHelper.kt new file mode 100644 index 0000000..ad1528c --- /dev/null +++ b/app/src/main/java/sh/sar/basedbank/util/ThemeHelper.kt @@ -0,0 +1,78 @@ +package sh.sar.basedbank.util + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color +import android.os.Build +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.color.DynamicColors +import com.google.android.material.color.DynamicColorsOptions +import sh.sar.basedbank.R + +object ThemeHelper { + + const val PRESET_BLUE = "blue" + const val PRESET_RED = "red" + const val PRESET_GREEN = "green" + const val PRESET_CUSTOM = "custom" + + fun isSystemTheme(context: Context): Boolean = + context.getSharedPreferences("prefs", Context.MODE_PRIVATE) + .getString("theme", "system") == "system" + + fun getAccentPreset(context: Context): String = + context.getSharedPreferences("prefs", Context.MODE_PRIVATE) + .getString("accent_preset", PRESET_BLUE) ?: PRESET_BLUE + + fun getCustomColor(context: Context): Int? { + val hex = context.getSharedPreferences("prefs", Context.MODE_PRIVATE) + .getString("accent_custom_color", null) ?: return null + return try { Color.parseColor(hex) } catch (_: Exception) { null } + } + + fun presetSeedColor(context: Context, preset: String): Int = when (preset) { + PRESET_RED -> Color.parseColor("#D32F2F") + PRESET_GREEN -> Color.parseColor("#4CAF50") + PRESET_CUSTOM -> getCustomColor(context) ?: Color.parseColor("#3F65AD") + else -> Color.parseColor("#3F65AD") + } + + fun isPitchBlackEnabled(context: Context): Boolean = + context.getSharedPreferences("prefs", Context.MODE_PRIVATE) + .getBoolean("pitch_black", false) + + /** + * Apply the user-chosen accent theme to the given activity. + * Must be called BEFORE super.onCreate() so window-level attributes + * (status bar color, etc.) are resolved from the correct overlay. + * Has no effect when the theme is set to "system" (dynamic colors are + * handled by BasedBankApp via DynamicColors.applyToActivitiesIfAvailable). + */ + fun applyAccent(activity: AppCompatActivity) { + if (isSystemTheme(activity)) return + val preset = getAccentPreset(activity) + val seed = presetSeedColor(activity, preset) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + bitmap.setPixel(0, 0, seed) + DynamicColors.applyToActivityIfAvailable( + activity, + DynamicColorsOptions.Builder().setContentBasedSource(bitmap).build() + ) + } else { + // API < 31: apply a simple style overlay (partial palette, but functional) + val styleRes = when (preset) { + PRESET_RED -> R.style.ThemeOverlay_Accent_Orange + PRESET_GREEN -> R.style.ThemeOverlay_Accent_Green + else -> R.style.ThemeOverlay_Accent_Blue + } + activity.theme.applyStyle(styleRes, true) + } + + val prefs = activity.getSharedPreferences("prefs", Context.MODE_PRIVATE) + val isDark = prefs.getString("theme", "system") == "dark" + if (isDark && prefs.getBoolean("pitch_black", false)) { + activity.theme.applyStyle(R.style.ThemeOverlay_PitchBlack, true) + } + } +} diff --git a/app/src/main/res/layout/fragment_settings_appearance.xml b/app/src/main/res/layout/fragment_settings_appearance.xml index 12d6416..d32740f 100644 --- a/app/src/main/res/layout/fragment_settings_appearance.xml +++ b/app/src/main/res/layout/fragment_settings_appearance.xml @@ -156,6 +156,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + System Light Dark + Pitch Black + Accent Color + Blue + Red + Green + Custom + Custom Accent Color + #RRGGBB hex color + Invalid color — enter a valid hex code Language English ދިވެހި diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index c47bc14..249db22 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -6,4 +6,32 @@ @color/seed_secondary adjustResize + + + + + + + + + +