├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── vegabobo │ │ └── languageselector │ │ └── IUserService.aidl │ ├── ic_launcher-playstore.png │ ├── java │ └── vegabobo │ │ └── languageselector │ │ ├── App.kt │ │ ├── LocaleManager.kt │ │ ├── MainActivity.kt │ │ ├── QSTile.kt │ │ ├── RootReceivedListener.kt │ │ ├── Utils.kt │ │ ├── dao │ │ ├── AppInfoDao.kt │ │ ├── AppInfoDb.kt │ │ └── AppInfoEntity.kt │ │ ├── di │ │ └── Modules.kt │ │ ├── service │ │ ├── Connection.kt │ │ ├── RootUserService.kt │ │ ├── UserService.kt │ │ └── UserServiceProvider.kt │ │ └── ui │ │ ├── components │ │ ├── AppListItem.kt │ │ ├── AppSearchBar.kt │ │ ├── BackButton.kt │ │ ├── FilterLabel.kt │ │ ├── LocaleItemList.kt │ │ ├── QuickTextButton.kt │ │ └── Title.kt │ │ ├── screen │ │ ├── BaseScreen.kt │ │ ├── Navigation.kt │ │ ├── about │ │ │ └── AboutScreen.kt │ │ ├── appinfo │ │ │ ├── AppInfoScreen.kt │ │ │ ├── AppInfoState.kt │ │ │ └── AppInfoVm.kt │ │ └── main │ │ │ ├── MainScreen.kt │ │ │ ├── MainScreenState.kt │ │ │ ├── MainScreenVm.kt │ │ │ ├── SearchBarActions.kt │ │ │ ├── ShizukuRequiredWarning.kt │ │ │ └── SystemDialogWarn.kt │ │ └── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ └── res │ ├── drawable-night │ └── qs_tile.png │ ├── drawable │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── icon_placeholder.webp │ └── qs_tile.png │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ ├── ic_launcher_background.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ ├── ic_launcher_background.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_background.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_background.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ ├── ic_launcher_background.webp │ ├── ic_launcher_foreground.webp │ └── ic_launcher_round.webp │ ├── values-ja │ └── strings.xml │ ├── values-night │ └── colors.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ ├── data_extraction_rules.xml │ └── locales_config.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hidden_api ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── android │ └── app │ ├── ActivityManager.java │ ├── ActivityTaskManager.java │ ├── IActivityManager.java │ ├── IActivityTaskManager.java │ ├── IApplicationThread.java │ ├── ILocaleManager.java │ └── ProfilerInfo.java ├── other ├── preview_1.jpg └── preview_2.jpg └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Licensed under the Apache License, Version 2.0 (the "License"); 179 | you may not use this file except in compliance with the License. 180 | You may obtain a copy of the License at 181 | 182 | http://www.apache.org/licenses/LICENSE-2.0 183 | 184 | Unless required by applicable law or agreed to in writing, software 185 | distributed under the License is distributed on an "AS IS" BASIS, 186 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 187 | See the License for the specific language governing permissions and 188 | limitations under the License. 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Language Selector 2 | 3 | Language Selector allows users to set individual app languages. It tries to replicates the behavior of the "App languages" feature introduced in Android 13. 4 | 5 | To use this app: 6 | - MUST be on Android 13 or higher, there is no compatiblity with older Android versions. 7 | - MUST have Shizuku. 8 | 9 | You can get this app at Releases section. 10 | 11 |
12 | preview 13 | preview 14 |
15 | 16 | ### Features 17 | 18 | - Set individual app languages 19 | - Allows selecting language from any app ** 20 | - Quick change languages with QSTile 21 | 22 | ** Language Selector DOES NOT translate apps, it just specify a locale that will be used by application, if the desired language is supported by the app, it should be displayed as expected. 23 | 24 | ** Please note that changing locale for unsupported applications and system apps may cause unexpected behavior and is NOT RECOMMENDED. 25 | 26 | #### Language availability 27 | 28 | This app parses Locale (java.util.Locale) from Locale.getAvailableLocales(), consequently, numerous locales are present in the app, the language list is huge, if someone want to improve that, feel free to send a PR, because this way is pretty slow and languages aren't filtered accurately. 29 | 30 | ### Usage 31 | 32 | Before using this app, you MUST install and start Shizuku, the way this app works makes Shizuku MANDATORY, after that, you should follow this steps: 33 | 34 | 1. Install "Language Selector" (check Releases) 35 | 2. Open, grant Shizuku permissions and tap on "Proceed" 36 | 3. Choose a app you want to select it's language. 37 | 4. Select any language from list 38 | 5. That is it? 39 | 40 | #### Pinning languages 41 | 42 | You can pin languages by long-pressing on desired language, pinned languages will appear at the top of the list and will also be available in the QS tile. 43 | 44 | #### Quick tile 45 | 46 | You can quick change current running app language by adding a QS tile, available tile languages are the pinned ones, if no pinned language is set, then tile will be marked as Unavailable, changing system apps language from QS is also not supported. 47 | 48 | ### Background 49 | 50 | I've made this app because MIUI doesn't seem to have app languages in Android 13 (at least on my device, running global MIUI 14/Android 13), by not having the feature, i mean, there is no option inside Settings app to change app languages individually, but since it is as Android 13 build, there is a high change that locale service is still present, if so, we can use LocaleManager to do per-app basis locale operations. 51 | 52 | Locale manager can be acessible via ADB, using "cmd locale" command, since adb has the ability to change other app languages, i've decided to make my own "front-end" for managing application locales, so i can set languages and use this feature, even if there is no UI for app languages in stock Settings app yet. 53 | 54 | Since ADB is required to manage other application languages, this app uses Shizuku to interact with LocaleManager APIs at privileged level, that's why Shizuku is mandatory to use this app. 55 | 56 | If your device is running Android 13 or higher, and your ROM doesn't include any option related to the app languages, this app may be useful. 57 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.com.android.application) 3 | alias(libs.plugins.org.jetbrains.kotlin.android) 4 | alias(libs.plugins.com.google.dagger.hilt) 5 | alias(libs.plugins.com.mikepenz.aboutlibraries) 6 | alias(libs.plugins.compose.compiler) 7 | alias(libs.plugins.com.google.devtools.ksp) 8 | } 9 | 10 | android { 11 | namespace = "vegabobo.languageselector" 12 | compileSdk = 35 13 | 14 | defaultConfig { 15 | applicationId = "vegabobo.languageselector" 16 | minSdk = 33 17 | targetSdk = 35 18 | versionCode = 5 19 | versionName = "1.04" 20 | 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | vectorDrawables { 23 | useSupportLibrary = true 24 | } 25 | } 26 | 27 | buildTypes { 28 | release { 29 | signingConfig = signingConfigs.getByName("debug") 30 | isMinifyEnabled = true 31 | isShrinkResources = true 32 | proguardFiles( 33 | getDefaultProguardFile("proguard-android-optimize.txt"), 34 | "proguard-rules.pro" 35 | ) 36 | } 37 | } 38 | compileOptions { 39 | sourceCompatibility = JavaVersion.VERSION_21 40 | targetCompatibility = JavaVersion.VERSION_21 41 | } 42 | kotlinOptions { 43 | jvmTarget = "21" 44 | } 45 | buildFeatures { 46 | buildConfig = true 47 | compose = true 48 | aidl = true 49 | } 50 | packaging { 51 | resources { 52 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 53 | } 54 | } 55 | } 56 | 57 | aboutLibraries { 58 | excludeFields = arrayOf("generated") 59 | } 60 | 61 | dependencies { 62 | debugImplementation(libs.ui.tooling) 63 | debugImplementation(libs.ui.test.manifest) 64 | 65 | implementation(libs.libsu.core) 66 | implementation(libs.libsu.service) 67 | 68 | implementation(libs.core.ktx) 69 | implementation(libs.lifecycle.runtime.ktx) 70 | implementation(libs.activity.compose) 71 | implementation(platform(libs.compose.bom)) 72 | implementation(libs.ui) 73 | implementation(libs.ui.graphics) 74 | implementation(libs.ui.tooling.preview) 75 | implementation(libs.material) 76 | implementation(libs.material3) 77 | 78 | implementation(libs.androidx.hilt.navigation.compose) 79 | implementation(libs.androidx.navigation.compose) 80 | implementation(libs.androidx.material.icons.extended) 81 | implementation(libs.androidx.lifecycle.viewmodel.compose) 82 | 83 | implementation(libs.hilt.android) 84 | ksp(libs.hilt.android.compiler) 85 | 86 | implementation(libs.aboutlibraries.core) 87 | 88 | implementation(libs.shizuku.api) 89 | implementation(libs.shizuku.provider) 90 | 91 | implementation(libs.hiddenapibypass) 92 | 93 | implementation(libs.androidx.room.runtime) 94 | ksp(libs.androidx.room.compiler) 95 | 96 | compileOnly(project(":hidden_api")) 97 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -dontobfuscate 2 | # Add project specific ProGuard rules here. 3 | # You can control the set of applied configuration files using the 4 | # proguardFiles setting in build.gradle. 5 | # 6 | # For more details, see 7 | # http://developer.android.com/guide/developing/tools/proguard.html 8 | 9 | # If your project uses WebView with JS, uncomment the following 10 | # and specify the fully qualified class name to the JavaScript interface 11 | # class: 12 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 13 | # public *; 14 | #} 15 | 16 | # Uncomment this to preserve the line number information for 17 | # debugging stack traces. 18 | #-keepattributes SourceFile,LineNumberTable 19 | 20 | # If you keep the line number information, uncomment this to 21 | # hide the original source file name. 22 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 39 | 40 | 46 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /app/src/main/aidl/vegabobo/languageselector/IUserService.aidl: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector; 2 | 3 | interface IUserService { 4 | void exit() = 1; 5 | void destroy() = 16777114; 6 | int getUid() = 1000; 7 | 8 | // ILocaleManager 9 | void setApplicationLocales(String packageName, in LocaleList locales) = 2000; 10 | LocaleList getApplicationLocales(String packageName) = 2001; 11 | LocaleList getSystemLocales() = 2002; 12 | 13 | // IActivityManager 14 | void forceStopPackage(String packageName) = 3000; 15 | 16 | // IActivityTaskManager 17 | String getFirstRunningTaskPackage() = 4000; 18 | } -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/App.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App : Application() -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/LocaleManager.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector 2 | 3 | import vegabobo.languageselector.ui.screen.appinfo.LocaleRegion 4 | import vegabobo.languageselector.ui.screen.appinfo.SingleLocale 5 | import vegabobo.languageselector.ui.screen.appinfo.capDisplayName 6 | import java.util.Locale 7 | 8 | class LocaleManager { 9 | 10 | val localeList = ArrayList() 11 | 12 | init { 13 | val locales = Locale.getAvailableLocales() 14 | val localeListMap = mutableMapOf() 15 | for (locale in locales) { 16 | val languageName = locale.capDisplayName() 17 | val languageTag = locale.toLanguageTag() 18 | val language = locale.getDisplayLanguage(locale).replaceFirstChar { it.uppercaseChar() } 19 | 20 | val existingLocale = localeListMap[language] 21 | if (existingLocale != null) { 22 | val singleLocale = SingleLocale(languageName, languageTag) 23 | existingLocale.locales.add(singleLocale) 24 | continue 25 | } 26 | 27 | localeListMap[language] = 28 | LocaleRegion(language, arrayListOf()) 29 | } 30 | localeList.addAll(localeListMap.values) 31 | localeList.sortBy { it.language } 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector 2 | 3 | import android.content.ComponentName 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.Bundle 7 | import android.util.Log 8 | import androidx.activity.ComponentActivity 9 | import androidx.activity.compose.setContent 10 | import androidx.compose.foundation.layout.WindowInsets 11 | import androidx.compose.foundation.layout.navigationBars 12 | import androidx.core.view.ViewCompat 13 | import androidx.core.view.WindowCompat 14 | import androidx.core.view.WindowInsetsCompat 15 | import androidx.core.view.updatePadding 16 | import com.topjohnwu.superuser.Shell 17 | import com.topjohnwu.superuser.ipc.RootService 18 | import dagger.hilt.android.AndroidEntryPoint 19 | import rikka.shizuku.Shizuku 20 | import vegabobo.languageselector.service.RootUserService 21 | import vegabobo.languageselector.service.UserService 22 | import vegabobo.languageselector.service.UserServiceProvider 23 | import vegabobo.languageselector.ui.screen.Navigation 24 | import vegabobo.languageselector.ui.screen.main.OperationMode 25 | import vegabobo.languageselector.ui.theme.LanguageSelector 26 | 27 | object ShizukuArgs { 28 | val userServiceArgs = 29 | Shizuku.UserServiceArgs( 30 | ComponentName(BuildConfig.APPLICATION_ID, UserService::class.java.name), 31 | ) 32 | .daemon(false) 33 | .processNameSuffix("service") 34 | .debuggable(BuildConfig.DEBUG) 35 | .version(BuildConfig.VERSION_CODE) 36 | } 37 | 38 | 39 | @AndroidEntryPoint 40 | class MainActivity : ComponentActivity(), Shizuku.OnRequestPermissionResultListener { 41 | 42 | init { 43 | Shell.enableVerboseLogging = BuildConfig.DEBUG 44 | Shell.setDefaultBuilder(Shell.Builder.create().setTimeout(10)) 45 | } 46 | 47 | val acRequestCode = 1 48 | 49 | fun bindShizuku() { 50 | Shizuku.bindUserService(ShizukuArgs.userServiceArgs, UserServiceProvider.connection) 51 | } 52 | 53 | private val REQUEST_PERMISSION_RESULT_LISTENER = this::onRequestPermissionResult 54 | 55 | override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { 56 | if (grantResult == PackageManager.PERMISSION_GRANTED) 57 | bindShizuku() 58 | } 59 | 60 | private fun checkPermission(code: Int): Boolean { 61 | return if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { 62 | bindShizuku() 63 | true 64 | } else if (Shizuku.shouldShowRequestPermissionRationale()) { 65 | false 66 | } else { 67 | Shizuku.requestPermission(code) 68 | false 69 | } 70 | } 71 | 72 | override fun onCreate(savedInstanceState: Bundle?) { 73 | super.onCreate(savedInstanceState) 74 | WindowCompat.setDecorFitsSystemWindows(window, false) 75 | setContent { 76 | LanguageSelector { Navigation() } 77 | } 78 | 79 | if (Shizuku.pingBinder() && savedInstanceState == null) { 80 | Shizuku.addRequestPermissionResultListener(REQUEST_PERMISSION_RESULT_LISTENER) 81 | checkPermission(acRequestCode) 82 | } 83 | 84 | RootReceivedListener.setListener(object : IRootListener { 85 | override fun onRootReceived() { 86 | val intent = Intent(application, RootUserService::class.java) 87 | RootService.bind(intent, UserServiceProvider.connection) 88 | } 89 | }) 90 | } 91 | 92 | override fun onResume() { 93 | super.onResume() 94 | if ( 95 | Shizuku.pingBinder() && 96 | Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED && 97 | !UserServiceProvider.isConnected() 98 | ) { 99 | bindShizuku() 100 | } 101 | } 102 | 103 | override fun onDestroy() { 104 | Shizuku.removeRequestPermissionResultListener(REQUEST_PERMISSION_RESULT_LISTENER) 105 | RootReceivedListener.destroy() 106 | if (UserServiceProvider.isConnected()) { 107 | when (UserServiceProvider.opMode) { 108 | OperationMode.ROOT -> RootService.unbind(UserServiceProvider.connection) 109 | OperationMode.SHIZUKU -> Shizuku.unbindUserService( 110 | ShizukuArgs.userServiceArgs, 111 | UserServiceProvider.connection, 112 | true 113 | ) 114 | 115 | else -> Log.d(BuildConfig.APPLICATION_ID, "UserService not bound.") 116 | } 117 | } 118 | super.onDestroy() 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/QSTile.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector 2 | 3 | import android.content.Context 4 | import android.content.pm.ApplicationInfo 5 | import android.content.pm.PackageManager 6 | import android.os.LocaleList 7 | import android.service.quicksettings.Tile 8 | import android.service.quicksettings.TileService 9 | import android.util.Log 10 | import rikka.shizuku.Shizuku 11 | import vegabobo.languageselector.service.UserServiceProvider 12 | import vegabobo.languageselector.ui.screen.appinfo.PrefConstants 13 | import vegabobo.languageselector.ui.screen.appinfo.SingleLocale 14 | import vegabobo.languageselector.ui.screen.appinfo.capDisplayName 15 | import vegabobo.languageselector.ui.screen.appinfo.parseSetLangs 16 | import vegabobo.languageselector.ui.screen.main.getLabel 17 | 18 | 19 | class QSTile : TileService() { 20 | 21 | private var isLoaded = false 22 | private val locales = mutableListOf() 23 | private lateinit var targetPackage: ApplicationInfo 24 | 25 | private fun getNextSingleLocale(localeList: LocaleList): SingleLocale { 26 | if (locales.isEmpty()) 27 | throw Exception("getNextSingleLocale() should be not called with empty MutableList locales") 28 | if (localeList.isEmpty) 29 | return locales[1] 30 | for (i in 0 until locales.size) { 31 | val thisLocale = locales[i] 32 | if (localeList[0].toLanguageTag() == thisLocale.languageTag) { 33 | if (i == locales.size - 1) { 34 | return locales.first() 35 | } 36 | return locales[i + 1] 37 | } 38 | } 39 | return locales.first() 40 | } 41 | 42 | private fun setDisabledTile() { 43 | qsTile.label = getString(R.string.app_name) 44 | qsTile.subtitle = getString(R.string.unavailable) 45 | qsTile.state = Tile.STATE_UNAVAILABLE 46 | qsTile.updateTile() 47 | } 48 | 49 | private fun updateTile() { 50 | UserServiceProvider.run { 51 | val currentAppPackage = firstRunningTaskPackage 52 | targetPackage = 53 | packageManager.getApplicationInfo( 54 | currentAppPackage, 55 | PackageManager.ApplicationInfoFlags.of(0) 56 | ) 57 | if ( 58 | (targetPackage.flags and ApplicationInfo.FLAG_SYSTEM) != 0 || 59 | targetPackage.packageName == BuildConfig.APPLICATION_ID 60 | ) { 61 | // Prevent system apps and this app package to have locale replaced by QS toggle 62 | setDisabledTile() 63 | return@run 64 | } 65 | var isCustomLocale = false 66 | val currentLocale = 67 | try { 68 | val appLocales = getApplicationLocales(currentAppPackage) 69 | if (!appLocales.isEmpty) { 70 | isCustomLocale = true 71 | appLocales[0].capDisplayName() 72 | } else { 73 | "" 74 | } 75 | } catch (e: Exception) { 76 | "" 77 | }.ifBlank { getString(R.string.system_default) } 78 | qsTile.state = Tile.STATE_INACTIVE 79 | qsTile.updateTile() 80 | 81 | qsTile.label = currentLocale 82 | qsTile.subtitle = packageManager.getLabel(targetPackage) 83 | qsTile.state = if (isCustomLocale) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE 84 | qsTile.updateTile() 85 | } 86 | } 87 | 88 | fun loadLangs() { 89 | if (!isLoaded) { 90 | val sp = getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE) 91 | val set = sp.getStringSet(PrefConstants.PINNED_LOCALES, emptySet()) ?: emptySet() 92 | if (set.isNotEmpty()) { 93 | val systemDefaultLocale = SingleLocale("", "") 94 | locales.add(systemDefaultLocale) 95 | locales.addAll(set.parseSetLangs()) 96 | } 97 | isLoaded = true 98 | } 99 | } 100 | 101 | override fun onTileAdded() { 102 | if (BuildConfig.DEBUG) 103 | Log.d(BuildConfig.APPLICATION_ID, "QSTile onTileAdded()") 104 | super.onTileAdded() 105 | } 106 | 107 | override fun onStartListening() { 108 | if (BuildConfig.DEBUG) 109 | Log.d(BuildConfig.APPLICATION_ID, "QSTile onStartListening()") 110 | 111 | super.onStartListening() 112 | setDisabledTile() 113 | 114 | try { 115 | if (!UserServiceProvider.isConnected()) 116 | Shizuku.bindUserService(ShizukuArgs.userServiceArgs, UserServiceProvider.connection) 117 | } catch (e: Exception) { 118 | Log.e( 119 | BuildConfig.APPLICATION_ID, 120 | "Cannot bind UserService, non-fatal because it happened on QSTile.\n" + e.stackTraceToString() 121 | ) 122 | return 123 | } 124 | 125 | loadLangs() 126 | if (locales.isNotEmpty()) 127 | updateTile() 128 | } 129 | 130 | override fun onStopListening() { 131 | if (BuildConfig.DEBUG) 132 | Log.d(BuildConfig.APPLICATION_ID, "QSTile onStopListening()") 133 | isLoaded = false 134 | locales.clear() 135 | 136 | var shouldUnbind = true 137 | run { 138 | try { 139 | val service = UserServiceProvider.connection.SERVICE ?: return@run 140 | if (BuildConfig.APPLICATION_ID == service.firstRunningTaskPackage) 141 | shouldUnbind = false 142 | } catch (e: Exception) { 143 | // 144 | } 145 | } 146 | if (UserServiceProvider.isConnected() && shouldUnbind) 147 | Shizuku.unbindUserService( 148 | ShizukuArgs.userServiceArgs, 149 | UserServiceProvider.connection, 150 | true 151 | ) 152 | super.onStopListening() 153 | } 154 | 155 | override fun onClick() { 156 | if (BuildConfig.DEBUG) 157 | Log.d(BuildConfig.APPLICATION_ID, "QSTile onClick()") 158 | 159 | super.onClick() 160 | 161 | if (!this::targetPackage.isInitialized) 162 | return 163 | 164 | UserServiceProvider.run { 165 | val currentLocale = getApplicationLocales(targetPackage.packageName) 166 | try { 167 | Log.d(BuildConfig.APPLICATION_ID, "QSTile: ${currentLocale.isEmpty}") 168 | } catch (e: Exception) { 169 | Log.d(BuildConfig.APPLICATION_ID, e.stackTraceToString()) 170 | } 171 | val nextLocale = getNextSingleLocale(currentLocale) 172 | val localeList = 173 | if (nextLocale.languageTag.isEmpty()) 174 | LocaleList() 175 | else 176 | LocaleList(nextLocale.toLocale()) 177 | setApplicationLocales(targetPackage.packageName, localeList) 178 | updateTile() 179 | } 180 | } 181 | 182 | override fun onTileRemoved() { 183 | if (BuildConfig.DEBUG) 184 | Log.d(BuildConfig.APPLICATION_ID, "QSTile onTileRemoved()") 185 | super.onTileRemoved() 186 | } 187 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/RootReceivedListener.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector 2 | 3 | 4 | interface IRootListener { 5 | fun onRootReceived() 6 | } 7 | 8 | object RootReceivedListener { 9 | var callback: IRootListener? = null 10 | 11 | fun setListener(inCallback: IRootListener?) { 12 | callback = inCallback 13 | } 14 | 15 | fun onRootReceived() { 16 | callback?.onRootReceived() 17 | } 18 | 19 | fun destroy() { 20 | callback = null 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/Utils.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector 2 | 3 | import android.util.Log 4 | 5 | fun log(s: Any) { 6 | Log.d(BuildConfig.APPLICATION_ID, ""+s) 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/dao/AppInfoDao.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.dao 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | 8 | @Dao 9 | interface AppInfoDao { 10 | @Query("SELECT * FROM appinfoentity") 11 | fun getAll(): List 12 | 13 | @Query("SELECT * FROM appinfoentity WHERE pkg = :pkg") 14 | fun findByPkg(pkg: String): AppInfoEntity? 15 | 16 | @Insert 17 | fun insert(aie: AppInfoEntity) 18 | 19 | @Insert 20 | fun insertAll(vararg aie: AppInfoEntity) 21 | 22 | @Delete 23 | fun delete(aie: AppInfoEntity) 24 | 25 | @Query("DELETE FROM appinfoentity") 26 | fun deleteAll(): Int 27 | 28 | // 29 | 30 | @Query("SELECT (SELECT COUNT(*) FROM appinfoentity) == 0") 31 | fun isEmpty(): Boolean 32 | 33 | // 34 | 35 | @Query("UPDATE appinfoentity SET last_selected = NULL") 36 | fun cleanLastSelectedAll() 37 | 38 | @Query("UPDATE appinfoentity SET last_selected = :lastSelected WHERE pkg = :pkg") 39 | fun setLastSelected(pkg: String, lastSelected: Long) 40 | 41 | @Query("SELECT * FROM appinfoentity WHERE last_selected IS NOT NULL ORDER BY last_selected DESC") 42 | fun getHistory(): List 43 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/dao/AppInfoDb.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.dao 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | 6 | @Database(entities = [AppInfoEntity::class], version = 1) 7 | abstract class AppInfoDb : RoomDatabase() { 8 | abstract fun appInfoDao(): AppInfoDao 9 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/dao/AppInfoEntity.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.dao 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity 8 | data class AppInfoEntity( 9 | // Package name 10 | @PrimaryKey @ColumnInfo(name = "pkg") val pkg: String, 11 | // App name 12 | @ColumnInfo(name = "name") val name: String, 13 | // Last time user selected this app, history feature 14 | @ColumnInfo(name = "last_selected") val lastSelected: Long?, 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/di/Modules.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.di 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.SharedPreferences 6 | import androidx.room.Room 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | import vegabobo.languageselector.BuildConfig 12 | import vegabobo.languageselector.LocaleManager 13 | import vegabobo.languageselector.dao.AppInfoDb 14 | import javax.inject.Singleton 15 | 16 | @InstallIn(SingletonComponent::class) 17 | @Module 18 | object Modules { 19 | 20 | @Singleton 21 | @Provides 22 | fun provideLocaleManager(): LocaleManager { 23 | return LocaleManager() 24 | } 25 | 26 | @Singleton 27 | @Provides 28 | fun provideSharedPreferences(app: Application): SharedPreferences { 29 | return app.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE) 30 | } 31 | 32 | @Singleton 33 | @Provides 34 | fun provideAppInfoDb(app: Application): AppInfoDb { 35 | return Room.databaseBuilder(app, AppInfoDb::class.java, "app-info-db").build() 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/service/Connection.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.service 2 | 3 | import android.content.ComponentName 4 | import android.content.ServiceConnection 5 | import android.os.IBinder 6 | import vegabobo.languageselector.IUserService 7 | 8 | class Connection : ServiceConnection { 9 | 10 | var SERVICE: IUserService? = null 11 | fun set(service: IUserService?) { 12 | if (SERVICE == null) { 13 | SERVICE = service 14 | } 15 | } 16 | 17 | override fun onServiceConnected(name: ComponentName?, service: IBinder?) { 18 | set(IUserService.Stub.asInterface(service)) 19 | } 20 | 21 | override fun onServiceDisconnected(name: ComponentName?) { 22 | SERVICE = null 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/service/RootUserService.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.service 2 | 3 | import android.content.Intent 4 | import android.os.IBinder 5 | import com.topjohnwu.superuser.ipc.RootService 6 | 7 | class RootUserService : RootService() { 8 | override fun onBind(intent: Intent): IBinder { 9 | return UserService() 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/service/UserService.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.service 2 | 3 | import android.app.ActivityManager 4 | import android.app.IActivityManager 5 | import android.app.IActivityTaskManager 6 | import android.app.ILocaleManager 7 | import android.os.Build 8 | import android.os.LocaleList 9 | import android.os.Process 10 | import android.util.Log 11 | import rikka.shizuku.SystemServiceHelper 12 | import vegabobo.languageselector.BuildConfig 13 | import vegabobo.languageselector.IUserService 14 | import kotlin.system.exitProcess 15 | 16 | 17 | class UserService : IUserService.Stub() { 18 | 19 | override fun exit() { 20 | destroy() 21 | } 22 | 23 | override fun destroy() { 24 | exitProcess(0) 25 | } 26 | 27 | override fun getUid(): Int { 28 | return Process.myUid() 29 | } 30 | 31 | var LOCALE_MANAGER: ILocaleManager? = null 32 | fun requiresLocaleManager() { 33 | if (LOCALE_MANAGER != null) return 34 | val localeBinder = SystemServiceHelper.getSystemService("locale") 35 | LOCALE_MANAGER = ILocaleManager.Stub.asInterface(localeBinder) 36 | } 37 | 38 | override fun setApplicationLocales(packageName: String?, locales: LocaleList?) { 39 | requiresLocaleManager() 40 | val currentUser = ActivityManager.getCurrentUser() 41 | if (Build.VERSION.SDK_INT == 33 && Build.VERSION.RELEASE_OR_CODENAME != "UpsideDownCake") { 42 | LOCALE_MANAGER!!.setApplicationLocales(packageName, currentUser, locales) 43 | return 44 | } 45 | LOCALE_MANAGER!!.setApplicationLocales(packageName, currentUser, locales, true) 46 | } 47 | 48 | override fun getApplicationLocales(packageName: String?): LocaleList { 49 | requiresLocaleManager() 50 | val currentUser = ActivityManager.getCurrentUser() 51 | return LOCALE_MANAGER!!.getApplicationLocales(packageName, currentUser) 52 | } 53 | 54 | override fun getSystemLocales(): LocaleList { 55 | requiresLocaleManager() 56 | return LOCALE_MANAGER!!.systemLocales 57 | } 58 | 59 | var ACTIVITY_MANAGER: IActivityManager? = null 60 | fun requiresActivityManager() { 61 | if (ACTIVITY_MANAGER != null) return 62 | val am = SystemServiceHelper.getSystemService("activity") 63 | ACTIVITY_MANAGER = IActivityManager.Stub.asInterface(am) 64 | } 65 | 66 | override fun forceStopPackage(packageName: String?) { 67 | requiresActivityManager() 68 | val currentUser = ActivityManager.getCurrentUser() 69 | ACTIVITY_MANAGER!!.forceStopPackage(packageName, currentUser) 70 | } 71 | 72 | var ACTIVITY_TASK_MANAGER: IActivityTaskManager? = null 73 | fun requiresActivityTaskManager() { 74 | if (ACTIVITY_TASK_MANAGER != null) return 75 | val am = SystemServiceHelper.getSystemService("activity_task") 76 | ACTIVITY_TASK_MANAGER = IActivityTaskManager.Stub.asInterface(am) 77 | } 78 | 79 | override fun getFirstRunningTaskPackage(): String { 80 | requiresActivityTaskManager() 81 | val runningTask = 82 | try { 83 | ACTIVITY_TASK_MANAGER!!.getTasks(1, false, false, -1).first() 84 | } catch (e: NoSuchMethodError) { 85 | Log.w( 86 | BuildConfig.APPLICATION_ID, 87 | "getTasks failed, trying again without displayId, error: ${e.stackTraceToString()}" 88 | ) 89 | ACTIVITY_TASK_MANAGER!!.getTasks(1, false, false).first() 90 | } 91 | return runningTask.topActivity?.packageName ?: "" 92 | } 93 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/service/UserServiceProvider.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.service 2 | 3 | import android.util.Log 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.launch 8 | import vegabobo.languageselector.IUserService 9 | import vegabobo.languageselector.ui.screen.main.OperationMode 10 | 11 | object UserServiceProvider { 12 | 13 | private val tag = this.javaClass.simpleName 14 | 15 | var connection = Connection() 16 | var opMode = OperationMode.NONE 17 | 18 | // Blocking 19 | fun getService(): IUserService { 20 | var timeout = 0 21 | while (!isConnected()) { 22 | timeout += 1000 23 | if (timeout > 20000) { 24 | throw Exception("Service unavailable.") 25 | } 26 | Thread.sleep(1000) 27 | } 28 | return this.connection.SERVICE!! 29 | } 30 | 31 | fun run( 32 | onFail: () -> Unit = {}, 33 | onConnected: suspend IUserService.() -> Unit, 34 | ) { 35 | fun service() = connection.SERVICE!! 36 | CoroutineScope(Dispatchers.IO).launch { 37 | if (isConnected()) { 38 | onConnected(service()) 39 | return@launch 40 | } 41 | var timeout = 0 42 | while (!isConnected()) { 43 | timeout += 1000 44 | if (timeout > 20000) { 45 | Log.e(tag, "Service unavailable.") 46 | onFail() 47 | return@launch 48 | } 49 | delay(1000) 50 | Log.d(tag, "Service unavailable, checking again in 1s.. [${timeout / 1000}s/20s]") 51 | } 52 | val serviceUid = service().uid 53 | Log.d(tag, "IUserService available, uid: $serviceUid") 54 | if(serviceUid == 0) 55 | opMode = OperationMode.ROOT 56 | if(serviceUid <= 2000) 57 | opMode = OperationMode.SHIZUKU 58 | onConnected(service()) 59 | } 60 | } 61 | 62 | fun isConnected(): Boolean { 63 | return connection.SERVICE != null 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/components/AppListItem.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.components 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.clip 20 | import androidx.compose.ui.graphics.asImageBitmap 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import androidx.core.graphics.drawable.toBitmap 25 | import vegabobo.languageselector.ui.screen.main.AppInfo 26 | 27 | @Composable 28 | fun AppListItem( 29 | modifier: Modifier = Modifier, 30 | app: AppInfo, 31 | onClickApp: (String) -> Unit 32 | ) { 33 | Row( 34 | modifier = Modifier 35 | .clickable { onClickApp(app.pkg) } 36 | .then(modifier), 37 | verticalAlignment = Alignment.CenterVertically 38 | ) { 39 | Image( 40 | modifier = Modifier.size(32.dp), 41 | bitmap = app.icon.toBitmap().asImageBitmap(), 42 | contentDescription = "app icon" 43 | ) 44 | Spacer(modifier = Modifier.padding(8.dp)) 45 | Column( 46 | modifier = Modifier.weight(1f), 47 | verticalArrangement = Arrangement.spacedBy((-4).dp) 48 | ) { 49 | Text(text = app.name, fontSize = 18.sp, fontWeight = FontWeight.Medium, maxLines = 1) 50 | Text(text = app.pkg, fontSize = 12.sp, maxLines = 1) 51 | Row { 52 | TextLabel(text = if (app.isSystemApp()) "System App" else "User App") 53 | if (app.isModified()) 54 | TextLabel(text = "Modified") 55 | } 56 | } 57 | } 58 | } 59 | 60 | @Composable 61 | fun TextLabel(text: String) { 62 | Box(Modifier.padding(top = 2.dp, end = 4.dp, bottom = 4.dp)) { 63 | Box( 64 | Modifier 65 | .clip(RoundedCornerShape(8.dp)) 66 | .background(MaterialTheme.colorScheme.onPrimary) 67 | ) { 68 | Text( 69 | modifier = Modifier.padding(start = 4.dp, end = 4.dp, top = 2.dp, bottom = 2.dp), 70 | text = text, 71 | maxLines = 1, 72 | lineHeight = 16.sp, 73 | fontSize = 10.sp 74 | ) 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/components/AppSearchBar.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.components 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.compose.foundation.horizontalScroll 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.RowScope 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.lazy.LazyColumn 12 | import androidx.compose.foundation.rememberScrollState 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.filled.Search 15 | import androidx.compose.material3.ExperimentalMaterial3Api 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.MaterialTheme 18 | import androidx.compose.material3.SearchBar 19 | import androidx.compose.material3.SearchBarDefaults 20 | import androidx.compose.material3.Text 21 | import androidx.compose.material3.TextButton 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.ui.Alignment 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.draw.alpha 26 | import androidx.compose.ui.semantics.isTraversalGroup 27 | import androidx.compose.ui.semantics.semantics 28 | import androidx.compose.ui.text.font.FontWeight 29 | import androidx.compose.ui.text.style.TextAlign 30 | import androidx.compose.ui.unit.dp 31 | import androidx.compose.ui.unit.sp 32 | import vegabobo.languageselector.ui.screen.main.AppInfo 33 | import vegabobo.languageselector.ui.screen.main.AppLabels 34 | 35 | @OptIn(ExperimentalMaterial3Api::class) 36 | @Composable 37 | fun AppSearchBar( 38 | modifier: Modifier = Modifier, 39 | placeholder: String = "", 40 | query: String, 41 | onUpdatedValue: (String) -> Unit, 42 | apps: List = emptyList(), 43 | history: List = emptyList(), 44 | isExpanded: Boolean, 45 | onExpandedChange: (Boolean) -> Unit, 46 | selectedLabels: List, 47 | onSelectedLabelsChange: (AppLabels) -> Unit, 48 | onClickApp: (AppInfo) -> Unit, 49 | onClickClear: () -> Unit, 50 | actions: @Composable RowScope.() -> Unit, 51 | ) { 52 | SearchBar( 53 | modifier = Modifier 54 | .semantics { isTraversalGroup = true } 55 | .then(modifier), 56 | inputField = { 57 | SearchBarDefaults.InputField( 58 | onSearch = { onUpdatedValue(it) }, 59 | expanded = isExpanded, 60 | onExpandedChange = { onExpandedChange(it) }, 61 | placeholder = { Text(placeholder) }, 62 | leadingIcon = { 63 | Icon( 64 | imageVector = Icons.Default.Search, 65 | contentDescription = null 66 | ) 67 | }, 68 | trailingIcon = { 69 | Row { actions() } 70 | }, 71 | query = query, 72 | onQueryChange = { onUpdatedValue(it) } 73 | ) 74 | }, 75 | expanded = isExpanded, 76 | onExpandedChange = { onExpandedChange(it) }, 77 | ) { 78 | LazyColumn { 79 | if (query.isNotBlank()) { 80 | item { 81 | Row( 82 | modifier = Modifier 83 | .padding( 84 | start = 23.dp, 85 | top = 8.dp, 86 | bottom = 8.dp, 87 | end = 8.dp 88 | ) 89 | .horizontalScroll(rememberScrollState()) 90 | ) { 91 | FilterLabel( 92 | title = "Show System", 93 | onClick = { 94 | onSelectedLabelsChange(AppLabels.SYSTEM_APP) 95 | }, 96 | isSelected = selectedLabels.contains(AppLabels.SYSTEM_APP) 97 | ) 98 | Spacer(Modifier.padding(8.dp)) 99 | FilterLabel( 100 | title = "Show Modified", 101 | onClick = { onSelectedLabelsChange(AppLabels.MODIFIED) }, 102 | isSelected = selectedLabels.contains(AppLabels.MODIFIED) 103 | ) 104 | } 105 | } 106 | 107 | items(apps.size) { 108 | val app = apps[it] 109 | if (filter(query, app, selectedLabels)) 110 | return@items 111 | AppListItem( 112 | modifier = Modifier.padding( 113 | start = 23.dp, 114 | end = 23.dp, 115 | top = 4.dp, 116 | bottom = 4.dp 117 | ), 118 | app = app, 119 | onClickApp = { onClickApp(app) } 120 | ) 121 | } 122 | } else if (history.isNotEmpty()) { 123 | item { 124 | Row( 125 | Modifier.padding(4.dp), 126 | verticalAlignment = Alignment.CenterVertically, 127 | horizontalArrangement = Arrangement.Center 128 | ) { 129 | Text( 130 | text = "History".uppercase(), 131 | fontSize = 14.sp, 132 | fontWeight = FontWeight.Medium, 133 | letterSpacing = 1.sp, 134 | color = MaterialTheme.colorScheme.secondary, 135 | modifier = modifier 136 | .padding(start = 18.dp) 137 | .padding(bottom = 8.dp) 138 | .padding(top = 8.dp) 139 | ) 140 | Spacer(modifier = Modifier.weight(1f)) 141 | TextButton(onClick = { onClickClear() }) { 142 | Row( 143 | verticalAlignment = Alignment.CenterVertically, 144 | horizontalArrangement = Arrangement.Center 145 | ) { 146 | Text(text = "Clear") 147 | } 148 | } 149 | Spacer(modifier = Modifier.padding(6.dp)) 150 | } 151 | } 152 | items(history.size) { 153 | val app = history[it] 154 | AppListItem( 155 | modifier = Modifier.padding( 156 | start = 23.dp, 157 | end = 23.dp, 158 | top = 4.dp, 159 | bottom = 4.dp 160 | ), 161 | app = app, 162 | onClickApp = { onClickApp(app) } 163 | ) 164 | } 165 | item { 166 | Row(modifier = Modifier.fillMaxWidth()) { 167 | Spacer(Modifier.weight(1f)) 168 | 169 | } 170 | } 171 | } else { 172 | item { 173 | Text( 174 | modifier = Modifier 175 | .fillMaxWidth() 176 | .padding(10.dp) 177 | .alpha(0.4f), 178 | text = "Type something to search", 179 | textAlign = TextAlign.Center 180 | ) 181 | } 182 | } 183 | } 184 | } 185 | 186 | if (query.isNotBlank()) 187 | BackHandler { 188 | onUpdatedValue("") 189 | } 190 | } 191 | 192 | fun filter(query: String, app: AppInfo, cLabels: List): Boolean { 193 | if (cLabels.contains(AppLabels.MODIFIED) && !app.labels.contains(AppLabels.MODIFIED)) 194 | return true 195 | 196 | if (!cLabels.contains(AppLabels.SYSTEM_APP) && app.labels.contains(AppLabels.SYSTEM_APP)) 197 | return true 198 | 199 | val lQuery = query.lowercase() 200 | return !(app.pkg.lowercase().contains(lQuery) || app.name.lowercase().contains(lQuery)) 201 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/components/BackButton.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.components 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.automirrored.outlined.ArrowBack 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.IconButton 7 | import androidx.compose.runtime.Composable 8 | 9 | @Composable 10 | fun BackButton( 11 | onClick: () -> Unit 12 | ){ 13 | IconButton(onClick = { onClick() }) { 14 | Icon( 15 | imageVector = Icons.AutoMirrored.Outlined.ArrowBack, 16 | contentDescription = "Back arrow" 17 | ) 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/components/FilterLabel.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.components 2 | 3 | import androidx.compose.foundation.layout.size 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.Done 6 | import androidx.compose.material3.FilterChip 7 | import androidx.compose.material3.FilterChipDefaults 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | 13 | @Composable 14 | fun FilterLabel( 15 | title: String, 16 | onClick: (Boolean) -> Unit, 17 | isSelected: Boolean 18 | ) { 19 | FilterChip( 20 | onClick = { onClick(isSelected) }, 21 | label = { Text(title) }, 22 | selected = isSelected, 23 | leadingIcon = if (isSelected) { 24 | { 25 | Icon( 26 | imageVector = Icons.Filled.Done, 27 | contentDescription = "Done icon", 28 | modifier = Modifier.size(FilterChipDefaults.IconSize) 29 | ) 30 | } 31 | } else { 32 | null 33 | }, 34 | ) 35 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/components/LocaleItemList.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.components 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.combinedClickable 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.unit.sp 15 | 16 | @OptIn(ExperimentalFoundationApi::class) 17 | @Composable 18 | fun LocaleItemList( 19 | itemText: String, 20 | onLongClick: () -> Unit = {}, 21 | onClick: () -> Unit 22 | ) { 23 | Box( 24 | modifier = Modifier 25 | .combinedClickable( 26 | onClick = { onClick() }, 27 | onLongClick = { onLongClick() } 28 | ) 29 | .fillMaxWidth() 30 | .height(72.dp) 31 | .padding(18.dp) 32 | ) { 33 | Text( 34 | modifier = Modifier.align(Alignment.CenterStart), 35 | text = itemText, 36 | fontSize = 19.sp 37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/components/QuickTextButton.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.graphics.vector.ImageVector 18 | import androidx.compose.ui.text.style.TextAlign 19 | import androidx.compose.ui.unit.dp 20 | 21 | @Composable 22 | fun QuickTextButton( 23 | modifier: Modifier = Modifier, 24 | onClick: () -> Unit, 25 | icon: ImageVector, 26 | text: String 27 | ) { 28 | Column( 29 | modifier = Modifier 30 | .clip(RoundedCornerShape(12.dp)) 31 | .clickable { onClick() } 32 | .padding(18.dp) 33 | .then(modifier), 34 | verticalArrangement = Arrangement.Center, 35 | horizontalAlignment = Alignment.CenterHorizontally 36 | ) { 37 | Icon( 38 | modifier = Modifier.size(28.dp), 39 | imageVector = icon, 40 | contentDescription = text, 41 | tint = MaterialTheme.colorScheme.primary 42 | ) 43 | Spacer(modifier = Modifier.padding(2.dp)) 44 | Text(textAlign = TextAlign.Center, text = text, color = MaterialTheme.colorScheme.primary) 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/components/Title.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.text.font.FontWeight 9 | import androidx.compose.ui.unit.dp 10 | import androidx.compose.ui.unit.sp 11 | 12 | @Composable 13 | fun Title(title: String, modifier: Modifier = Modifier) { 14 | Text( 15 | text = title, 16 | fontSize = 14.sp, 17 | fontWeight = FontWeight.Medium, 18 | color = MaterialTheme.colorScheme.secondary, 19 | modifier = modifier 20 | .padding(start = 18.dp) 21 | .padding(bottom = 8.dp) 22 | .padding(top = 8.dp) 23 | ) 24 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/screen/BaseScreen.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.screen 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.RowScope 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.Scaffold 8 | import androidx.compose.material3.SnackbarHost 9 | import androidx.compose.material3.SnackbarHostState 10 | import androidx.compose.material3.Text 11 | import androidx.compose.material3.TopAppBar 12 | import androidx.compose.material3.TopAppBarDefaults 13 | import androidx.compose.material3.TopAppBarScrollBehavior 14 | import androidx.compose.material3.rememberTopAppBarState 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.input.nestedscroll.nestedScroll 18 | 19 | @OptIn(ExperimentalMaterial3Api::class) 20 | @Composable 21 | fun BaseScreen( 22 | modifier: Modifier = Modifier, 23 | title: String? = null, 24 | snackBarHost: SnackbarHostState = SnackbarHostState(), 25 | topBar: (@Composable (TopAppBarScrollBehavior) -> Unit)? = null, 26 | navIcon: (@Composable () -> Unit)? = null, 27 | actions: @Composable RowScope.() -> Unit = {}, 28 | content: @Composable (PaddingValues) -> Unit, 29 | ) { 30 | val defScrollBehavior = topBar != null || title?.isNotEmpty() == true 31 | val scrollBehavior = 32 | if (defScrollBehavior) 33 | TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) 34 | else 35 | null 36 | 37 | val sbMod = 38 | if (defScrollBehavior) 39 | Modifier.nestedScroll(scrollBehavior!!.nestedScrollConnection) 40 | else 41 | Modifier 42 | 43 | Scaffold( 44 | modifier = Modifier 45 | .fillMaxSize() 46 | .then(sbMod) 47 | .then(modifier), 48 | snackbarHost = { SnackbarHost(hostState = snackBarHost) }, 49 | topBar = { 50 | if (topBar != null) { 51 | topBar(scrollBehavior!!) 52 | } else if (title?.isNotEmpty() == true) { 53 | TopAppBar( 54 | scrollBehavior = scrollBehavior, 55 | navigationIcon = { navIcon?.invoke() }, 56 | title = { Text(title) }, 57 | actions = { actions(this) } 58 | ) 59 | } 60 | }, 61 | content = { content(it) } 62 | ) 63 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/screen/Navigation.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.screen 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.navigation.NavType 5 | import androidx.navigation.compose.NavHost 6 | import androidx.navigation.compose.composable 7 | import androidx.navigation.compose.rememberNavController 8 | import androidx.navigation.navArgument 9 | import vegabobo.languageselector.ui.screen.Destinations.ABOUT 10 | import vegabobo.languageselector.ui.screen.Destinations.APP_INFO 11 | import vegabobo.languageselector.ui.screen.Destinations.HOME 12 | import vegabobo.languageselector.ui.screen.about.AboutScreen 13 | import vegabobo.languageselector.ui.screen.appinfo.AppInfoScreen 14 | import vegabobo.languageselector.ui.screen.main.MainScreen 15 | 16 | object Destinations { 17 | const val HOME = "home" 18 | const val APP_INFO = "app_info" 19 | const val ABOUT = "about" 20 | } 21 | 22 | @Composable 23 | fun Navigation() { 24 | val navController = rememberNavController() 25 | NavHost( 26 | navController = navController, 27 | startDestination = HOME 28 | ) { 29 | composable( 30 | route = HOME 31 | ) { 32 | MainScreen( 33 | navigateToAppScreen = { navController.navigate("$APP_INFO/$it") }, 34 | navigateToAbout = { navController.navigate(ABOUT)} 35 | ) 36 | } 37 | 38 | composable( 39 | route = "$APP_INFO/{app_id}", 40 | arguments = listOf(navArgument("app_id") { type = NavType.StringType }) 41 | ) { backStackEntry -> 42 | val appId = backStackEntry.arguments?.getString("app_id") ?: return@composable 43 | AppInfoScreen(appId = appId, navigateBack = { navController.navigateUp() }) 44 | } 45 | 46 | composable(route = ABOUT) { 47 | AboutScreen(navigateBack = { navController.navigateUp() }) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/screen/about/AboutScreen.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.screen.about 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.size 14 | import androidx.compose.foundation.lazy.LazyColumn 15 | import androidx.compose.material3.ExperimentalMaterial3Api 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.MaterialTheme 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.ui.Alignment 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.graphics.asImageBitmap 25 | import androidx.compose.ui.graphics.vector.ImageVector 26 | import androidx.compose.ui.platform.LocalContext 27 | import androidx.compose.ui.platform.LocalUriHandler 28 | import androidx.compose.ui.res.stringResource 29 | import androidx.compose.ui.unit.dp 30 | import androidx.compose.ui.unit.sp 31 | import androidx.core.graphics.drawable.toBitmap 32 | import vegabobo.languageselector.R 33 | import vegabobo.languageselector.ui.components.BackButton 34 | import vegabobo.languageselector.ui.components.Title 35 | import vegabobo.languageselector.ui.screen.BaseScreen 36 | import vegabobo.languageselector.ui.screen.main.getAppIcon 37 | import com.mikepenz.aboutlibraries.Libs 38 | import com.mikepenz.aboutlibraries.util.withContext 39 | import vegabobo.languageselector.BuildConfig 40 | 41 | @OptIn(ExperimentalMaterial3Api::class) 42 | @Composable 43 | fun AboutScreen( 44 | navigateBack: () -> Unit 45 | ) { 46 | val libs = remember { mutableStateOf(null) } 47 | val context = LocalContext.current 48 | val uriHandler = LocalUriHandler.current 49 | libs.value = Libs.Builder().withContext(context).build() 50 | val libraries = libs.value!!.libraries 51 | 52 | BaseScreen( 53 | title = stringResource(R.string.about), 54 | navIcon = { BackButton { navigateBack() } } 55 | ) { 56 | LazyColumn( 57 | modifier = Modifier 58 | .fillMaxSize() 59 | .padding(top = it.calculateTopPadding()) 60 | ) { 61 | item { 62 | Column( 63 | modifier = Modifier.fillMaxWidth(), 64 | verticalArrangement = Arrangement.Center, 65 | horizontalAlignment = Alignment.CenterHorizontally 66 | ) { 67 | Image( 68 | modifier = Modifier.size(96.dp), 69 | bitmap = context.packageManager 70 | .getAppIcon(context.applicationInfo) 71 | .toBitmap().asImageBitmap(), 72 | contentDescription = "App icon" 73 | ) 74 | Text(text = stringResource(R.string.app_name), fontSize = 22.sp) 75 | Text( 76 | stringResource(R.string.version).format( 77 | BuildConfig.VERSION_NAME, 78 | BuildConfig.VERSION_CODE 79 | ) 80 | ) 81 | } 82 | } 83 | item { 84 | Title(stringResource(id = R.string.app)) 85 | PreferenceItem( 86 | title = stringResource(R.string.ghrepo), 87 | description = stringResource(R.string.view_source) 88 | ) { 89 | uriHandler.openUri("https://github.com/VegaBobo/Language-Selector") 90 | } 91 | } 92 | item { Title(stringResource(R.string.deps_libs)) } 93 | items(libraries.size) { 94 | val thisLibrary = libraries[it] 95 | val name = thisLibrary.name 96 | var licenses = "" 97 | for (license in thisLibrary.licenses) { 98 | licenses += license.name 99 | } 100 | val urlToOpen = thisLibrary.website ?: "" 101 | PreferenceItem( 102 | title = name, 103 | description = licenses, 104 | onClick = { 105 | if (urlToOpen.isNotEmpty()) { 106 | uriHandler.openUri(urlToOpen) 107 | } 108 | }, 109 | ) 110 | } 111 | item { Spacer(modifier = Modifier.padding(bottom = it.calculateBottomPadding())) } 112 | } 113 | } 114 | 115 | } 116 | 117 | @Composable 118 | fun PreferenceItem( 119 | title: String, 120 | description: String, 121 | icon: ImageVector? = null, 122 | onClick: () -> Unit 123 | ) { 124 | Row( 125 | verticalAlignment = Alignment.CenterVertically, 126 | modifier = Modifier 127 | .fillMaxWidth() 128 | .clickable(onClick = onClick) 129 | .padding( 130 | start = 24.dp, 131 | top = 16.dp, 132 | bottom = 16.dp, 133 | end = 16.dp 134 | ) 135 | ) { 136 | if (icon != null) { 137 | Icon( 138 | imageVector = icon, 139 | contentDescription = null, 140 | modifier = Modifier.padding(end = 16.dp), 141 | ) 142 | } 143 | Column { 144 | Text( 145 | text = title, 146 | style = MaterialTheme.typography.titleLarge 147 | ) 148 | Spacer(Modifier.height(2.dp)) 149 | Text( 150 | text = description, 151 | style = MaterialTheme.typography.bodyMedium 152 | ) 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/screen/appinfo/AppInfoScreen.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.screen.appinfo 2 | 3 | import android.graphics.BitmapFactory 4 | import android.widget.Toast 5 | import androidx.activity.compose.BackHandler 6 | import androidx.compose.animation.animateContentSize 7 | import androidx.compose.foundation.Image 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.Row 11 | import androidx.compose.foundation.layout.Spacer 12 | import androidx.compose.foundation.layout.fillMaxWidth 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.lazy.LazyColumn 16 | import androidx.compose.foundation.lazy.rememberLazyListState 17 | import androidx.compose.material.icons.Icons 18 | import androidx.compose.material.icons.automirrored.outlined.OpenInNew 19 | import androidx.compose.material.icons.outlined.Close 20 | import androidx.compose.material.icons.outlined.Settings 21 | import androidx.compose.material3.ExperimentalMaterial3Api 22 | import androidx.compose.material3.Text 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.LaunchedEffect 25 | import androidx.compose.runtime.collectAsState 26 | import androidx.compose.runtime.getValue 27 | import androidx.compose.runtime.rememberCoroutineScope 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.graphics.asImageBitmap 31 | import androidx.compose.ui.platform.LocalContext 32 | import androidx.compose.ui.res.stringResource 33 | import androidx.compose.ui.unit.dp 34 | import androidx.compose.ui.unit.sp 35 | import androidx.core.graphics.drawable.toBitmap 36 | import androidx.hilt.navigation.compose.hiltViewModel 37 | import vegabobo.languageselector.R 38 | import vegabobo.languageselector.ui.components.BackButton 39 | import vegabobo.languageselector.ui.components.LocaleItemList 40 | import vegabobo.languageselector.ui.components.QuickTextButton 41 | import vegabobo.languageselector.ui.components.Title 42 | import vegabobo.languageselector.ui.screen.BaseScreen 43 | import kotlinx.coroutines.launch 44 | 45 | @OptIn(ExperimentalMaterial3Api::class) 46 | @Composable 47 | fun AppInfoScreen( 48 | appId: String, 49 | navigateBack: () -> Unit, 50 | appInfoVm: AppInfoVm = hiltViewModel(), 51 | ) { 52 | val uiState by appInfoVm.uiState.collectAsState() 53 | val ctx = LocalContext.current 54 | val listState = rememberLazyListState() 55 | val coroutineScope = rememberCoroutineScope() 56 | 57 | fun pinToast(locale: String) { 58 | val pinTxt = 59 | ctx.resources.getString(R.string.pinned_ok).format(locale) 60 | Toast.makeText(ctx, pinTxt, Toast.LENGTH_SHORT).show() 61 | } 62 | 63 | fun unpinToast(locale: String) { 64 | val pinTxt = 65 | ctx.resources.getString(R.string.unpinned).format(locale) 66 | Toast.makeText(ctx, pinTxt, Toast.LENGTH_SHORT).show() 67 | } 68 | 69 | LaunchedEffect(Unit) { 70 | appInfoVm.initFromAppId(appId) 71 | appInfoVm.updatePinnedLangsFromSP() 72 | } 73 | BaseScreen( 74 | title = stringResource(R.string.app_language), 75 | navIcon = { 76 | BackButton { navigateBack() } 77 | } 78 | ) { 79 | LazyColumn( 80 | state = listState, 81 | modifier = Modifier 82 | .padding(top = it.calculateTopPadding()) 83 | .animateContentSize(), 84 | ) { 85 | item { 86 | Row( 87 | modifier = Modifier 88 | .fillMaxWidth() 89 | .padding(start = 18.dp, end = 18.dp), 90 | verticalAlignment = Alignment.CenterVertically 91 | ) { 92 | Image( 93 | modifier = Modifier.size(84.dp), 94 | bitmap = uiState.appIcon?.toBitmap()?.asImageBitmap() 95 | ?: BitmapFactory.decodeResource( 96 | ctx.resources, R.drawable.icon_placeholder 97 | ).asImageBitmap(), 98 | contentDescription = "App icon" 99 | ) 100 | Column( 101 | modifier = Modifier 102 | .padding(18.dp) 103 | .weight(1f) 104 | ) { 105 | Text(text = uiState.appName, fontSize = 22.sp, maxLines = 1) 106 | Text(text = uiState.appPackage, fontSize = 14.sp, maxLines = 1) 107 | Text( 108 | text = uiState.currentLanguage.ifEmpty { stringResource(R.string.system_default) }, 109 | fontSize = 14.sp, 110 | maxLines = 1 111 | ) 112 | } 113 | } 114 | } 115 | 116 | item { 117 | Row( 118 | modifier = Modifier 119 | .fillMaxWidth() 120 | .padding(8.dp), 121 | verticalAlignment = Alignment.CenterVertically, 122 | horizontalArrangement = Arrangement.Center 123 | ) { 124 | QuickTextButton( 125 | modifier = Modifier.weight(1f), 126 | onClick = { appInfoVm.onClickOpen() }, 127 | icon = Icons.AutoMirrored.Outlined.OpenInNew, 128 | text = stringResource(R.string.open) 129 | ) 130 | QuickTextButton( 131 | modifier = Modifier.weight(1f), 132 | onClick = { appInfoVm.onClickForceClose() }, 133 | icon = Icons.Outlined.Close, 134 | text = stringResource(R.string.close) 135 | ) 136 | QuickTextButton( 137 | modifier = Modifier.weight(1f), 138 | onClick = { appInfoVm.onClickSettings() }, 139 | icon = Icons.Outlined.Settings, 140 | text = stringResource(R.string.settings) 141 | ) 142 | } 143 | } 144 | 145 | if (uiState.selectedLanguage != -1) { 146 | item { Title(stringResource(R.string.region)) } 147 | items(uiState.listOfAllLanguages[uiState.selectedLanguage].locales.size) { index -> 148 | val thisLangReg = 149 | uiState.listOfAllLanguages[uiState.selectedLanguage].locales[index] 150 | LocaleItemList( 151 | itemText = thisLangReg.name, 152 | onClick = { 153 | appInfoVm.onClickLocale(thisLangReg) 154 | appInfoVm.onBackWhenSelectedLang() 155 | coroutineScope.launch { listState.scrollToItem(0) } 156 | }, 157 | onLongClick = { 158 | pinToast(thisLangReg.name) 159 | appInfoVm.onPinLang(thisLangReg) 160 | } 161 | ) 162 | } 163 | } else { 164 | if (uiState.listOfPinnedLanguages.size != 0) { 165 | item { Title(stringResource(R.string.pinned)) } 166 | items(uiState.listOfPinnedLanguages.size) { index -> 167 | val thisLanguage = uiState.listOfPinnedLanguages[index] 168 | LocaleItemList( 169 | itemText = thisLanguage.name, 170 | onClick = { appInfoVm.onClickLocale(thisLanguage) }, 171 | onLongClick = { 172 | unpinToast(thisLanguage.name) 173 | appInfoVm.onRemovePin(thisLanguage) 174 | } 175 | ) 176 | } 177 | } 178 | 179 | item { Title(stringResource(R.string.user_languages)) } 180 | item { 181 | LocaleItemList(stringResource(R.string.system_default)) { appInfoVm.onClickResetLang() } 182 | } 183 | items(uiState.listOfSuggestedLanguages.size) { index -> 184 | val thisLanguage = uiState.listOfSuggestedLanguages[index] 185 | LocaleItemList( 186 | itemText = thisLanguage.name, 187 | onClick = { appInfoVm.onClickLocale(thisLanguage) }, 188 | onLongClick = { 189 | pinToast(thisLanguage.name) 190 | appInfoVm.onPinLang(thisLanguage) 191 | } 192 | ) 193 | } 194 | 195 | item { Title(stringResource(R.string.all_languages)) } 196 | items(uiState.listOfAllLanguages.size) { index -> 197 | val thisLanguage = uiState.listOfAllLanguages[index] 198 | LocaleItemList(thisLanguage.language) { 199 | appInfoVm.onClickSingleLanguage(index) 200 | coroutineScope.launch { listState.scrollToItem(0) } 201 | } 202 | } 203 | } 204 | item { Spacer(modifier = Modifier.padding(it.calculateBottomPadding())) } 205 | } 206 | } 207 | 208 | if (uiState.selectedLanguage != -1) 209 | BackHandler { appInfoVm.onBackWhenSelectedLang() } 210 | 211 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/screen/appinfo/AppInfoState.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.screen.appinfo 2 | 3 | import android.graphics.drawable.Drawable 4 | import androidx.compose.runtime.mutableStateListOf 5 | import java.util.Locale 6 | 7 | data class LocaleRegion( 8 | val language: String, 9 | val locales: ArrayList 10 | ) 11 | 12 | data class SingleLocale( 13 | val name: String, 14 | val languageTag: String 15 | ) { 16 | fun toLocale(): Locale { 17 | return Locale.forLanguageTag(languageTag) 18 | } 19 | } 20 | 21 | data class AppInfoState( 22 | val appIcon: Drawable? = null, 23 | val appName: String = "", 24 | val appPackage: String = "", 25 | val currentLanguage: String = "", 26 | val listOfSuggestedLanguages: MutableList = mutableStateListOf(), 27 | val listOfPinnedLanguages: MutableList = mutableStateListOf(), 28 | val selectedLanguage: Int = -1, 29 | val listOfAllLanguages: MutableList = mutableStateListOf(), 30 | ) -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/screen/appinfo/AppInfoVm.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.screen.appinfo 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.SharedPreferences 7 | import android.content.pm.ApplicationInfo 8 | import android.content.pm.PackageManager 9 | import android.net.Uri 10 | import android.os.LocaleList 11 | import android.provider.Settings 12 | import android.util.Log 13 | import androidx.lifecycle.ViewModel 14 | import vegabobo.languageselector.LocaleManager 15 | import vegabobo.languageselector.service.UserServiceProvider 16 | import vegabobo.languageselector.ui.screen.main.getAppIcon 17 | import vegabobo.languageselector.ui.screen.main.getLabel 18 | import dagger.hilt.android.lifecycle.HiltViewModel 19 | import kotlinx.coroutines.flow.MutableStateFlow 20 | import kotlinx.coroutines.flow.StateFlow 21 | import kotlinx.coroutines.flow.asStateFlow 22 | import kotlinx.coroutines.flow.update 23 | import vegabobo.languageselector.BuildConfig 24 | import java.util.Locale 25 | import javax.inject.Inject 26 | 27 | object PrefConstants { 28 | const val PINNED_LOCALES = "pinned_locales" 29 | } 30 | 31 | 32 | @HiltViewModel 33 | class AppInfoVm @Inject constructor( 34 | val app: Application, 35 | val localeManager: LocaleManager 36 | ) : ViewModel() { 37 | private val _uiState = MutableStateFlow(AppInfoState()) 38 | val uiState: StateFlow = _uiState.asStateFlow() 39 | 40 | lateinit var appInfo: ApplicationInfo 41 | 42 | fun initFromAppId(appId: String) { 43 | appInfo = 44 | app.packageManager.getApplicationInfo(appId, PackageManager.ApplicationInfoFlags.of(0)) 45 | _uiState.update { 46 | it.copy( 47 | appName = app.packageManager.getLabel(appInfo), 48 | appPackage = appInfo.packageName, 49 | appIcon = app.packageManager.getAppIcon(appInfo) 50 | ) 51 | } 52 | 53 | UserServiceProvider.run { 54 | _uiState.value.listOfSuggestedLanguages.clear() 55 | for (locale in 0 until systemLocales.size()) { 56 | val thisLocale = systemLocales[locale] 57 | val thisLLI = 58 | SingleLocale(thisLocale.capDisplayName(), thisLocale.toLanguageTag()) 59 | _uiState.value.listOfSuggestedLanguages.add(thisLLI) 60 | updateCurrentLanguageState() 61 | } 62 | } 63 | 64 | _uiState.update { it.copy(listOfAllLanguages = localeManager.localeList) } 65 | } 66 | 67 | fun updateCurrentLanguageState() { 68 | UserServiceProvider.run { 69 | val currentLocale = getApplicationLocales(appInfo.packageName) 70 | if (!currentLocale.isEmpty) 71 | _uiState.update { it.copy(currentLanguage = currentLocale.get(0).capDisplayName()) } 72 | } 73 | } 74 | 75 | fun onClickSingleLanguage(index: Int) { 76 | _uiState.update { it.copy(selectedLanguage = index) } 77 | } 78 | 79 | fun onBackWhenSelectedLang() { 80 | _uiState.update { it.copy(selectedLanguage = -1) } 81 | } 82 | 83 | fun onClickLocale(singleLocale: SingleLocale) { 84 | UserServiceProvider.run { 85 | setApplicationLocales( 86 | appInfo.packageName, 87 | LocaleList(singleLocale.toLocale()) 88 | ) 89 | updateCurrentLanguageState() 90 | } 91 | } 92 | 93 | fun onClickSettings() { 94 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 95 | val uri = Uri.fromParts("package", appInfo.packageName, null) 96 | intent.setData(uri) 97 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 98 | app.startActivity(intent) 99 | } 100 | 101 | fun onClickOpen() { 102 | val launchIntent = 103 | app.packageManager.getLaunchIntentForPackage(appInfo.packageName) 104 | launchIntent?.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ?: return 105 | app.startActivity(launchIntent) 106 | } 107 | 108 | fun onClickResetLang() { 109 | UserServiceProvider.run { 110 | setApplicationLocales(appInfo.packageName, LocaleList()) 111 | updateCurrentLanguageState() 112 | _uiState.update { it.copy(currentLanguage = "") } 113 | } 114 | } 115 | 116 | fun onClickForceClose() { 117 | UserServiceProvider.run { 118 | forceStopPackage(appInfo.packageName) 119 | } 120 | } 121 | 122 | fun getSp(): SharedPreferences = 123 | app.getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE) 124 | 125 | fun onPinLang(singleLocale: SingleLocale) { 126 | val sp = getSp() 127 | val set = sp.getStringSet(PrefConstants.PINNED_LOCALES, emptySet()) ?: emptySet() 128 | val mset = set.toMutableSet() 129 | mset.add("${singleLocale.name},${singleLocale.languageTag}") 130 | sp.edit().putStringSet(PrefConstants.PINNED_LOCALES, mset).apply() 131 | updatePinnedLangsFromSP() 132 | } 133 | 134 | fun onRemovePin(singleLocale: SingleLocale) { 135 | val sp = getSp() 136 | val set = sp.getStringSet(PrefConstants.PINNED_LOCALES, emptySet()) ?: emptySet() 137 | val newSet = mutableSetOf() 138 | set.forEach { 139 | if (!it.contains(singleLocale.languageTag)) 140 | newSet.add(it) 141 | } 142 | sp.edit().putStringSet(PrefConstants.PINNED_LOCALES, newSet).apply() 143 | updatePinnedLangsFromSP() 144 | } 145 | 146 | fun updatePinnedLangsFromSP() { 147 | val sp = getSp() 148 | val set = sp.getStringSet(PrefConstants.PINNED_LOCALES, emptySet()) ?: return 149 | val pinnedLocaleList = set.parseSetLangs() 150 | _uiState.update { it.copy(listOfPinnedLanguages = pinnedLocaleList) } 151 | } 152 | 153 | } 154 | 155 | fun Locale.capDisplayName(): String { 156 | return this.getDisplayName(this).replaceFirstChar { it.uppercaseChar() } 157 | } 158 | 159 | fun Set.parseSetLangs(): MutableList { 160 | return this.mapNotNull { 161 | try { 162 | val stringLocale = it.split(",") 163 | val name = stringLocale[0] 164 | val tag = stringLocale[1] 165 | SingleLocale(name, tag) 166 | } catch (e: Exception) { 167 | Log.e(BuildConfig.APPLICATION_ID, e.stackTraceToString()) 168 | null 169 | } 170 | }.toMutableList() 171 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/screen/main/MainScreen.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.screen.main 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.statusBarsPadding 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.rememberLazyListState 11 | import androidx.compose.material3.CircularProgressIndicator 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.SnackbarHostState 14 | import androidx.compose.material3.SnackbarResult 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.LaunchedEffect 17 | import androidx.compose.runtime.collectAsState 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.semantics.isTraversalGroup 24 | import androidx.compose.ui.semantics.semantics 25 | import androidx.compose.ui.semantics.traversalIndex 26 | import androidx.compose.ui.unit.dp 27 | import androidx.hilt.navigation.compose.hiltViewModel 28 | import kotlinx.coroutines.flow.collectLatest 29 | import vegabobo.languageselector.R 30 | import vegabobo.languageselector.ui.components.AppListItem 31 | import vegabobo.languageselector.ui.components.AppSearchBar 32 | import vegabobo.languageselector.ui.screen.BaseScreen 33 | 34 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) 35 | @Composable 36 | fun MainScreen( 37 | mainScreenVm: MainScreenVm = hiltViewModel(), 38 | navigateToAppScreen: (String) -> Unit, 39 | navigateToAbout: () -> Unit, 40 | ) { 41 | val uiState by mainScreenVm.uiState.collectAsState() 42 | val sb = remember { SnackbarHostState() } 43 | val lazyListState = rememberLazyListState() 44 | 45 | LaunchedEffect(Unit) { 46 | mainScreenVm.reloadLastSelectedItem() 47 | mainScreenVm.uiState.collectLatest { 48 | when (it.snackBarDisplay) { 49 | SnackBarDisplay.MOVED_TO_TOP -> { 50 | val r = sb.showSnackbar( 51 | message = "Modified app has been moved up", 52 | actionLabel = "Navigate" 53 | ) 54 | if (r == SnackbarResult.ActionPerformed) { 55 | val i = 56 | mainScreenVm.getIndexFromAppInfoItem() + 1 /* first item is a spacer */ 57 | lazyListState.animateScrollToItem(i) 58 | } 59 | } 60 | 61 | SnackBarDisplay.MOVED_TO_BOTTOM -> { 62 | val r = sb.showSnackbar( 63 | message = "Unmodified has been moved down", 64 | actionLabel = "Navigate" 65 | ) 66 | if (r == SnackbarResult.ActionPerformed) { 67 | val i = 68 | mainScreenVm.getIndexFromAppInfoItem() + 1 /* first item is a spacer */ 69 | lazyListState.animateScrollToItem(i) 70 | } 71 | } 72 | 73 | else -> {} 74 | } 75 | mainScreenVm.resetSnackBarDisplay() 76 | } 77 | } 78 | BaseScreen(snackBarHost = sb) { 79 | if (uiState.isLoading) 80 | Box(modifier = Modifier.fillMaxSize()) { 81 | CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) 82 | } 83 | else { 84 | Box( 85 | Modifier 86 | .fillMaxSize() 87 | .semantics { isTraversalGroup = true }) { 88 | AppSearchBar( 89 | modifier = Modifier 90 | .align(Alignment.TopCenter) 91 | .semantics { traversalIndex = 0f }, 92 | placeholder = stringResource(R.string.search), 93 | onUpdatedValue = { mainScreenVm.onSearchTextFieldChange(it) }, 94 | query = uiState.searchTextFieldValue, 95 | onClickApp = { mainScreenVm.onClickApp(it); navigateToAppScreen(it.pkg) }, 96 | history = uiState.history, 97 | apps = uiState.listOfApps, 98 | isExpanded = uiState.isExpanded, 99 | onExpandedChange = { mainScreenVm.onSearchExpandedChange() }, 100 | selectedLabels = uiState.selectLabels, 101 | onSelectedLabelsChange = { mainScreenVm.onSelectedLabelChange(it) }, 102 | onClickClear = { mainScreenVm.onClickClear() }, 103 | actions = { 104 | if (!uiState.isExpanded) 105 | SearchBarActions( 106 | isDropdownVisible = uiState.isDropdownVisible, 107 | isShowingSystemApps = uiState.isShowSystemAppsHome, 108 | onClickToggleDropdown = { mainScreenVm.toggleDropdown() }, 109 | onToggleDropdown = { mainScreenVm.toggleDropdown() }, 110 | onClickToggleSystemApps = { mainScreenVm.toggleSystemAppsVisibility() }, 111 | onClickAbout = { navigateToAbout() } 112 | ) 113 | }) 114 | 115 | if (uiState.operationMode == OperationMode.NONE) { 116 | ShizukuRequiredWarning { mainScreenVm.onClickProceedShizuku() } 117 | } 118 | 119 | LazyColumn( 120 | state = lazyListState, 121 | modifier = Modifier.semantics { traversalIndex = 1f } 122 | ) { 123 | item { 124 | Spacer( 125 | Modifier 126 | .statusBarsPadding() 127 | .padding(top = 72.dp) /* 64 + 10 */ 128 | ) 129 | } 130 | items(uiState.listOfApps.size) { 131 | val thisApp = uiState.listOfApps[it] 132 | if (!uiState.isShowSystemAppsHome && thisApp.isSystemApp() && !thisApp.isModified()) 133 | return@items 134 | AppListItem( 135 | modifier = Modifier.padding( 136 | start = 26.dp, 137 | end = 26.dp, 138 | top = 4.dp, 139 | bottom = 4.dp 140 | ), 141 | app = thisApp, 142 | onClickApp = { 143 | mainScreenVm.onClickApp(thisApp) 144 | navigateToAppScreen(it) 145 | } 146 | ) 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/screen/main/MainScreenState.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.screen.main 2 | 3 | import android.content.pm.ApplicationInfo 4 | import android.content.pm.PackageManager 5 | import android.graphics.drawable.Drawable 6 | import androidx.compose.runtime.mutableStateListOf 7 | import vegabobo.languageselector.dao.AppInfoEntity 8 | 9 | enum class OperationMode { 10 | NONE, SHIZUKU, ROOT 11 | } 12 | 13 | enum class SnackBarDisplay { 14 | NONE, MOVED_TO_TOP, MOVED_TO_BOTTOM 15 | } 16 | 17 | data class MainScreenState( 18 | val listOfApps: MutableList = mutableStateListOf(), 19 | val history: MutableList = mutableStateListOf(), 20 | val operationMode: OperationMode = OperationMode.NONE, 21 | val isDropdownVisible: Boolean = false, 22 | val isAboutDialogVisible: Boolean = false, 23 | val isLoading: Boolean = true, 24 | val isShowSystemAppsHome: Boolean = false, 25 | val snackBarDisplay: SnackBarDisplay = SnackBarDisplay.NONE, 26 | 27 | /* Search bar */ 28 | val isExpanded: Boolean = false, 29 | val searchTextFieldValue: String = "", 30 | val selectLabels: MutableList = mutableStateListOf() 31 | ) 32 | 33 | enum class AppLabels { 34 | SYSTEM_APP, MODIFIED 35 | } 36 | 37 | data class AppInfo( 38 | val icon: Drawable, 39 | val name: String, 40 | val pkg: String, 41 | val labels: List = emptyList() 42 | ) { 43 | fun isSystemApp() = labels.contains(AppLabels.SYSTEM_APP) 44 | fun isModified() = labels.contains(AppLabels.MODIFIED) 45 | } 46 | 47 | fun AppInfo.toAppInfoEntity(): AppInfoEntity { 48 | return AppInfoEntity(this.pkg, this.name, System.currentTimeMillis()) 49 | } 50 | 51 | fun PackageManager.getLabel(applicationInfo: ApplicationInfo): String { 52 | return applicationInfo.loadLabel(this).toString() 53 | } 54 | 55 | fun PackageManager.getAppIcon(applicationInfo: ApplicationInfo): Drawable { 56 | return this.getApplicationIcon(applicationInfo) 57 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/screen/main/MainScreenVm.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.screen.main 2 | 3 | import android.app.Application 4 | import android.content.pm.ApplicationInfo 5 | import android.content.pm.PackageManager 6 | import android.os.Handler 7 | import android.os.Looper 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.viewModelScope 11 | import com.topjohnwu.superuser.Shell 12 | import dagger.hilt.android.lifecycle.HiltViewModel 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.flow.MutableStateFlow 15 | import kotlinx.coroutines.flow.StateFlow 16 | import kotlinx.coroutines.flow.asStateFlow 17 | import kotlinx.coroutines.flow.update 18 | import kotlinx.coroutines.launch 19 | import rikka.shizuku.Shizuku 20 | import vegabobo.languageselector.BuildConfig 21 | import vegabobo.languageselector.RootReceivedListener 22 | import vegabobo.languageselector.dao.AppInfoDb 23 | import vegabobo.languageselector.service.UserServiceProvider 24 | import javax.inject.Inject 25 | 26 | 27 | @HiltViewModel 28 | class MainScreenVm @Inject constructor( 29 | val app: Application, 30 | appInfoDb: AppInfoDb 31 | ) : ViewModel() { 32 | private val _uiState = MutableStateFlow(MainScreenState()) 33 | val uiState: StateFlow = _uiState.asStateFlow() 34 | var lastSelectedApp: AppInfo? = null 35 | val dao = appInfoDb.appInfoDao() 36 | 37 | fun getIndexFromAppInfoItem(): Int { 38 | return _uiState.value.listOfApps.indexOfFirst { it.pkg == lastSelectedApp?.pkg } 39 | } 40 | 41 | fun loadOperationMode() { 42 | if (Shell.getShell().isAlive) 43 | Shell.getShell().close() 44 | Shell.getShell() 45 | if (Shell.isAppGrantedRoot() == true) { 46 | _uiState.update { it.copy(operationMode = OperationMode.ROOT) } 47 | RootReceivedListener.onRootReceived() 48 | return 49 | } 50 | 51 | val isAvail = Shizuku.pingBinder() && 52 | Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED 53 | if (isAvail) { 54 | _uiState.update { it.copy(operationMode = OperationMode.SHIZUKU) } 55 | return 56 | } 57 | 58 | _uiState.update { it.copy(operationMode = OperationMode.NONE) } 59 | } 60 | 61 | init { 62 | fillListOfApps() 63 | } 64 | 65 | fun parseAppInfo(a: ApplicationInfo): AppInfo { 66 | val isSystemApp = (a.flags and ApplicationInfo.FLAG_SYSTEM) != 0 67 | val service = UserServiceProvider.getService() 68 | val languagePreferences = service.getApplicationLocales(a.packageName) 69 | val labels = arrayListOf() 70 | if (isSystemApp) 71 | labels.add(AppLabels.SYSTEM_APP) 72 | if (!languagePreferences.isEmpty) 73 | labels.add(AppLabels.MODIFIED) 74 | return AppInfo( 75 | icon = app.packageManager.getAppIcon(a), 76 | name = app.packageManager.getLabel(a), 77 | pkg = a.packageName, 78 | labels = labels 79 | ) 80 | } 81 | 82 | fun fillListOfApps() { 83 | viewModelScope.launch(Dispatchers.IO) { 84 | if (_uiState.value.operationMode == OperationMode.NONE) 85 | loadOperationMode() 86 | val packageList = getInstalledPackages().map { parseAppInfo(it) } 87 | var sortedList = 88 | packageList.sortedBy { it.name.lowercase() }.sortedBy { !it.isModified() } 89 | _uiState.value.listOfApps.clear() 90 | _uiState.value.listOfApps.addAll(sortedList) 91 | _uiState.update { it.copy(isLoading = false) } 92 | } 93 | } 94 | 95 | fun getInstalledPackages(): List { 96 | return app.packageManager.getInstalledApplications( 97 | PackageManager.ApplicationInfoFlags.of(0) 98 | ).mapNotNull { 99 | if (!it.enabled || BuildConfig.APPLICATION_ID == it.packageName) 100 | null 101 | else 102 | it 103 | } 104 | } 105 | 106 | fun toggleDropdown() { 107 | val newDropdownVisibility = !uiState.value.isDropdownVisible 108 | _uiState.update { it.copy(isDropdownVisible = newDropdownVisibility) } 109 | } 110 | 111 | fun toggleSystemAppsVisibility() { 112 | val newShowSystemApps = !uiState.value.isShowSystemAppsHome 113 | _uiState.update { 114 | it.copy( 115 | isLoading = true, 116 | isShowSystemAppsHome = newShowSystemApps 117 | ) 118 | } 119 | fillListOfApps() 120 | toggleDropdown() 121 | } 122 | 123 | fun onClickProceedShizuku() { 124 | loadOperationMode() 125 | } 126 | 127 | val searchQuery = mutableStateOf("") 128 | private val handler = Handler(Looper.getMainLooper()) 129 | private var workRunnable: Runnable? = null 130 | 131 | fun onSearchTextFieldChange(newText: String) { 132 | _uiState.update { it.copy(searchTextFieldValue = newText) } 133 | 134 | if (workRunnable != null) 135 | handler.removeCallbacks(workRunnable!!) 136 | 137 | workRunnable = Runnable { searchQuery.value = newText } 138 | handler.postDelayed(workRunnable!!, 1000) 139 | } 140 | 141 | fun onSearchExpandedChange() { 142 | val isExpanded = !uiState.value.isExpanded 143 | _uiState.update { it.copy(isExpanded = isExpanded) } 144 | if (isExpanded) 145 | updateHistory() 146 | else 147 | _uiState.update { it.copy(searchTextFieldValue = "") } 148 | } 149 | 150 | fun onSelectedLabelChange(label: AppLabels) { 151 | val lb = _uiState.value.selectLabels 152 | if (lb.contains(label)) 153 | lb.remove(label) 154 | else 155 | lb.add(label) 156 | } 157 | 158 | fun updateHistory() { 159 | viewModelScope.launch(Dispatchers.IO) { 160 | val appInfoList = dao.getHistory().map { it.pkg } 161 | val history = appInfoList.mapNotNull { pkg -> 162 | val listOfApps = _uiState.value.listOfApps 163 | val idx = listOfApps.indexOfFirst { it.pkg == pkg } 164 | if (idx == -1) 165 | null 166 | else 167 | listOfApps[idx] 168 | } 169 | _uiState.value.history.clear() 170 | _uiState.value.history.addAll(history) 171 | } 172 | } 173 | 174 | fun addAppToHistory(ai: AppInfo) { 175 | viewModelScope.launch(Dispatchers.IO) { 176 | if (dao.findByPkg(ai.pkg) == null) { 177 | dao.insert(ai.toAppInfoEntity()) 178 | } 179 | dao.setLastSelected(ai.pkg, System.currentTimeMillis()) 180 | updateHistory() 181 | } 182 | } 183 | 184 | fun onClickClear() { 185 | viewModelScope.launch(Dispatchers.IO) { 186 | dao.cleanLastSelectedAll() 187 | updateHistory() 188 | } 189 | } 190 | 191 | fun reloadLastSelectedItem() { 192 | if (lastSelectedApp == null) return 193 | val pkg = app.packageManager.getApplicationInfo(lastSelectedApp!!.pkg, 0) 194 | val updatedAi = parseAppInfo(pkg) 195 | val apps = _uiState.value.listOfApps 196 | val idx = apps.indexOfFirst { it.pkg == updatedAi.pkg } 197 | if (idx != -1 && updatedAi.labels != apps[idx].labels) { 198 | apps[idx] = updatedAi 199 | val newList = _uiState.value.listOfApps.sortedBy { it.name.lowercase() } 200 | .sortedBy { !it.isModified() }.toMutableList() 201 | _uiState.update { 202 | it.copy( 203 | listOfApps = newList, 204 | snackBarDisplay = if (updatedAi.isModified()) SnackBarDisplay.MOVED_TO_TOP else SnackBarDisplay.MOVED_TO_BOTTOM 205 | ) 206 | } 207 | return 208 | } 209 | } 210 | 211 | fun resetSnackBarDisplay() = _uiState.update { it.copy(snackBarDisplay = SnackBarDisplay.NONE) } 212 | 213 | fun onClickApp(ai: AppInfo) { 214 | lastSelectedApp = ai 215 | addAppToHistory(ai) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/screen/main/SearchBarActions.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.screen.main 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.wrapContentSize 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.outlined.MoreVert 8 | import androidx.compose.material3.DropdownMenu 9 | import androidx.compose.material3.DropdownMenuItem 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import vegabobo.languageselector.R 18 | 19 | @Composable 20 | fun SearchBarActions( 21 | isDropdownVisible: Boolean = false, 22 | isShowingSystemApps: Boolean = false, 23 | onClickToggleDropdown: () -> Unit, 24 | onToggleDropdown: () -> Unit, 25 | onClickToggleSystemApps: () -> Unit, 26 | onClickAbout: () -> Unit 27 | ) { 28 | Box( 29 | modifier = Modifier.wrapContentSize(Alignment.Center) 30 | ) { 31 | ToolbarNormal( 32 | onToggleDropdown = { onToggleDropdown() } 33 | ) 34 | 35 | DropdownMenu( 36 | expanded = isDropdownVisible, 37 | onDismissRequest = { onClickToggleDropdown() } 38 | ) { 39 | DropdownMenuItem( 40 | text = { 41 | Text( 42 | text = if (isShowingSystemApps) 43 | stringResource(R.string.show_only_user_apps) 44 | else 45 | stringResource(R.string.show_system_apps) 46 | ) 47 | }, 48 | onClick = { onClickToggleSystemApps() } 49 | ) 50 | DropdownMenuItem( 51 | text = { Text(stringResource(R.string.about)) }, 52 | onClick = { onClickAbout(); onClickToggleDropdown() } 53 | ) 54 | } 55 | } 56 | } 57 | 58 | @Composable 59 | fun ToolbarNormal( 60 | onToggleDropdown: () -> Unit, 61 | ) { 62 | Row(verticalAlignment = Alignment.CenterVertically) { 63 | IconButton(onClick = { onToggleDropdown() }) { 64 | Icon( 65 | imageVector = Icons.Outlined.MoreVert, 66 | contentDescription = "More icon" 67 | ) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/screen/main/ShizukuRequiredWarning.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.screen.main 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.WarningAmber 5 | import androidx.compose.material3.AlertDialog 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TextButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.res.stringResource 11 | import vegabobo.languageselector.R 12 | 13 | @Composable 14 | fun ShizukuRequiredWarning( 15 | onClickContinue: () -> Unit 16 | ) { 17 | AlertDialog( 18 | onDismissRequest = {}, 19 | confirmButton = { 20 | TextButton(onClick = { onClickContinue() }) { Text(stringResource(id = R.string.proceed)) } 21 | }, 22 | icon = { 23 | Icon( 24 | imageVector = Icons.Outlined.WarningAmber, 25 | contentDescription = "Warning icon" 26 | ) 27 | }, 28 | title = { Text(stringResource(id = R.string.permissions_required)) }, 29 | text = { Text(stringResource(id = R.string.shizuku_required)) } 30 | ) 31 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/screen/main/SystemDialogWarn.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.screen.main 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.WarningAmber 5 | import androidx.compose.material3.AlertDialog 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TextButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.res.stringResource 11 | import vegabobo.languageselector.R 12 | 13 | @Composable 14 | fun SystemDialogWarn( 15 | onClickContinue: () -> Unit, 16 | onClickCancel: () -> Unit, 17 | ) { 18 | AlertDialog( 19 | icon = { 20 | Icon( 21 | imageVector = Icons.Outlined.WarningAmber, 22 | contentDescription = "Warning icon" 23 | ) 24 | }, 25 | text = { Text(stringResource(R.string.warning_system_apps)) }, 26 | title = { Text(stringResource(R.string.warning)) }, 27 | onDismissRequest = { onClickCancel() }, 28 | confirmButton = { 29 | TextButton(onClick = { onClickContinue() }) { 30 | Text(stringResource(R.string.proceed)) 31 | } 32 | }, 33 | dismissButton = { 34 | TextButton(onClick = { onClickCancel() }) { 35 | Text(stringResource(R.string.cancel)) 36 | } 37 | } 38 | ) 39 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple80 = Color(0xFFD0BCFF) 6 | val PurpleGrey80 = Color(0xFFCCC2DC) 7 | val Pink80 = Color(0xFFEFB8C8) 8 | 9 | val Purple40 = Color(0xFF6650a4) 10 | val PurpleGrey40 = Color(0xFF625b71) 11 | val Pink40 = Color(0xFF7D5260) -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.darkColorScheme 8 | import androidx.compose.material3.dynamicDarkColorScheme 9 | import androidx.compose.material3.dynamicLightColorScheme 10 | import androidx.compose.material3.lightColorScheme 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.SideEffect 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.platform.LocalView 15 | import androidx.core.view.WindowCompat 16 | 17 | private val DarkColorScheme = darkColorScheme( 18 | primary = Purple80, 19 | secondary = PurpleGrey80, 20 | tertiary = Pink80 21 | ) 22 | 23 | private val LightColorScheme = lightColorScheme( 24 | primary = Purple40, 25 | secondary = PurpleGrey40, 26 | tertiary = Pink40 27 | 28 | /* Other default colors to override 29 | background = Color(0xFFFFFBFE), 30 | surface = Color(0xFFFFFBFE), 31 | onPrimary = Color.White, 32 | onSecondary = Color.White, 33 | onTertiary = Color.White, 34 | onBackground = Color(0xFF1C1B1F), 35 | onSurface = Color(0xFF1C1B1F), 36 | */ 37 | ) 38 | 39 | @Composable 40 | fun LanguageSelector( 41 | darkTheme: Boolean = isSystemInDarkTheme(), 42 | // Dynamic color is available on Android 12+ 43 | dynamicColor: Boolean = true, 44 | content: @Composable () -> Unit 45 | ) { 46 | val colorScheme = when { 47 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 48 | val context = LocalContext.current 49 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 50 | } 51 | 52 | darkTheme -> DarkColorScheme 53 | else -> LightColorScheme 54 | } 55 | val view = LocalView.current 56 | if (!view.isInEditMode) { 57 | SideEffect { 58 | val window = (view.context as Activity).window 59 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme 60 | } 61 | } 62 | 63 | MaterialTheme( 64 | colorScheme = colorScheme, 65 | typography = Typography, 66 | content = content 67 | ) 68 | } -------------------------------------------------------------------------------- /app/src/main/java/vegabobo/languageselector/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package vegabobo.languageselector.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val Typography = Typography( 11 | bodyLarge = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp, 15 | lineHeight = 24.sp, 16 | letterSpacing = 0.5.sp 17 | ) 18 | /* Other default text styles to override 19 | titleLarge = TextStyle( 20 | fontFamily = FontFamily.Default, 21 | fontWeight = FontWeight.Normal, 22 | fontSize = 22.sp, 23 | lineHeight = 28.sp, 24 | letterSpacing = 0.sp 25 | ), 26 | labelSmall = TextStyle( 27 | fontFamily = FontFamily.Default, 28 | fontWeight = FontWeight.Medium, 29 | fontSize = 11.sp, 30 | lineHeight = 16.sp, 31 | letterSpacing = 0.5.sp 32 | ) 33 | */ 34 | ) -------------------------------------------------------------------------------- /app/src/main/res/drawable-night/qs_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/drawable-night/qs_tile.png -------------------------------------------------------------------------------- /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 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/icon_placeholder.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/drawable/icon_placeholder.webp -------------------------------------------------------------------------------- /app/src/main/res/drawable/qs_tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/drawable/qs_tile.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-hdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-mdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-xhdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Language Selector 4 | アプリの言語 5 | システムのデフォルト 6 | ユーザー言語: 7 | ピン留め: 8 | 地域: 9 | すべての言語: 10 | ユーザーアプリのみを表示 11 | システムアプリを表示 12 | 設定 13 | 開く 14 | 閉じる 15 | 必要な許可 16 | このアプリにはShizukuが必要です。Shizukuをインストールしてから起動し、Shizukuにこのアプリへのアクセス許可を与えた後、次に進んでください。 17 | 続行 18 | %1$s をピン留めしました 19 | %1$s のピン留めを解除しました 20 | About 21 | 依存関係およびライブラリ: 22 | Version %1$s (%2$s) 23 | アプリケーション 24 | GitHub リポジトリ 25 | ソースコードを表示 26 | 読み込み中… 27 | 利用できません 28 | 警告 29 | システムアプリから言語を選択することは推奨されていません。続行しますか? 30 | キャンセル 31 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | @android:color/system_neutral1_900 4 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt-rBR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Language Selector 4 | Idioma do app 5 | Padrão do sistema 6 | Idiomas do usuário: 7 | Fixado: 8 | Região: 9 | Todos os idiomas: 10 | Mostrar apenas apps do usuário 11 | Mostrar apps do sistema 12 | Config. 13 | Abrir 14 | Fechar 15 | Permissões necessárias 16 | É necessário o Shizuku, por favor instale e inicie o Shizuku, depois que as permissões do Shizuku forem concecidas ao app, será possível continuar. 17 | Continuar 18 | %1$s fixado 19 | %1$s desafixado 20 | Sobre 21 | Dependências e bibliotecas 22 | versão %1$s (%2$s) 23 | Aplicativo 24 | GitHub repo. 25 | Ver código fonte 26 | Carregando… 27 | Indisponível 28 | Aviso 29 | Selecionar idiomas de aplicativos do sistema não é recomendado, você tem certeza? 30 | Cancelar 31 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | App语言 4 | 系统默认 5 | 用户语言 6 | 置顶: 7 | 地区: 8 | 全部语言: 9 | 只显示用户应用 10 | 显示系统应用 11 | 设置 12 | 打开 13 | 关闭 14 | 需要权限 15 | 该应用需要 Shizuku,请安装并启动 Shizuku,获得 Shizuku 权限后,你可以继续 16 | 继续 17 | %1$s 已置顶 18 | %1$s 取消置顶 19 | 关于 20 | 依赖库信息: 21 | 版本 %1$s (%2$s) 22 | 应用 23 | Github 仓库 24 | 查看源码 25 | 加载中... 26 | 不可用 27 | 警告 28 | 不建议对系统应用选择语言,你确定? 29 | 取消 30 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | @android:color/system_neutral1_10 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Language Selector 3 | App language 4 | System default 5 | User languages: 6 | Pinned: 7 | Region: 8 | All languages: 9 | Show only user apps 10 | Show system apps 11 | Settings 12 | Open 13 | Close 14 | Permissions required 15 | This app requires Shizuku, please install and start Shizuku, after granting Shizuku permissions to this app, you can proceed. 16 | Proceed 17 | %1$s pinned 18 | %1$s unpinned 19 | About 20 | Dependencies and libraries: 21 | version %1$s (%2$s) 22 | Application 23 | GitHub repo 24 | View source code 25 | Loading… 26 | Unavailable 27 | Warning 28 | Selecting language from system apps is not recommended, are you sure? 29 | Cancel 30 | 31 | Search apps 32 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/locales_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.com.android.application) apply false 4 | alias(libs.plugins.org.jetbrains.kotlin.android) apply false 5 | alias(libs.plugins.com.android.library) apply false 6 | alias(libs.plugins.com.google.dagger.hilt) apply false 7 | alias(libs.plugins.com.mikepenz.aboutlibraries) apply false 8 | alias(libs.plugins.compose.compiler) apply false 9 | alias(libs.plugins.com.google.devtools.ksp) apply false 10 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.enableR8.fullMode=false -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | aboutlibraries = "10.6.3" 3 | libsu = "6.0.0" 4 | hiddenapibypass = "4.3" 5 | hilt = "2.51.1" 6 | hilt-navigation-compose = "1.2.0" 7 | material-icons-extended = "1.7.6" 8 | navigation-compose = "2.8.5" 9 | room-compiler = "2.6.1" 10 | shizuku = "13.1.1" 11 | com-android-application = "8.7.3" 12 | org-jetbrains-kotlin-android = "2.0.21" 13 | core-ktx = "1.15.0" 14 | lifecycle-runtime-ktx = "2.8.7" 15 | activity-compose = "1.9.3" 16 | compose-bom = "2024.12.01" 17 | com-android-library = "8.7.3" 18 | material = "1.12.0" 19 | ksp = "2.0.21-1.0.27" 20 | 21 | [libraries] 22 | aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibraries" } 23 | androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt-navigation-compose" } 24 | androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle-runtime-ktx" } 25 | androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "material-icons-extended" } 26 | androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } 27 | androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room-compiler" } 28 | androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room-compiler" } 29 | libsu-core = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" } 30 | libsu-service = { module = "com.github.topjohnwu.libsu:service", version.ref = "libsu" } 31 | hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapibypass" } 32 | hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } 33 | hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } 34 | shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" } 35 | shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" } 36 | core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } 37 | lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" } 38 | activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } 39 | compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } 40 | ui = { group = "androidx.compose.ui", name = "ui" } 41 | ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 42 | ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } 43 | ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 44 | ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } 45 | material3 = { group = "androidx.compose.material3", name = "material3" } 46 | material = { group = "com.google.android.material", name = "material", version.ref = "material" } 47 | 48 | [plugins] 49 | com-android-application = { id = "com.android.application", version.ref = "com-android-application" } 50 | org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "org-jetbrains-kotlin-android" } 51 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "org-jetbrains-kotlin-android" } 52 | com-android-library = { id = "com.android.library", version.ref = "com-android-library" } 53 | com-google-dagger-hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } 54 | com-mikepenz-aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" } 55 | com-google-devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 56 | 57 | [bundles] 58 | 59 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /hidden_api/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /hidden_api/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.com.android.library) 3 | alias(libs.plugins.org.jetbrains.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace = "com.example.hidden_api" 8 | compileSdk = 35 9 | 10 | defaultConfig { 11 | minSdk = 33 12 | 13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles("consumer-rules.pro") 15 | } 16 | buildTypes { 17 | release { 18 | isMinifyEnabled = false 19 | proguardFiles( 20 | getDefaultProguardFile("proguard-android-optimize.txt"), 21 | "proguard-rules.pro" 22 | ) 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility = JavaVersion.VERSION_21 27 | targetCompatibility = JavaVersion.VERSION_21 28 | } 29 | } 30 | 31 | dependencies {} -------------------------------------------------------------------------------- /hidden_api/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/hidden_api/consumer-rules.pro -------------------------------------------------------------------------------- /hidden_api/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 -------------------------------------------------------------------------------- /hidden_api/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /hidden_api/src/main/java/android/app/ActivityManager.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | public class ActivityManager { 7 | 8 | public static class RunningTaskInfo extends TaskInfo implements Parcelable { 9 | protected RunningTaskInfo(Parcel in) { 10 | } 11 | 12 | public static final Creator CREATOR = new Creator() { 13 | @Override 14 | public RunningTaskInfo createFromParcel(Parcel in) { 15 | return new RunningTaskInfo(in); 16 | } 17 | 18 | @Override 19 | public RunningTaskInfo[] newArray(int size) { 20 | return new RunningTaskInfo[size]; 21 | } 22 | }; 23 | 24 | @Override 25 | public int describeContents() { 26 | return 0; 27 | } 28 | 29 | @Override 30 | public void writeToParcel(Parcel dest, int flags) { 31 | } 32 | } 33 | 34 | public static int getCurrentUser() { 35 | return -1; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /hidden_api/src/main/java/android/app/ActivityTaskManager.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import java.util.List; 4 | 5 | public class ActivityTaskManager { 6 | public List getTasks(int maxNum) { return null; } 7 | } 8 | -------------------------------------------------------------------------------- /hidden_api/src/main/java/android/app/IActivityManager.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | 7 | public interface IActivityManager extends IInterface { 8 | 9 | void forceStopPackage(String packageName, int userId); 10 | 11 | abstract class Stub extends Binder implements IActivityManager { 12 | 13 | public static IActivityManager asInterface(IBinder obj) { 14 | throw new UnsupportedOperationException(); 15 | } 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /hidden_api/src/main/java/android/app/IActivityTaskManager.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | 7 | import java.util.List; 8 | 9 | public interface IActivityTaskManager extends IInterface { 10 | 11 | List getTasks(int maxNum, boolean filterOnlyVisibleRecents, boolean keepIntentExtra, int displayId); 12 | List getTasks(int maxNum, boolean filterOnlyVisibleRecents, boolean keepIntentExtra); 13 | 14 | abstract class Stub extends Binder implements IActivityTaskManager { 15 | 16 | public static IActivityTaskManager asInterface(IBinder obj) { 17 | throw new UnsupportedOperationException(); 18 | } 19 | 20 | } 21 | } -------------------------------------------------------------------------------- /hidden_api/src/main/java/android/app/IApplicationThread.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | public class IApplicationThread { 4 | } -------------------------------------------------------------------------------- /hidden_api/src/main/java/android/app/ILocaleManager.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import android.os.Binder; 4 | import android.os.IBinder; 5 | import android.os.IInterface; 6 | import android.os.LocaleList; 7 | 8 | public interface ILocaleManager extends IInterface { 9 | 10 | // 33 11 | void setApplicationLocales(String packageName, int userId, LocaleList locales); 12 | 13 | // U 14 | void setApplicationLocales(String packageName, int userId, LocaleList locales, boolean fromDelegate); 15 | LocaleList getApplicationLocales(String packageName, int userId); 16 | LocaleList getSystemLocales(); 17 | 18 | abstract class Stub extends Binder implements ILocaleManager { 19 | 20 | public static ILocaleManager asInterface(IBinder obj) { 21 | throw new UnsupportedOperationException(); 22 | } 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /hidden_api/src/main/java/android/app/ProfilerInfo.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | public class ProfilerInfo { 4 | } -------------------------------------------------------------------------------- /other/preview_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/other/preview_1.jpg -------------------------------------------------------------------------------- /other/preview_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VegaBobo/Language-Selector/ca8e4fae32cd0481693d79b84599ad0fd5a04d03/other/preview_2.jpg -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | maven("https://jitpack.io") 7 | } 8 | } 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | maven("https://jitpack.io") 15 | } 16 | } 17 | 18 | rootProject.name = "language_selector" 19 | include(":app") 20 | include(":hidden_api") 21 | --------------------------------------------------------------------------------