APK開發需要實現 選擇系統語言 功能,使用反射和導入framework架包2種方法都可實現。
由于修改系統語言需要系統權限,所以無論使用哪種方法,都需要給APK添加系統權限,添加系統權限又必須添加系統簽名,系統會要求該APK具有與系統相同的簽名,否則會安裝失敗。
系統權限和系統簽名
1. 添加系統權限
添加系統權限很簡單,只需要在APK的AndroidManifest.xml中聲明android:sharedUserId="android.uid.system"即可。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
// 添加系統權限
android:sharedUserId="android.uid.system">
...
</manifest>
2. 添加系統簽名
添加系統簽名,首先需要將系統簽名復制到工程目錄下,在config.gradle中配置參數,并在工程的build.gradle中應用下:
<config.gradle>
ext {
sign = [
file : '../system_key.jks',
storePassword: 'xxxxxxxx',
keyAlias : 'system_key',
keyPassword : 'xxxxxxxx'
]
}
<build.gradle>
apply from: "config.gradle"
然后在APP的build.gradle中打包時增加系統簽名:
android{
...
signingConfigs {
hikvision {
storeFile file(sign.file)
storePassword sign.storePassword
keyAlias sign.keyAlias
keyPassword sign.keyPassword
}
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.hikvision
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.hikvision
}
}
...
}
3. 可能存在的問題
APK添加了系統權限,webView會報錯
WebView is not allowed in privileged processes
Android 6.0 以上不允許在擁有系統權限的應用中使用 WebView,需要在onCreate()方法中的setContentView()之前調用以下的hookWebView()方法:
public static void hookWebView() {
int sdkInt = android.os.Build.VERSION.SDK_INT;
try {
Class<?> factoryClass = Class.forName("android.webkit.WebViewFactory");
Field field = factoryClass.getDeclaredField("sProviderInstance");
field.setAccessible(true);
Object sProviderInstance = field.get(null);
if (sProviderInstance!= null) {
Log.i(TAG, "sProviderInstance isn't null");
return;
}
Method getProviderClassMethod = null;
if (sdkInt > 22) {
getProviderClassMethod = factoryClass.getDeclaredMethod("getProviderClass");
} else if (sdkInt == 22) {
getProviderClassMethod = factoryClass.getDeclaredMethod("getFactoryClass");
} else {
Log.i(TAG, "Don't need to Hook WebView");
return;
}
getProviderClassMethod.setAccessible(true);
Class<?> factoryProviderClass = (Class<?>) getProviderClassMethod.invoke(factoryClass);
Class<?> delegateClass = Class.forName("android.webkit.WebViewDelegate");
Constructor<?> delegateConstructor = delegateClass.getDeclaredConstructor();
delegateConstructor.setAccessible(true);
if (sdkInt < 26) {
Constructor<?> providerConstructor = factoryProviderClass.getConstructor(delegateClass);
if (providerConstructor!= null) {
providerConstructor.setAccessible(true);
sProviderInstance = providerConstructor.newInstance(delegateConstructor.newInstance());
}
} else {
Field chromiumMethodName = factoryClass.getDeclaredField("CHROMIUM_WEBVIEW_FACTORY_METHOD");
chromiumMethodName.setAccessible(true);
String chromiumMethodNameStr = (String) chromiumMethodName.get(null);
if (chromiumMethodNameStr == null) {
chromiumMethodNameStr = "create";
}
Method staticFactory = factoryProviderClass.getMethod(chromiumMethodNameStr, delegateClass);
if (staticFactory!= null) {
sProviderInstance = staticFactory.invoke(null, delegateConstructor.newInstance());
}
}
if (sProviderInstance!= null) {
field.set(null, sProviderInstance);
Log.i(TAG, "Hook success!");
} else {
Log.i(TAG, "Hook failed!");
}
} catch (Exception e) {
Log.w(TAG, e);
}
}
反射實現系統語言切換
獲取到系統權限之后就可以通過反射方法拿到framework中的LocalePicker類,然后調用其中的updateLocale方法就可以實現系統語言切換。
<MainActivity.java>
private final List<String> systemLanguageList = new ArrayList<String>(
Arrays.asList(
"zh", "en", "fr", "es", "de", "it", "pt", "ru", "pl", "ar", "tr", "vi",
"hu", "nl", "ro", "cs", "bg", "uk", "hr", "sr", "el", "no", "da"
));
private void initView(){
findViewById(R.id.get_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getLanguage();
}
});
findViewById(R.id.set_btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
setLanguage(systemLanguageList.get(1));
}
});
}
// 獲取當前系統語言
private void getLanguage(){
// 獲取當前Activity的Locale
Locale activityLocale = getResources().getConfiguration().locale;
// 獲取語言代碼
String language = activityLocale.getLanguage();
Log.d("getLanguage", "current Language: " + language );
}
// 設置當前系統語言
private void setLanguage(String language){
try {
Class localPicker = Class.forName("com.android.internal.app.LocalePicker");
Method updateLocale = localPicker.getDeclaredMethod("updateLocale", Locale.class);
updateLocale.invoke(null, new Locale(language, ""));
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
Log.d("error", "try setLanguage Exception " );
}
}
導入framework.jar
1. 拷貝framework.jar
首先,需要從源碼中把framework.jar拷貝到本地工程app/libs下,可將源碼中的jar更名為framework.jar
Android N/O: 7 和 8
out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar
Android P/Q: 9 和 10
out/soong/.intermediates/frameworks/base/framework/android_common/combined/framework.jar
Android R: 11以上
out/soong/.intermediates/frameworks/base/framework-minus-apex/android_common/combined/framework-minus-apex.jar
2. Android Studio適配
.iml文件是IntelliJ IDEA和Android Studio用來存儲模塊級別的配置信息的文件,3.6.3以后的Android Stuido版本默認關閉 .iml 文件的生成,導入framework.jar需要配置使用的優先級,所有需要先把Android Stuido中生成 .iml 文件的功能打開。
Android Studio —> File —> Settings —> Build... —> Build Tools —> Gradle —> 勾選Generate *.iml files for modules imported from Gradle —> Apply —> OK —> 重啟Android Studio
3. 修改build.gradle(:app)配置
(1)添加framework.jar依賴
compileOnly files('libs\\framework.jar')
(2)修改資源鏈接優先級
// 優先鏈接framework.jar
gradle.projectsEvaluated {
// 方法一
tasks.withType(JavaCompile) {
Set<File> fileSet = options.bootstrapClasspath.getFiles();
List<File> newFileList = new ArrayList<>()
newFileList.add(new File("libs/framework.jar"))
newFileList.addAll(fileSet)
options.bootstrapClasspath = files(newFileList.toArray())
}
// 方法二
// tasks.withType(JavaCompile).tap {
// configureEach {
// options.compilerArgs.add("-Xbootclasspath/p:$rootProject.rootDir/app/libs/framework.jar")
// }
// }
}
(3)修改類的使用優先級
// 降低Android SDK的使用級別,優先使用framework.jar中的類文件
preBuild {
doLast {
def rootProjectName = rootProject.name.replace(" ", "_")
def projectName = project.name.replace(" ", "_")
def iml_path = "$rootProject.rootDir\\.idea\\modules\\" + projectName + "\\" + rootProjectName + "." + projectName + ".main.iml"
def imlFile = file(iml_path)
try {
// 如果AS未適配,這里會找不到XmlParser
def parsedXml = (new XmlParser()).parse(imlFile)
def jdkNode = parsedXml.component[1].orderEntry.find { it.'@type' == 'jdk' }
def sdkString = jdkNode.'@jdkName'
parsedXml.component[1].remove(jdkNode)
new Node(parsedXml.component[1], 'orderEntry', ['type': 'jdk', 'jdkName': sdkString, 'jdkType': 'Android SDK'])
groovy.xml.XmlUtil.serialize(parsedXml, new FileOutputStream(imlFile))
} catch (FileNotFoundException e) {
e.printStackTrace()
}
}
}
(4)解決65536限制
MultiDex是Android開發中用于解決65536個方法限制的一種機制,當應用的代碼量超過65536個方法時,需要使用MultiDex來將應用拆分成多個DEX文件。
android {
...
defaultConfig {
applicationId "com.android.kc9demo"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
...
}
dependencies{
...
implementation 'com.android.support:multidex:1.0.0'
}
(5)使用framework.jar中的方法
導入framework.jar之后,在MainActivity中調用updateLocale方法就可以直接使用了。
private void setLanguage(String language){
LocalePicker.updateLocale(new Locale(language, ""));
}