├── .github └── FUNDING.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── nightdavisao │ │ └── multilocale │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ ├── android │ │ │ ├── content │ │ │ │ └── pm │ │ │ │ │ ├── BaseParceledListSlice.java │ │ │ │ │ ├── IPackageInstaller.java │ │ │ │ │ ├── IPackageInstallerSession.java │ │ │ │ │ ├── IPackageManager.java │ │ │ │ │ └── ParceledListSlice.java │ │ │ └── permission │ │ │ │ └── IPermissionManager.java │ │ └── io │ │ │ └── nightdavisao │ │ │ └── multilocale │ │ │ ├── LanguageDialogFragment.kt │ │ │ ├── LocaleRecyclerAdapter.kt │ │ │ ├── LocaleSettingsActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── PermissionCheckActivity.kt │ │ │ ├── RecyclerItemClickListener.kt │ │ │ └── utils │ │ │ └── LocaleUtils.kt │ └── res │ │ ├── drawable │ │ ├── baseline_add_24.xml │ │ ├── baseline_close_24.xml │ │ ├── baseline_delete_outline_24.xml │ │ ├── baseline_done_24.xml │ │ ├── baseline_drag_handle_24.xml │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ │ ├── layout │ │ ├── activity_locale_settings.xml │ │ ├── activity_main.xml │ │ ├── activity_permission_check.xml │ │ ├── content_language_selector.xml │ │ ├── content_main.xml │ │ └── locale_item.xml │ │ ├── menu │ │ └── menu_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-land │ │ └── dimens.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values-w1240dp │ │ └── dimens.xml │ │ ├── values-w600dp │ │ └── dimens.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── ids.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ └── test │ └── java │ └── io │ └── nightdavisao │ └── multilocale │ └── ExampleUnitTest.kt ├── assets ├── IzzyOnDroid.png ├── screenshot01.jpg └── screenshot02.jpg ├── build.gradle ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.jpg │ │ └── 2.jpg │ └── short_description.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: nightdavisao 2 | github: Nightdavisao -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 122 | 123 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Nightdavisao 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MultiLocale 2 | 3 | A simple app that enables you to add additional (or "unsupported") languages to your device's locale settings, if the OEM (*ahem* **Xiaomi**) doesn't let you. 4 |

5 | 6 |

7 | 8 | # Requirements 9 | * Android 7.0 (SDK 24) or more. 10 | * Shizuku/root or ADB to grant one of the needed permissions for changing the device's locale settings (`android.permission.CHANGE_CONFIGURATION`). 11 | # Motivation 12 | Some OEMs, particularly Xiaomi with MIUI/HyperOS, limit users to selecting only one language or locale. 13 | This restriction can lead to issues, such as apps displaying incorrect characters due to [Han unification](https://en.wikipedia.org/wiki/Han_unification), where characters in one language may appear in another. 14 | For example, without Japanese added as a locale, apps may show Chinese characters instead of Japanese ones. 15 | # Releases 16 | 17 | 18 | Get it on F-Droid 19 | 20 | 21 | Get it on IzzyOnDroid 22 | 23 | 24 | [GitHub Releases](https://github.com/Nightdavisao/MultiLocale/releases/latest) 25 | # Donate 26 | [Ko-fi](https://ko-fi.com/nightdavisao) 27 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'io.nightdavisao.multilocale' 8 | compileSdk 34 9 | 10 | dependenciesInfo { 11 | includeInApk = false 12 | includeInBundle = false 13 | } 14 | 15 | defaultConfig { 16 | applicationId "io.nightdavisao.multilocale" 17 | minSdk 24 18 | targetSdk 34 19 | versionCode 2 20 | versionName "0.2" 21 | 22 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 23 | } 24 | 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | buildFeatures { 39 | viewBinding true 40 | } 41 | } 42 | 43 | dependencies { 44 | 45 | implementation 'androidx.core:core-ktx:1.8.0' 46 | implementation 'androidx.appcompat:appcompat:1.6.1' 47 | implementation 'com.google.android.material:material:1.5.0' 48 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 49 | implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0' 50 | implementation 'androidx.navigation:navigation-ui-ktx:2.6.0' 51 | implementation 'androidx.activity:activity:1.8.0' 52 | testImplementation 'junit:junit:4.13.2' 53 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 55 | 56 | def shizuku_version = "13.1.4" 57 | implementation "dev.rikka.shizuku:api:$shizuku_version" 58 | 59 | // Add this line if you want to support Shizuku 60 | implementation "dev.rikka.shizuku:provider:$shizuku_version" 61 | 62 | implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3' 63 | 64 | def libsuVersion = '6.0.0' 65 | // The core module that provides APIs to a shell 66 | implementation "com.github.topjohnwu.libsu:core:${libsuVersion}" 67 | 68 | // Optional: APIs for creating root services. Depends on ":core" 69 | implementation "com.github.topjohnwu.libsu:service:${libsuVersion}" 70 | 71 | // Optional: Provides remote file system support 72 | implementation "com.github.topjohnwu.libsu:nio:${libsuVersion}" 73 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/io/nightdavisao/multilocale/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.nightdavisao.multilocale 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.nightdavisao.multilocale", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 36 | 37 | 38 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nightdavisao/MultiLocale/9b56dfa2494de8ca30c51eaf3aaf296cb3df7088/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/android/content/pm/BaseParceledListSlice.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import java.util.List; 4 | 5 | abstract class BaseParceledListSlice { 6 | 7 | public List getList() { 8 | throw new RuntimeException("STUB"); 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/android/content/pm/IPackageInstaller.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | import android.os.RemoteException; 7 | 8 | public interface IPackageInstaller extends IInterface { 9 | 10 | void abandonSession(int sessionId) 11 | throws RemoteException; 12 | 13 | IPackageInstallerSession openSession(int sessionId) 14 | throws RemoteException; 15 | 16 | ParceledListSlice getMySessions(String installerPackageName, int userId) 17 | throws RemoteException; 18 | 19 | abstract class Stub extends Binder implements IPackageInstaller { 20 | 21 | public static IPackageInstaller asInterface(IBinder binder) { 22 | throw new UnsupportedOperationException(); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/android/content/pm/IPackageInstallerSession.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | 7 | public interface IPackageInstallerSession extends IInterface { 8 | 9 | abstract class Stub extends Binder implements IPackageInstallerSession { 10 | 11 | public static IPackageInstallerSession asInterface(IBinder binder) { 12 | throw new UnsupportedOperationException(); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/android/content/pm/IPackageManager.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | import android.os.RemoteException; 7 | 8 | import androidx.annotation.RequiresApi; 9 | 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | public interface IPackageManager extends IInterface { 13 | 14 | IPackageInstaller getPackageInstaller() 15 | throws RemoteException; 16 | 17 | void grantRuntimePermission(@NotNull String packageName, @NotNull String permissionName, int userId); 18 | 19 | abstract class Stub extends Binder implements IPackageManager { 20 | 21 | public static IPackageManager asInterface(IBinder obj) { 22 | throw new UnsupportedOperationException(); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/android/content/pm/ParceledListSlice.java: -------------------------------------------------------------------------------- 1 | package android.content.pm; 2 | 3 | public class ParceledListSlice extends BaseParceledListSlice { 4 | } -------------------------------------------------------------------------------- /app/src/main/java/android/permission/IPermissionManager.java: -------------------------------------------------------------------------------- 1 | package android.permission; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | import android.os.RemoteException; 7 | 8 | import androidx.annotation.RequiresApi; 9 | 10 | @RequiresApi(30) 11 | public interface IPermissionManager extends IInterface { 12 | 13 | void grantRuntimePermission(String packageName, String permissionName, int userId) 14 | throws RemoteException; 15 | 16 | void revokeRuntimePermission(String packageName, String permissionName, int userId) 17 | throws RemoteException; 18 | 19 | void revokeRuntimePermission(String packageName, String permissionName, int userId, String reason) 20 | throws RemoteException; 21 | 22 | int getPermissionFlags(String permissionName, String packageName, int userId) 23 | throws RemoteException; 24 | 25 | void updatePermissionFlags(String permissionName, String packageName, int flagMask, int flagValues, boolean checkAdjustPolicyFlagPermission, int userId) 26 | throws RemoteException; 27 | 28 | int checkPermission(String permName, String pkgName, int userId) 29 | throws RemoteException; 30 | 31 | int checkUidPermission(String permName, int uid) 32 | throws RemoteException; 33 | 34 | @RequiresApi(30) 35 | abstract class Stub extends Binder implements IPermissionManager { 36 | 37 | public static IPermissionManager asInterface(IBinder obj) { 38 | throw new UnsupportedOperationException(); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/io/nightdavisao/multilocale/LanguageDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package io.nightdavisao.multilocale 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import android.text.Editable 6 | import android.text.TextWatcher 7 | import android.view.View 8 | import android.widget.EditText 9 | import androidx.fragment.app.DialogFragment 10 | import androidx.recyclerview.widget.LinearLayoutManager 11 | import androidx.recyclerview.widget.RecyclerView 12 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 13 | import java.util.Locale 14 | 15 | class LanguageDialogFragment( 16 | private val onClick: (Locale) -> Unit 17 | ): DialogFragment() { 18 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 19 | activity?.let { 20 | val builder = MaterialAlertDialogBuilder(it) 21 | val availableLocales = Locale.getAvailableLocales() 22 | // inflate the custom view 23 | val view = layoutInflater.inflate(R.layout.content_language_selector, null) 24 | builder.setView(view) 25 | // set the adapter 26 | val recyclerView = view.findViewById(R.id.languageListRecyclerView) 27 | val localeRecyclerAdapter = LocaleRecyclerAdapter(recyclerView, availableLocales.toMutableList(), false, false) 28 | recyclerView.adapter = localeRecyclerAdapter 29 | recyclerView.layoutManager = LinearLayoutManager(it) 30 | // set the click listener 31 | recyclerView.addOnItemTouchListener(RecyclerItemClickListener(it, recyclerView, object : RecyclerItemClickListener.OnItemClickListener { 32 | override fun onItemClick(view: View?, position: Int) { 33 | val adapter = recyclerView.adapter as LocaleRecyclerAdapter 34 | onClick(adapter.filteredLocaleList[position]) 35 | dismiss() 36 | } 37 | 38 | override fun onItemLongClick(view: View?, position: Int) { 39 | // do nothing 40 | } 41 | })) 42 | // filter items in recycler view by searchEditText's text (THIS IS NOT A SEARCHVIEW) 43 | val searchEditText = view.findViewById(R.id.searchEditText) 44 | searchEditText.addTextChangedListener(object : TextWatcher { 45 | override fun afterTextChanged(s: Editable?) { 46 | localeRecyclerAdapter.searchFilter.filter(s) 47 | } 48 | 49 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { 50 | // do nothing 51 | } 52 | 53 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 54 | 55 | // do nothing 56 | } 57 | }) 58 | 59 | return builder.create() 60 | } ?: run { 61 | throw IllegalStateException("Activity cannot be null") 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/io/nightdavisao/multilocale/LocaleRecyclerAdapter.kt: -------------------------------------------------------------------------------- 1 | package io.nightdavisao.multilocale 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.CheckBox 8 | import android.widget.ImageView 9 | import android.widget.TextView 10 | import androidx.recyclerview.widget.RecyclerView 11 | import android.widget.Filter 12 | import androidx.recyclerview.widget.DiffUtil 13 | import java.util.Locale 14 | 15 | class LocaleRecyclerAdapter( 16 | val recyclerView: RecyclerView, 17 | val localeList: MutableList, 18 | var deleteMode: Boolean, 19 | var showDragHandle: Boolean = true, 20 | ) : 21 | RecyclerView.Adapter() { 22 | 23 | var filteredLocaleList: List = localeList 24 | 25 | val diffCallback = object : DiffUtil.Callback() { 26 | override fun getOldListSize(): Int { 27 | return localeList.size 28 | } 29 | 30 | override fun getNewListSize(): Int { 31 | return filteredLocaleList.size 32 | } 33 | 34 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 35 | return filteredLocaleList[newItemPosition].hashCode() == localeList[oldItemPosition].hashCode() 36 | } 37 | 38 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 39 | return filteredLocaleList[newItemPosition] == localeList[oldItemPosition] 40 | } 41 | 42 | } 43 | 44 | val searchFilter = object : Filter() { 45 | override fun performFiltering(constraint: CharSequence?): FilterResults { 46 | val charSeqStr = constraint.toString() 47 | filteredLocaleList = if (charSeqStr.isEmpty()) { 48 | localeList 49 | } else { 50 | localeList.filter { it.displayName.contains(charSeqStr, true) } 51 | } 52 | val results = FilterResults() 53 | results.values = filteredLocaleList 54 | return results 55 | } 56 | 57 | override fun publishResults(constraint: CharSequence?, results: FilterResults) { 58 | filteredLocaleList = results.values as List 59 | recyclerView.postDelayed({ 60 | notifyDataSetChanged() 61 | }, 500) 62 | } 63 | } 64 | 65 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 66 | val localeFriendlyName: TextView 67 | val localeLanguageName: TextView 68 | val deleteCheckBox: CheckBox 69 | val dragHandle: ImageView 70 | 71 | init { 72 | localeFriendlyName = view.findViewById(R.id.localeFriendlyName) 73 | localeLanguageName = view.findViewById(R.id.localeLanguageName) 74 | deleteCheckBox = view.findViewById(R.id.deleteCheckBox) 75 | dragHandle = view.findViewById(R.id.dragHandle) 76 | } 77 | } 78 | 79 | override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { 80 | val binding = LayoutInflater.from(viewGroup.context) 81 | .inflate(R.layout.locale_item, viewGroup, false) 82 | return ViewHolder(binding) 83 | } 84 | 85 | override fun getItemCount(): Int = filteredLocaleList.size 86 | 87 | @SuppressLint("SetTextI18n") 88 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 89 | val currentLocale = filteredLocaleList[position] 90 | 91 | holder.localeFriendlyName.text = currentLocale.displayName ?: "" 92 | holder.localeLanguageName.text = currentLocale.getDisplayName(filteredLocaleList[position]) 93 | holder.localeLanguageName.textLocale = currentLocale 94 | 95 | // if the texts are the same, hide the language name 96 | holder.localeLanguageName.visibility = 97 | if (holder.localeFriendlyName.text == holder.localeLanguageName.text) View.GONE else View.VISIBLE 98 | // only show the delete checkbox if the user clicked the "delete" action button 99 | holder.deleteCheckBox.visibility = if (deleteMode) View.VISIBLE else View.GONE 100 | holder.dragHandle.visibility = 101 | if (showDragHandle && !deleteMode) View.VISIBLE else View.GONE 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /app/src/main/java/io/nightdavisao/multilocale/LocaleSettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package io.nightdavisao.multilocale 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.content.ClipData 6 | import android.content.ClipboardManager 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.content.pm.PackageManager.PERMISSION_GRANTED 10 | import android.net.Uri 11 | import android.os.Build 12 | import android.os.Bundle 13 | import android.os.LocaleList 14 | import android.provider.Settings 15 | import android.util.Log 16 | import android.view.Menu 17 | import android.view.MenuItem 18 | import android.widget.Toast 19 | import androidx.activity.OnBackPressedCallback 20 | import androidx.appcompat.app.AppCompatActivity 21 | import androidx.core.app.ActivityCompat 22 | import androidx.core.text.HtmlCompat 23 | import androidx.core.view.WindowCompat 24 | import androidx.recyclerview.widget.DividerItemDecoration 25 | import androidx.recyclerview.widget.ItemTouchHelper 26 | import androidx.recyclerview.widget.LinearLayoutManager 27 | import androidx.recyclerview.widget.RecyclerView 28 | import com.google.android.material.color.DynamicColors 29 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 30 | import com.google.android.material.snackbar.Snackbar 31 | import io.nightdavisao.multilocale.databinding.ActivityLocaleSettingsBinding 32 | import io.nightdavisao.multilocale.utils.LocaleUtils 33 | import rikka.shizuku.Shizuku 34 | import java.util.Collections 35 | import java.util.Locale 36 | 37 | 38 | class LocaleSettingsActivity : AppCompatActivity() { 39 | companion object { 40 | private const val TAG = "LocaleSettingsActivity" 41 | } 42 | 43 | private val localeRecyclerView: RecyclerView 44 | get() = binding.contentMain.localeRecyclerView 45 | 46 | private val localeRecyclerViewAdapter: LocaleRecyclerAdapter 47 | get() = localeRecyclerView.adapter as LocaleRecyclerAdapter 48 | 49 | private var userHasChangedLocales = 50 | false // used to show the snackbar only once if the user changes the settings 51 | 52 | private val dragCallback = object : ItemTouchHelper.SimpleCallback( 53 | ItemTouchHelper.UP or ItemTouchHelper.DOWN, 54 | 0 55 | ) { 56 | override fun onMove( 57 | recyclerView: RecyclerView, 58 | viewHolder: RecyclerView.ViewHolder, 59 | target: RecyclerView.ViewHolder 60 | ): Boolean { 61 | val adapter = localeRecyclerViewAdapter 62 | val from = viewHolder.adapterPosition 63 | val to = target.adapterPosition 64 | Collections.swap(adapter.localeList, from, to) 65 | adapter.notifyItemMoved(from, to) 66 | makeSnackSaveConfiguration() 67 | return true 68 | } 69 | 70 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { 71 | // do nothing 72 | } 73 | } 74 | 75 | private val itemTouchHelper = ItemTouchHelper(dragCallback) 76 | 77 | private lateinit var binding: ActivityLocaleSettingsBinding 78 | 79 | private fun addLocale(locale: Locale) { 80 | val adapter = localeRecyclerViewAdapter 81 | adapter.localeList.add(locale) 82 | adapter.notifyItemInserted(adapter.localeList.size - 1) 83 | makeSnackSaveConfiguration() 84 | } 85 | 86 | private fun makeSnackSaveConfiguration() { 87 | if (userHasChangedLocales) return 88 | Snackbar.make( 89 | binding.root, 90 | R.string.save_cfg_are_you_sure, 91 | Snackbar.LENGTH_INDEFINITE 92 | ) 93 | .setAction("Save") { 94 | saveLocaleConfiguration() 95 | } 96 | .show() 97 | userHasChangedLocales = true 98 | } 99 | 100 | @SuppressLint("NotifyDataSetChanged") 101 | override fun onCreate(savedInstanceState: Bundle?) { 102 | DynamicColors.applyIfAvailable(this) 103 | WindowCompat.setDecorFitsSystemWindows(window, false) 104 | super.onCreate(savedInstanceState) 105 | 106 | binding = ActivityLocaleSettingsBinding.inflate(layoutInflater) 107 | localeRecyclerView.apply { 108 | layoutManager = LinearLayoutManager(this@LocaleSettingsActivity) 109 | // extract locales from the configuration and add them to a list 110 | val locales = mutableListOf() 111 | for (i in 0 until resources.configuration.locales.size()) { 112 | locales.add(resources.configuration.locales[i]) 113 | } 114 | 115 | adapter = LocaleRecyclerAdapter(localeRecyclerView, locales, false) 116 | } 117 | localeRecyclerView.addItemDecoration( 118 | DividerItemDecoration( 119 | this, 120 | DividerItemDecoration.VERTICAL 121 | ) 122 | ) 123 | // drag and drop to reorder locales 124 | itemTouchHelper.attachToRecyclerView(localeRecyclerView) 125 | setContentView(binding.root) 126 | setSupportActionBar(binding.toolbar) 127 | supportActionBar?.setHomeAsUpIndicator(R.drawable.baseline_close_24) 128 | 129 | binding.fab.setOnClickListener { 130 | if (localeRecyclerViewAdapter.deleteMode) { 131 | // if delete mode is enabled, delete all checked locales 132 | val adapter = localeRecyclerViewAdapter 133 | val localesToDelete = mutableListOf() 134 | for (i in 0 until adapter.localeList.size) { 135 | val holder = 136 | localeRecyclerView.findViewHolderForAdapterPosition(i) as LocaleRecyclerAdapter.ViewHolder 137 | if (holder.deleteCheckBox.isChecked) { 138 | localesToDelete.add(adapter.localeList[i]) 139 | } 140 | } 141 | // if ALL of the locales were selected, tell the user they can't delete all of them 142 | if (localesToDelete.size == adapter.localeList.size) { 143 | Toast.makeText( 144 | this, 145 | R.string.cant_nuke_locales, 146 | Toast.LENGTH_SHORT 147 | ).show() 148 | return@setOnClickListener 149 | } 150 | adapter.localeList.removeAll(localesToDelete) 151 | adapter.notifyDataSetChanged() 152 | toggleDeleteMode() 153 | makeSnackSaveConfiguration() 154 | } else { 155 | val dialog = LanguageDialogFragment { locale -> 156 | addLocale(locale) 157 | } 158 | dialog.show(supportFragmentManager, "LanguageDialogFragment") 159 | } 160 | } 161 | 162 | // register callback for back button presses 163 | val callback = object : OnBackPressedCallback(true) { 164 | override fun handleOnBackPressed() { 165 | if (localeRecyclerViewAdapter.deleteMode) { 166 | toggleDeleteMode() 167 | } else { 168 | finish() 169 | } 170 | } 171 | } 172 | onBackPressedDispatcher.addCallback(this, callback) 173 | } 174 | 175 | override fun onCreateOptionsMenu(menu: Menu): Boolean { 176 | // Inflate the menu; this adds items to the action bar if it is present. 177 | menuInflater.inflate(R.menu.menu_main, menu) 178 | return true 179 | } 180 | 181 | private fun toggleDeleteMode() { 182 | val adapter = localeRecyclerViewAdapter 183 | adapter.deleteMode = !adapter.deleteMode 184 | adapter.notifyDataSetChanged() 185 | // hide and show stuff depending on if delete mode is enabled 186 | binding.fab.setImageResource(if (adapter.deleteMode) R.drawable.baseline_done_24 else R.drawable.baseline_add_24) 187 | binding.toolbar.menu.findItem(R.id.action_save_locale_config).isVisible = 188 | !adapter.deleteMode 189 | binding.toolbar.menu.findItem(R.id.action_delete_locale).isVisible = !adapter.deleteMode 190 | // change the title of the toolbar depending on if delete mode is enabled 191 | binding.toolbar.title = 192 | if (adapter.deleteMode) getString(R.string.select_locales_deletion) else applicationInfo.loadLabel( 193 | packageManager 194 | ) 195 | // show an X button in the toolbar if delete mode is enabled 196 | supportActionBar?.setDisplayHomeAsUpEnabled(adapter.deleteMode) 197 | // toggle drag and drop 198 | itemTouchHelper.attachToRecyclerView(if (adapter.deleteMode) null else localeRecyclerView) 199 | // uncheck all checkboxes when delete mode is disabled 200 | if (!adapter.deleteMode) { 201 | for (i in 0 until adapter.localeList.size) { 202 | val holder = 203 | localeRecyclerView.findViewHolderForAdapterPosition(i) 204 | if (holder is LocaleRecyclerAdapter.ViewHolder) holder.deleteCheckBox.isChecked = false 205 | } 206 | } 207 | } 208 | 209 | override fun onSupportNavigateUp(): Boolean { 210 | toggleDeleteMode() 211 | return true 212 | } 213 | 214 | private fun saveLocaleConfiguration() { 215 | if (LocaleUtils.areAllPermissionsGranted(this)) { 216 | val adapter = localeRecyclerViewAdapter 217 | val locales = adapter.localeList 218 | val localeList = LocaleList(*locales.toTypedArray()) 219 | // set the application locale list first 220 | Log.d(TAG, "Setting locale list for the application: $localeList") 221 | resources.configuration.setLocales(localeList) 222 | resources.updateConfiguration(resources.configuration, resources.displayMetrics) 223 | Log.d(TAG, "Saving locale list: $localeList") 224 | try { 225 | LocaleUtils.setLocaleList(localeList) 226 | Snackbar.make(binding.root, R.string.locale_list_saved, Snackbar.LENGTH_LONG).show() 227 | } catch (e: Exception) { 228 | Log.e(TAG, "Failed to save locale list", e) 229 | Snackbar.make(binding.root, R.string.failed_to_save_locale_list, Snackbar.LENGTH_LONG) 230 | .show() 231 | } 232 | userHasChangedLocales = false 233 | } else { 234 | Toast.makeText(this, R.string.permissions_not_granted, Toast.LENGTH_SHORT).show() 235 | } 236 | } 237 | 238 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 239 | // Handle action bar item clicks here. The action bar will 240 | // automatically handle clicks on the Home/Up button, so long 241 | // as you specify a parent activity in AndroidManifest.xml. 242 | return when (item.itemId) { 243 | R.id.action_save_locale_config -> { 244 | saveLocaleConfiguration() 245 | true 246 | } 247 | 248 | R.id.action_delete_locale -> { 249 | toggleDeleteMode() 250 | true 251 | } 252 | 253 | else -> super.onOptionsItemSelected(item) 254 | } 255 | } 256 | } -------------------------------------------------------------------------------- /app/src/main/java/io/nightdavisao/multilocale/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.nightdavisao.multilocale 2 | 3 | import android.Manifest 4 | import android.content.Intent 5 | import android.content.pm.PackageManager.PERMISSION_GRANTED 6 | import android.os.Bundle 7 | import android.provider.Settings 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.core.app.ActivityCompat 10 | 11 | class MainActivity : AppCompatActivity() { 12 | companion object { 13 | private const val TAG = "MainActivity" 14 | } 15 | 16 | private fun areAllPermissionsGranted(): Boolean { 17 | return ActivityCompat.checkSelfPermission( 18 | this, 19 | Manifest.permission.CHANGE_CONFIGURATION 20 | ) == PERMISSION_GRANTED && Settings.System.canWrite(this) 21 | } 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | val intent = if (!areAllPermissionsGranted()) { 26 | Intent(this, PermissionCheckActivity::class.java) 27 | } else { 28 | Intent(this, LocaleSettingsActivity::class.java) 29 | } 30 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 31 | startActivity(intent) 32 | finish() 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/io/nightdavisao/multilocale/PermissionCheckActivity.kt: -------------------------------------------------------------------------------- 1 | package io.nightdavisao.multilocale 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Intent 6 | import android.content.pm.PackageManager.PERMISSION_GRANTED 7 | import android.net.Uri 8 | import android.os.Build 9 | import android.os.Bundle 10 | import android.provider.Settings 11 | import android.util.Log 12 | import android.widget.Toast 13 | import androidx.activity.enableEdgeToEdge 14 | import androidx.appcompat.app.AppCompatActivity 15 | import androidx.core.text.HtmlCompat 16 | import androidx.core.view.ViewCompat 17 | import androidx.core.view.WindowInsetsCompat 18 | import com.google.android.material.color.DynamicColors 19 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 20 | import com.topjohnwu.superuser.Shell 21 | import io.nightdavisao.multilocale.databinding.ActivityPermissionCheckBinding 22 | import io.nightdavisao.multilocale.utils.LocaleUtils 23 | import rikka.shizuku.Shizuku 24 | import rikka.shizuku.shared.BuildConfig 25 | 26 | 27 | class PermissionCheckActivity : AppCompatActivity() { 28 | private lateinit var binding: ActivityPermissionCheckBinding 29 | companion object { 30 | private const val TAG = "PermissionCheckActivity" 31 | private const val SHIZUKU_CODE = 69 32 | } 33 | 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | DynamicColors.applyIfAvailable(this) 36 | super.onCreate(savedInstanceState) 37 | binding = ActivityPermissionCheckBinding.inflate(layoutInflater) 38 | binding.grantOptionRadioGroup.setOnCheckedChangeListener { radioGroup, i -> 39 | binding.grantSecurePermission.isEnabled = true 40 | } 41 | 42 | binding.grantSecurePermission.setOnClickListener { 43 | // get the selected radio button 44 | val selectedRadioButton = binding.grantOptionRadioGroup.checkedRadioButtonId 45 | 46 | when (selectedRadioButton) { 47 | R.id.radio_shizuku -> { 48 | if (Shizuku.pingBinder()) { 49 | if (checkShizukuPermission(SHIZUKU_CODE)) { 50 | LocaleUtils.grantConfigurationPermissionShizuku(this) 51 | } 52 | } else { 53 | Toast.makeText(this, R.string.shizuku_not_found, Toast.LENGTH_SHORT).show() 54 | } 55 | } 56 | R.id.radio_root -> { 57 | // there's no programmatic way to check if root is available, so we just try to run a command and hope for the best 58 | if (!LocaleUtils.grantConfigurationPermissionRoot(this)) { 59 | Toast.makeText(this, R.string.failed_granting_permission_root, Toast.LENGTH_LONG).show() 60 | } 61 | } 62 | R.id.radio_adb -> showADBDialog() 63 | } 64 | checkPermissions() 65 | } 66 | checkPermissions() 67 | setContentView(binding.root) 68 | Shizuku.addRequestPermissionResultListener(this::onRequestPermissionResult) 69 | } 70 | 71 | override fun onDestroy() { 72 | Shizuku.removeRequestPermissionResultListener(this::onRequestPermissionResult) 73 | super.onDestroy() 74 | } 75 | 76 | override fun onResume() { 77 | super.onResume() 78 | checkPermissions() 79 | } 80 | 81 | private fun checkPermissions() { 82 | // check if the user has granted the permission(s) 83 | if (LocaleUtils.isChangeConfigurationPermissionGranted(this)) { 84 | showWriteSettingsDialog() 85 | } else { 86 | binding.grantOptionRadioGroup.clearCheck() 87 | binding.grantSecurePermission.isEnabled = false 88 | } 89 | if (LocaleUtils.areAllPermissionsGranted(this)) { 90 | startLocaleSettingsActivity() 91 | } 92 | } 93 | 94 | private fun startLocaleSettingsActivity() { 95 | val intent = Intent(this, LocaleSettingsActivity::class.java) 96 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK 97 | startActivity(intent) 98 | } 99 | 100 | private fun showADBDialog() { 101 | val adbCommand = "adb shell pm grant $packageName android.permission.CHANGE_CONFIGURATION" 102 | val message = getString(R.string.grant_permission_instructions, adbCommand) 103 | val dialog = MaterialAlertDialogBuilder(this) 104 | .setTitle(R.string.permission_required) 105 | .setMessage(message) 106 | .setPositiveButton("OK") { _, _ -> 107 | val clipboardManager = applicationContext.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager 108 | val clipData = ClipData.newPlainText("ADB command", adbCommand) 109 | clipboardManager.setPrimaryClip(clipData) 110 | Toast.makeText(this, getString(R.string.pls_grant), Toast.LENGTH_LONG).show() 111 | } 112 | .setNeutralButton("What is ADB?") { _, _ -> 113 | val intent = Intent(Intent.ACTION_VIEW) 114 | intent.data = Uri.parse("https://github.com/Nightdavisao/MultiLocale/wiki/What-is-ADB%3F") 115 | startActivity(intent) 116 | } 117 | .create() 118 | dialog.setCancelable(false) 119 | dialog.show() 120 | } 121 | 122 | private fun showWriteSettingsDialog() { 123 | val dialog = MaterialAlertDialogBuilder(this) 124 | .setTitle(R.string.permission_required) 125 | .setMessage(R.string.grant_write_permission) 126 | .setPositiveButton("OK") { _, _ -> 127 | val intent = Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS) 128 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { 129 | intent.data = Uri.parse("package:$packageName") 130 | } 131 | startActivity(intent) 132 | } 133 | .create() 134 | dialog.setCancelable(false) 135 | dialog.show() 136 | } 137 | 138 | private fun checkShizukuPermission(code: Int): Boolean { 139 | if (Shizuku.isPreV11()) { 140 | return false 141 | } 142 | return if (Shizuku.checkSelfPermission() == PERMISSION_GRANTED) { 143 | true 144 | } else if (Shizuku.shouldShowRequestPermissionRationale()) { 145 | Toast.makeText(this, R.string.pls_grant, Toast.LENGTH_LONG).show() 146 | false 147 | } else { 148 | Shizuku.requestPermission(code) 149 | false 150 | } 151 | } 152 | 153 | private fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { 154 | val granted = grantResult == PERMISSION_GRANTED 155 | if (requestCode == SHIZUKU_CODE) { 156 | if (granted) { 157 | Log.i(TAG, "Shizuku permission granted") 158 | LocaleUtils.grantConfigurationPermissionShizuku(this) 159 | } else { 160 | Log.e(TAG, "Shizuku permission denied") 161 | } 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /app/src/main/java/io/nightdavisao/multilocale/RecyclerItemClickListener.kt: -------------------------------------------------------------------------------- 1 | package io.nightdavisao.multilocale 2 | 3 | import android.content.Context 4 | import android.view.GestureDetector 5 | import android.view.GestureDetector.SimpleOnGestureListener 6 | import android.view.MotionEvent 7 | import android.view.View 8 | import androidx.recyclerview.widget.RecyclerView 9 | import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener 10 | 11 | 12 | class RecyclerItemClickListener( 13 | context: Context?, 14 | recyclerView: RecyclerView, 15 | private val mListener: OnItemClickListener? 16 | ) : 17 | OnItemTouchListener { 18 | interface OnItemClickListener { 19 | fun onItemClick(view: View?, position: Int) 20 | fun onItemLongClick(view: View?, position: Int) 21 | } 22 | 23 | private val mGestureDetector: GestureDetector 24 | 25 | init { 26 | mGestureDetector = GestureDetector(context, object : SimpleOnGestureListener() { 27 | override fun onSingleTapUp(e: MotionEvent): Boolean { 28 | return true 29 | } 30 | 31 | override fun onLongPress(e: MotionEvent) { 32 | val childView = recyclerView.findChildViewUnder(e.x, e.y) 33 | if (childView != null && mListener != null) { 34 | mListener.onItemLongClick( 35 | childView, 36 | recyclerView.getChildAdapterPosition(childView) 37 | ) 38 | } 39 | } 40 | }) 41 | } 42 | 43 | override fun onInterceptTouchEvent(view: RecyclerView, e: MotionEvent): Boolean { 44 | val childView = view.findChildViewUnder(e.x, e.y) 45 | if (mGestureDetector.onTouchEvent(e) && childView != null && mListener != null) { 46 | mListener.onItemClick(childView, view.getChildAdapterPosition(childView)) 47 | } 48 | return false 49 | } 50 | 51 | override fun onTouchEvent(view: RecyclerView, motionEvent: MotionEvent) {} 52 | override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) {} 53 | } -------------------------------------------------------------------------------- /app/src/main/java/io/nightdavisao/multilocale/utils/LocaleUtils.kt: -------------------------------------------------------------------------------- 1 | package io.nightdavisao.multilocale.utils 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.content.Context 6 | import android.content.pm.IPackageManager 7 | import android.content.pm.PackageManager.PERMISSION_GRANTED 8 | import android.content.res.Configuration 9 | import android.os.Build 10 | import android.os.LocaleList 11 | import android.os.RemoteException 12 | import android.permission.IPermissionManager 13 | import android.provider.Settings 14 | import androidx.annotation.RequiresApi 15 | import androidx.core.app.ActivityCompat 16 | import com.topjohnwu.superuser.Shell 17 | import org.lsposed.hiddenapibypass.HiddenApiBypass 18 | import rikka.shizuku.ShizukuBinderWrapper 19 | import rikka.shizuku.SystemServiceHelper 20 | import java.io.IOException 21 | 22 | 23 | object LocaleUtils { 24 | private val iPackageManager by lazy { 25 | IPackageManager.Stub.asInterface( 26 | ShizukuBinderWrapper(SystemServiceHelper.getSystemService("package")) 27 | ) 28 | } 29 | 30 | @delegate:RequiresApi(Build.VERSION_CODES.R) 31 | private val iPermissionManager by lazy { 32 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 33 | HiddenApiBypass.addHiddenApiExemptions( 34 | "Landroid/permission" 35 | ) 36 | } 37 | IPermissionManager.Stub.asInterface( 38 | ShizukuBinderWrapper(SystemServiceHelper.getSystemService("permissionmgr")) 39 | ) 40 | } 41 | private fun grantRuntimePermission(packageName: String, permissionName: String, userId: Int) { 42 | try { 43 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 44 | iPermissionManager.grantRuntimePermission(packageName, permissionName, userId) 45 | } else { 46 | iPackageManager.grantRuntimePermission(packageName, permissionName, userId) 47 | } 48 | } catch (tr: RemoteException) { 49 | throw RuntimeException(tr.message, tr) 50 | } 51 | } 52 | 53 | fun grantConfigurationPermissionShizuku(context: Context) { 54 | val userHandle = android.os.Process.myUserHandle().hashCode() 55 | grantRuntimePermission(context.packageName, "android.permission.CHANGE_CONFIGURATION", userHandle) 56 | } 57 | 58 | fun setLocaleList(locales: LocaleList?): Boolean { 59 | @SuppressLint("PrivateApi") val activityManagerNative = 60 | Class.forName("android.app.ActivityManagerNative") 61 | 62 | // ActivityManagerNative.getDefault(); 63 | val getDefault = activityManagerNative.getMethod("getDefault") 64 | val am = getDefault.invoke(activityManagerNative) 65 | 66 | // am.getConfiguration(); 67 | val getConfiguration = am.javaClass.getMethod("getConfiguration") 68 | val config = getConfiguration.invoke(am) as Configuration 69 | config.setLocales(locales) 70 | val field = config.javaClass.getField("userSetLocale") 71 | field[config] = true 72 | 73 | // am.updateConfiguration(config); 74 | val updateConfiguration = am.javaClass.getMethod( 75 | "updatePersistentConfiguration", *arrayOf>( 76 | Configuration::class.java 77 | ) 78 | ) 79 | updateConfiguration.invoke(am, *arrayOf(config)) 80 | return true 81 | } 82 | 83 | fun grantConfigurationPermissionRoot(context: Context): Boolean { 84 | return Shell.cmd("pm grant ${context.packageName} android.permission.CHANGE_CONFIGURATION") 85 | .exec() 86 | .isSuccess 87 | } 88 | 89 | fun isChangeConfigurationPermissionGranted(context: Context): Boolean { 90 | return ActivityCompat.checkSelfPermission( 91 | context, 92 | Manifest.permission.CHANGE_CONFIGURATION 93 | ) == PERMISSION_GRANTED 94 | } 95 | 96 | fun areAllPermissionsGranted(context: Context): Boolean { 97 | return isChangeConfigurationPermissionGranted(context) && Settings.System.canWrite(context) 98 | } 99 | 100 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_add_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_close_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_delete_outline_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_done_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_drag_handle_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_locale_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 19 | 20 | 21 | 22 | 25 | 26 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_permission_check.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 30 | 31 | 40 | 41 | 48 | 49 | 54 | 55 | 60 | 61 | 66 | 67 | 68 |