mirror of
https://github.com/zhanghai/MaterialFiles
synced 2024-07-09 03:55:50 +00:00
parent
97c0a04aa3
commit
25d298aba6
|
@ -26,7 +26,7 @@ apply plugin: 'com.google.firebase.crashlytics'
|
|||
|
||||
android {
|
||||
namespace 'me.zhanghai.android.files'
|
||||
compileSdk 33
|
||||
compileSdk 34
|
||||
ndkVersion '25.2.9519653'
|
||||
buildToolsVersion = '33.0.2'
|
||||
defaultConfig {
|
||||
|
@ -111,7 +111,8 @@ dependencies {
|
|||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version"
|
||||
|
||||
implementation 'androidx.activity:activity-ktx:1.7.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
// Appcompat 1.7.0-alpha01 is required for properly changing locale below API 24 (b/243119645).
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0-alpha03'
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.drawerlayout:drawerlayout:1.2.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.6'
|
||||
|
|
|
@ -315,6 +315,15 @@
|
|||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="autoStoreLocales"
|
||||
android:value="true" />
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="me.zhanghai.android.files.app.AppProvider"
|
||||
android:authorities="@string/app_provider_authority"
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.compat
|
||||
|
||||
import android.app.LocaleConfig
|
||||
import android.content.Context
|
||||
import android.content.res.XmlResourceParser
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.XmlRes
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
/**
|
||||
* @see android.app.LocaleConfig
|
||||
*/
|
||||
class LocaleConfigCompat(context: Context) {
|
||||
var status = 0
|
||||
private set
|
||||
|
||||
var supportedLocales: LocaleListCompat? = null
|
||||
private set
|
||||
|
||||
init {
|
||||
val impl = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
Api33Impl(context)
|
||||
} else {
|
||||
Api21Impl(context)
|
||||
}
|
||||
status = impl.status
|
||||
supportedLocales = impl.supportedLocales
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Succeeded reading the LocaleConfig structure stored in an XML file.
|
||||
*/
|
||||
const val STATUS_SUCCESS = 0
|
||||
|
||||
/**
|
||||
* No android:localeConfig tag on <application>.
|
||||
*/
|
||||
const val STATUS_NOT_SPECIFIED = 1
|
||||
|
||||
/**
|
||||
* Malformed input in the XML file where the LocaleConfig was stored.
|
||||
*/
|
||||
const val STATUS_PARSING_FAILED = 2
|
||||
}
|
||||
|
||||
private abstract class Impl {
|
||||
abstract val status: Int
|
||||
abstract val supportedLocales: LocaleListCompat?
|
||||
}
|
||||
|
||||
private class Api21Impl(context: Context) : Impl() {
|
||||
override var status = 0
|
||||
private set
|
||||
|
||||
override var supportedLocales: LocaleListCompat? = null
|
||||
private set
|
||||
|
||||
init {
|
||||
val resourceId = try {
|
||||
getLocaleConfigResourceId(context)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "The resource file pointed to by the given resource ID isn't found.", e)
|
||||
}
|
||||
if (resourceId == ResourcesCompat.ID_NULL) {
|
||||
status = STATUS_NOT_SPECIFIED
|
||||
} else {
|
||||
val resources = context.resources
|
||||
try {
|
||||
supportedLocales = resources.getXml(resourceId).use { parseLocaleConfig(it) }
|
||||
status = STATUS_SUCCESS
|
||||
} catch (e: Exception) {
|
||||
val resourceEntryName = resources.getResourceEntryName(resourceId)
|
||||
Log.w(TAG, "Failed to parse XML configuration from $resourceEntryName", e)
|
||||
status = STATUS_PARSING_FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @see com.android.server.pm.pkg.parsing.ParsingPackageUtils
|
||||
@XmlRes
|
||||
private fun getLocaleConfigResourceId(context: Context): Int {
|
||||
var cookie = 1
|
||||
while (true) {
|
||||
val parser = try {
|
||||
context.assets.openXmlResourceParser(cookie, FILE_NAME_ANDROID_MANIFEST)
|
||||
} catch (e: FileNotFoundException) {
|
||||
break
|
||||
}
|
||||
parser.use {
|
||||
do {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
continue
|
||||
}
|
||||
if (parser.name != TAG_MANIFEST) {
|
||||
parser.skipCurrentTag()
|
||||
continue
|
||||
}
|
||||
if (parser.getAttributeValue(null, ATTR_PACKAGE) != context.packageName) {
|
||||
break
|
||||
}
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
continue
|
||||
}
|
||||
if (parser.name != TAG_APPLICATION) {
|
||||
parser.skipCurrentTag()
|
||||
continue
|
||||
}
|
||||
return parser.getAttributeResourceValue(
|
||||
NAMESPACE_ANDROID, ATTR_LOCALE_CONFIG, ResourcesCompat.ID_NULL
|
||||
)
|
||||
}
|
||||
} while (parser.next() != XmlPullParser.END_DOCUMENT)
|
||||
}
|
||||
++cookie
|
||||
}
|
||||
return ResourcesCompat.ID_NULL
|
||||
}
|
||||
|
||||
private fun parseLocaleConfig(parser: XmlResourceParser): LocaleListCompat {
|
||||
val localeNames = mutableSetOf<String>()
|
||||
do {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
continue
|
||||
}
|
||||
if (parser.name != TAG_LOCALE_CONFIG) {
|
||||
parser.skipCurrentTag()
|
||||
continue
|
||||
}
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG) {
|
||||
continue
|
||||
}
|
||||
if (parser.name != TAG_LOCALE) {
|
||||
parser.skipCurrentTag()
|
||||
continue
|
||||
}
|
||||
localeNames += parser.getAttributeValue(NAMESPACE_ANDROID, ATTR_NAME)
|
||||
parser.skipCurrentTag()
|
||||
}
|
||||
} while (parser.next() != XmlPullParser.END_DOCUMENT)
|
||||
return LocaleListCompat.forLanguageTags(localeNames.joinToString(","))
|
||||
}
|
||||
|
||||
private fun XmlPullParser.skipCurrentTag() {
|
||||
val outerDepth = depth
|
||||
var type: Int
|
||||
do {
|
||||
type = next()
|
||||
} while (type != XmlPullParser.END_DOCUMENT &&
|
||||
(type != XmlPullParser.END_TAG || depth > outerDepth))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LocaleConfigCompat"
|
||||
|
||||
private const val FILE_NAME_ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
|
||||
private const val TAG_APPLICATION = "application"
|
||||
private const val TAG_LOCALE_CONFIG = "locale-config"
|
||||
private const val TAG_LOCALE = "locale"
|
||||
private const val TAG_MANIFEST = "manifest"
|
||||
|
||||
private const val NAMESPACE_ANDROID = "http://schemas.android.com/apk/res/android"
|
||||
|
||||
private const val ATTR_LOCALE_CONFIG = "localeConfig"
|
||||
private const val ATTR_NAME = "name"
|
||||
private const val ATTR_PACKAGE = "package"
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private class Api33Impl(context: Context) : Impl() {
|
||||
override var status: Int = 0
|
||||
private set
|
||||
|
||||
override var supportedLocales: LocaleListCompat? = null
|
||||
private set
|
||||
|
||||
init {
|
||||
val platformLocaleConfig = LocaleConfig(context)
|
||||
status = platformLocaleConfig.status
|
||||
supportedLocales = platformLocaleConfig.supportedLocales
|
||||
?.let { LocaleListCompat.wrap(it) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.app.LocaleManagerCompat
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference.SummaryProvider
|
||||
import me.zhanghai.android.files.R
|
||||
import me.zhanghai.android.files.app.application
|
||||
import me.zhanghai.android.files.compat.LocaleConfigCompat
|
||||
import me.zhanghai.android.files.util.toList
|
||||
import java.util.Locale
|
||||
|
||||
class LocalePreference : ListPreference {
|
||||
lateinit var setApplicationLocalesPreApi33: (LocaleListCompat) -> Unit
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int) : super(
|
||||
context, attrs, defStyleAttr
|
||||
)
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
@AttrRes defStyleAttr: Int,
|
||||
@StyleRes defStyleRes: Int
|
||||
) : super(context, attrs, defStyleAttr, defStyleRes)
|
||||
|
||||
init {
|
||||
val context = context
|
||||
val systemDefaultEntry = context.getString(R.string.system_default)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// Prefer using the system setting because it has better support for locales.
|
||||
intent = Intent(
|
||||
Settings.ACTION_APP_LOCALE_SETTINGS,
|
||||
Uri.fromParts("package", context.packageName, null)
|
||||
)
|
||||
summaryProvider = SummaryProvider<LocalePreference> {
|
||||
applicationLocale?.sentenceCasedLocalizedDisplayName ?: systemDefaultEntry
|
||||
}
|
||||
} else {
|
||||
setDefaultValue(VALUE_SYSTEM_DEFAULT)
|
||||
val supportedLocales = LocaleConfigCompat(context).supportedLocales!!.toList()
|
||||
.sortedBy { it.toLanguageTag() }
|
||||
entries = supportedLocales.mapTo(mutableListOf(systemDefaultEntry)) {
|
||||
it.sentenceCasedLocalizedDisplayName
|
||||
}.toTypedArray<CharSequence>()
|
||||
entryValues =
|
||||
supportedLocales
|
||||
.mapTo(mutableListOf(VALUE_SYSTEM_DEFAULT)) { it.toLanguageTag() }
|
||||
.toTypedArray<CharSequence>()
|
||||
summaryProvider = SimpleSummaryProvider.getInstance()
|
||||
}
|
||||
}
|
||||
|
||||
private val Locale.sentenceCasedLocalizedDisplayName: String
|
||||
// See com.android.internal.app.LocaleHelper.toSentenceCase() for a proper case conversion
|
||||
// implementation which requires android.icu.text.CaseMap that's only available on API 29+.
|
||||
@Suppress("DEPRECATION")
|
||||
get() = getDisplayName(this).capitalize(this)
|
||||
|
||||
override fun getPersistedString(defaultReturnValue: String?): String =
|
||||
applicationLocale?.toLanguageTag() ?: VALUE_SYSTEM_DEFAULT
|
||||
|
||||
override fun persistString(value: String?): Boolean {
|
||||
applicationLocale = if (value != null && value != VALUE_SYSTEM_DEFAULT) {
|
||||
Locale.forLanguageTag(value)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private var applicationLocale: Locale?
|
||||
get() = LocaleManagerCompat.getApplicationLocales(application).toList().firstOrNull()
|
||||
set(value) {
|
||||
check(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
|
||||
if (value == applicationLocale) {
|
||||
return
|
||||
}
|
||||
val locales = if (value != null) {
|
||||
LocaleListCompat.create(value)
|
||||
} else {
|
||||
LocaleListCompat.getEmptyLocaleList()
|
||||
}
|
||||
setApplicationLocalesPreApi33(locales)
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
// Don't show dialog if we have an intent.
|
||||
if (intent != null) {
|
||||
return
|
||||
}
|
||||
|
||||
super.onClick()
|
||||
}
|
||||
|
||||
// Exposed for SettingsPreferenceFragment.onResume().
|
||||
public override fun notifyChanged() {
|
||||
super.notifyChanged()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VALUE_SYSTEM_DEFAULT = ""
|
||||
}
|
||||
}
|
|
@ -63,7 +63,7 @@ abstract class SettingLiveData<T>(
|
|||
defaultValue: T
|
||||
): T
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
if (key == this.key) {
|
||||
loadValue()
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import android.view.KeyEvent
|
|||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.fragment.app.add
|
||||
import androidx.fragment.app.commit
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
@ -40,6 +42,13 @@ class SettingsActivity : AppActivity(), OnThemeChangedListener, OnNightModeChang
|
|||
}
|
||||
}
|
||||
|
||||
fun setApplicationLocalesPreApi33(locales: LocaleListCompat) {
|
||||
// HACK: Prevent this activity from being recreated due to locale change.
|
||||
delegate.onDestroy()
|
||||
AppCompatDelegate.setApplicationLocales(locales)
|
||||
restart()
|
||||
}
|
||||
|
||||
override fun onThemeChanged(@StyleRes theme: Int) {
|
||||
// ActivityCompat.recreate() may call ActivityRecreator.recreate() without calling
|
||||
// Activity.recreate(), so we cannot simply override it. To work around this, we just
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package me.zhanghai.android.files.settings
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import me.zhanghai.android.files.R
|
||||
import me.zhanghai.android.files.theme.custom.CustomThemeHelper
|
||||
|
@ -14,6 +15,20 @@ import me.zhanghai.android.files.theme.night.NightModeHelper
|
|||
import me.zhanghai.android.files.ui.PreferenceFragmentCompat
|
||||
|
||||
class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
||||
private lateinit var localePreference: LocalePreference
|
||||
|
||||
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.settings)
|
||||
|
||||
localePreference = preferenceScreen.findPreference(getString(R.string.pref_key_locale))!!
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
localePreference.setApplicationLocalesPreApi33 = { locales ->
|
||||
val activity = requireActivity() as SettingsActivity
|
||||
activity.setApplicationLocalesPreApi33(locales)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
|
@ -33,10 +48,6 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||
Settings.BLACK_NIGHT_MODE.observe(viewLifecycleOwner, this::onBlackNightModeChanged)
|
||||
}
|
||||
|
||||
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.settings)
|
||||
}
|
||||
|
||||
private fun onThemeColorChanged(themeColor: ThemeColor) {
|
||||
CustomThemeHelper.sync()
|
||||
}
|
||||
|
@ -52,4 +63,14 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||
private fun onBlackNightModeChanged(blackNightMode: Boolean) {
|
||||
CustomThemeHelper.sync()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
// Refresh locale preference summary because we aren't notified for an external change
|
||||
// between system default and the locale that's the current system default.
|
||||
localePreference.notifyChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Hai Zhang <dreaming.in.code.zh@gmail.com>
|
||||
* All Rights Reserved.
|
||||
*/
|
||||
|
||||
package me.zhanghai.android.files.util
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import java.util.Locale
|
||||
|
||||
fun LocaleListCompat.toList(): List<Locale> = List(size()) { this[it]!! }
|
|
@ -43,6 +43,7 @@
|
|||
<string name="show">显示</string>
|
||||
<string name="skip">跳过</string>
|
||||
<string name="stop">停止</string>
|
||||
<string name="system_default">系统默认</string>
|
||||
<string name="unknown">未知</string>
|
||||
<string name="view">查看</string>
|
||||
|
||||
|
@ -532,6 +533,7 @@
|
|||
|
||||
<string name="settings_title">设置</string>
|
||||
<string name="settings_interface_title">界面</string>
|
||||
<string name="settings_locale_title">语言</string>
|
||||
<string name="settings_theme_color_title">主题色</string>
|
||||
<string name="settings_theme_color_summary">应用中最常见的颜色</string>
|
||||
<string name="settings_material_design_3_title">质感设计 3</string>
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
<string name="show">顯示</string>
|
||||
<string name="skip">跳過</string>
|
||||
<string name="stop">停止</string>
|
||||
<string name="system_default">系統預設</string>
|
||||
<string name="unknown">不明</string>
|
||||
<string name="view">查看</string>
|
||||
|
||||
|
@ -532,6 +533,7 @@
|
|||
|
||||
<string name="settings_title">設定</string>
|
||||
<string name="settings_interface_title">介面</string>
|
||||
<string name="settings_locale_title">語言</string>
|
||||
<string name="settings_theme_color_title">主題色</string>
|
||||
<string name="settings_theme_color_summary">應用程式中最常見的色彩</string>
|
||||
<string name="settings_material_design_3_title">質感設計 3</string>
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
<string name="pref_key_ftp_server_writable">key_ftp_server_writable</string>
|
||||
<bool name="pref_default_value_ftp_server_writable">true</bool>
|
||||
|
||||
<string name="pref_key_locale">key_locale</string>
|
||||
<string name="pref_key_theme_color">key_theme_color</string>
|
||||
<string name="pref_default_value_theme_color">0</string>
|
||||
<string name="pref_key_material_design_3">key_material_design_3</string>
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
<string name="show">Show</string>
|
||||
<string name="skip">Skip</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="system_default">System default</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
<string name="view">View</string>
|
||||
|
||||
|
@ -664,6 +665,7 @@
|
|||
|
||||
<string name="settings_title">Settings</string>
|
||||
<string name="settings_interface_title">Interface</string>
|
||||
<string name="settings_locale_title">Language</string>
|
||||
<string name="settings_theme_color_title">Theme color</string>
|
||||
<string name="settings_theme_color_summary">Color that appears most frequently in the app</string>
|
||||
<string name="settings_material_design_3_title">Material Design 3</string>
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
|
||||
<PreferenceCategory android:title="@string/settings_interface_title">
|
||||
|
||||
<me.zhanghai.android.files.settings.LocalePreference
|
||||
android:key="@string/pref_key_locale"
|
||||
android:title="@string/settings_locale_title" />
|
||||
|
||||
<me.zhanghai.android.files.theme.custom.ThemeColorPreference
|
||||
android:key="@string/pref_key_theme_color"
|
||||
android:title="@string/settings_theme_color_title"
|
||||
|
|
Loading…
Reference in New Issue
Block a user