├── .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 |

13 |

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 |
--------------------------------------------------------------------------------