feat: implement app name presets and enhance password management UI

This commit is contained in:
Sarto
2026-04-15 21:48:51 +03:30
parent 99f63f2e8e
commit 4968bd191e
4 changed files with 309 additions and 223 deletions
+3 -13
View File
@@ -27,23 +27,13 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask" />
<!-- Separate alias for launcher entry so it can be disabled
when the user applies a custom icon/name shortcut -->
<activity-alias
android:name=".DefaultLauncher"
android:targetActivity=".MainActivity"
android:exported="true"
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name">
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
</activity>
</application>
</manifest>
@@ -1,19 +1,8 @@
package com.thefeed.android
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Base64
import android.webkit.JavascriptInterface
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import java.io.File
import java.io.FileOutputStream
import java.security.MessageDigest
class AndroidBridge(private val activity: Activity) {
@@ -27,94 +16,25 @@ class AndroidBridge(private val activity: Activity) {
@JavascriptInterface
fun isAndroid(): Boolean = true
/**
* Change the app's launcher icon and name.
* If iconBase64 is non-empty, disables the default launcher alias and
* pins a new shortcut with the custom image — the result looks like
* the app itself changed.
* If iconBase64 is empty, only saves the custom name (used on lock screen).
*/
/** Set a preset display name for the app (shown on lock screen). */
@JavascriptInterface
fun setAppIdentity(name: String, iconBase64: String): Boolean {
return try {
prefs.edit().putString(PREF_CUSTOM_APP_NAME, name).apply()
if (iconBase64.isBlank()) return true // name-only change
val raw = if (iconBase64.contains(",")) iconBase64.substringAfter(",") else iconBase64
val bytes = Base64.decode(raw, Base64.DEFAULT)
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return false
val iconFile = File(activity.filesDir, "custom_icon.png")
FileOutputStream(iconFile).use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
}
prefs.edit().putString(PREF_CUSTOM_ICON_PATH, iconFile.absolutePath).apply()
// Create pinned shortcut with custom icon/name
val icon = IconCompat.createWithBitmap(bitmap)
val shortcut = ShortcutInfoCompat.Builder(activity, "custom_launcher")
.setShortLabel(name)
.setLongLabel(name)
.setIcon(icon)
.setIntent(
Intent(activity, MainActivity::class.java).apply {
action = Intent.ACTION_MAIN
}
)
.build()
ShortcutManagerCompat.requestPinShortcut(activity, shortcut, null)
// Hide original launcher icon
activity.packageManager.setComponentEnabledSetting(
ComponentName(activity, "${activity.packageName}.DefaultLauncher"),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP
)
true
} catch (_: Exception) {
false
}
fun setPresetName(name: String) {
prefs.edit().putString(PREF_CUSTOM_APP_NAME, name).apply()
}
/** Returns the custom display name, or empty string if using defaults. */
/** Returns the preset display name, or empty string if using defaults. */
@JavascriptInterface
fun getCustomAppName(): String {
fun getPresetName(): String {
return prefs.getString(PREF_CUSTOM_APP_NAME, "") ?: ""
}
/** Returns the display name for the app — custom name if set, otherwise "thefeed". */
/** Returns the display name for the app — preset name if set, otherwise "thefeed". */
@JavascriptInterface
fun getAppDisplayName(): String {
val custom = prefs.getString(PREF_CUSTOM_APP_NAME, null)
return if (!custom.isNullOrBlank()) custom else "thefeed"
}
/** Restore original app icon and name. */
@JavascriptInterface
fun resetAppIdentity() {
prefs.edit()
.remove(PREF_CUSTOM_APP_NAME)
.remove(PREF_CUSTOM_ICON_PATH)
.apply()
val iconFile = File(activity.filesDir, "custom_icon.png")
if (iconFile.exists()) iconFile.delete()
// Restore original launcher icon
try {
activity.packageManager.setComponentEnabledSetting(
ComponentName(activity, "${activity.packageName}.DefaultLauncher"),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP
)
} catch (_: Exception) { }
// Remove dynamic shortcut
try {
ShortcutManagerCompat.removeDynamicShortcuts(activity, listOf("custom_launcher"))
} catch (_: Exception) { }
}
// ===== Language =====
@JavascriptInterface
@@ -163,7 +83,6 @@ class AndroidBridge(private val activity: Activity) {
companion object {
const val PREF_CUSTOM_APP_NAME = "custom_app_name"
const val PREF_CUSTOM_ICON_PATH = "custom_icon_path"
const val PREF_PASSWORD_HASH = "password_hash"
const val PREF_LANG = "app_lang"
}
@@ -67,11 +67,12 @@ class MainActivity : ComponentActivity() {
controller.isAppearanceLightNavigationBars = false
setContentView(R.layout.activity_main)
// Apply top inset as padding so content isn't hidden behind the status bar
// Apply insets so content isn't hidden behind the status bar or keyboard
val rootView = findViewById<View>(android.R.id.content)
ViewCompat.setOnApplyWindowInsetsListener(rootView) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(0, systemBars.top, 0, systemBars.bottom)
val ime = insets.getInsets(WindowInsetsCompat.Type.ime())
v.setPadding(0, systemBars.top, 0, maxOf(systemBars.bottom, ime.bottom))
insets
}
// Trigger inset dispatch explicitly — required on some older Android versions
@@ -98,6 +99,23 @@ class MainActivity : ComponentActivity() {
return prefs.getString(AndroidBridge.PREF_PASSWORD_HASH, null) != null
}
private fun resolvePresetName(key: String?, isPersian: Boolean): String {
if (key == null) return getString(R.string.app_name)
val presets = mapOf(
"weather" to ("Weather" to "آب و هوا"),
"calculator" to ("Calculator" to "ماشین‌حساب"),
"calendar" to ("Calendar" to "تقویم"),
"notes" to ("Notes" to "یادداشت"),
"clock" to ("Clock" to "ساعت"),
"camera" to ("Camera" to "دوربین"),
"compass" to ("Compass" to "قطب‌نما"),
"gallery" to ("Gallery" to "گالری"),
"recorder" to ("Recorder" to "ضبط صدا"),
)
val pair = presets[key] ?: return key
return if (isPersian) pair.second else pair.first
}
@SuppressLint("SetTextI18n")
private fun showLockScreen() {
lockScreenVisible = true
@@ -109,10 +127,11 @@ class MainActivity : ComponentActivity() {
val lockError = findViewById<TextView>(R.id.lockError)
val prefs = getSharedPreferences(ThefeedService.PREFS_NAME, Context.MODE_PRIVATE)
val appName = prefs.getString(AndroidBridge.PREF_CUSTOM_APP_NAME, null)
?.takeIf { it.isNotBlank() } ?: getString(R.string.app_name)
val lang = prefs.getString(AndroidBridge.PREF_LANG, "fa") ?: "fa"
val isPersian = lang == "fa"
val presetKey = prefs.getString(AndroidBridge.PREF_CUSTOM_APP_NAME, null)
?.takeIf { it.isNotBlank() }
val appName = resolvePresetName(presetKey, isPersian)
lockTitle.text = appName
lockSubtitle.text = if (isPersian) "رمز عبور را وارد کنید" else "Enter password to unlock"