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, ""));
}