常规 App 开发,Android SDK 下载都是通过 Google 官方渠道获得的。对于定制过的 Android 系统,我们一般手里都有源码,会在 Framework 定制一些需求,这需要我们导出 API 给 App 使用。
一、编译 win-sdk
编译 win-sdk,只能使用 Linux 系统,下面我编译的 Android 源码基于 IMX6 芯片, Android 6.0.1。编译 win-sdk 首先要编译 linux-sdk。
1.1 编译 Linux Android SDK
- source ./build/envsetup.sh
- lunch sdk-eng
- make -j16 sdk
“-j16”可以根据自己的电脑配置替换,具体数值一般为核心数 * 2,比如 8 核心 cpu,即 16。
1.2 编译 Windows Android SDK
- 安装 tofrodos
sudo apt-get install tofrodos
- 安装 mingw32
sudo apt-get install mingw32
- source ./build/envsetup.sh
- lunch sdk-eng
- make win_sdk
编译成功之后可以在路径(out/host/windows/sdk/android-sdk_eng.${USER}_windows.zip)下找到 win-sdk。
二、Android Studio 配置 win-sdk
打开 Android Studio,找到 sdk 配置入口:File -> Settings -> Appearance & Behavior -> System Settings -> Android SDK -> Android SDK Location,修改为刚刚拷贝过来的 sdk (android-sdk_eng.${USER}_windows.zip)解压后的路径。
如果工程中需要更新 build tools 直接更新即可。
三、开放语言设置权限给第三方 APP
首先要找到设置语言的入口,然后定制 Framework 抛出接口给第三方调用,接着编译 android.jar,替换 win-sdk 中的 android.jar,最后编译 system.img 烧写到机器搭配 APP 验证。
3.1 设置语言入口
设置语言入口位于 LocalePicker 类中,系统 APP 调用 updateLocale 静态方法即可切换语言。updateLocale 先获取 ActivityManagerProxy,接着调用 getConfiguration() 方法获取 Configuration 对象,然后设置其 Locale,最后更新配置,当然这最终会调用 ActivityManagerService 的 updateConfiguration 方法。
frameworks/base/core/java/com/android/internal/app/LocalePicker.java
public class LocalePicker extends ListFragment {
......
public static void updateLocale(Locale locale) {
try {
IActivityManager am = ActivityManagerNative.getDefault();
Configuration config = am.getConfiguration();
config.setLocale(locale);
config.userSetLocale = true;
am.updateConfiguration(config);
// Trigger the dirty bit for the Settings Provider.
BackupManager.dataChanged("com.android.providers.settings");
} catch (RemoteException e) {
// Intentionally left blank
}
}
......
}
3.2 开放 updateLocale API
在 frameworks/base/core/java/android/app/ 增加 LanguageChanger.java。它包装了 LocalePicker.updateLocale 方法。这就会从 Framework 导出 API。
frameworks/base/core/java/android/app/LanguageChanger.java
package android.app;
import com.android.internal.app.LocalePicker;
import java.util.Locale;
public class LanguageChanger {
/**Change language*/
public static void updateLocale(Locale locale){
LocalePicker.updateLocale(locale);
}
}
重新编译 sdk
- make update-api
- 编译 Linux sdk(参见 1.1)
由于我们只提取 android.jar,因此编译 Linux sdk 即可。编译结束以后可以到路径(out/target/common/obj/PACKAGING/android_jar_intermediates/ )提取 android.jar。拷贝此 android.jar 覆盖路径(android-sdk_eng.${USER}_windows/platforms/android-23/ )下的 android.jar。
3.3 编写测试 Demo
如果我们直接调用 LocalePicker.updateLocale 毫无疑问编译都无法通过。
下面是 MainActivity 中调用自定义切换语言的代码,当点击按钮的时候直接把系统语言切换为日语。
package helper.update.com.myapplication
import android.app.LanguageChanger
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.util.Log
import java.util.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val btn = findViewById(R.id.button)
btn?.setOnClickListener {
var localeResult: Locale? = null
val availableList = Locale.getAvailableLocales()
for (locale in availableList) {
val language = locale.language
if (language.equals("ja", ignoreCase = true)) {
localeResult = locale
break
}
}
// 设置日语
//LocalePicker.updateLocale(localeResult)
if (null != localeResult) {
LanguageChanger.updateLocale(localeResult)
Log.d("MainActivity", "Change Language to ja")
}
}
}
}
AndroidManifest.xml 中注意添加 CHANGE_CONFIGURATION 权限。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="helper.update.com.myapplication">
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
build.gradle 中也要注意配置使用我们的 sdk,compileSdkVersion 要指定为对应版本。
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 23
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "helper.update.com.myapplication"
minSdkVersion 23
targetSdkVersion 23
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:23.4.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
3.4 烧写 system.img 并运行 Demo 验证
- make systemimage
- adb reboot bootloader
- fastboot flash system C:\Users\xxx\Desktop\write\new\system.img
- 长按设备电源键关机,开机以后安装 Demo.apk。
不出意外运行以后 Apk 直接挂掉了。
java.lang.SecurityException: Permission Denial: updateConfiguration() from pid=2667, uid=10029 requires android.permission.CHANGE_CONFIGURATION
at android.os.Parcel.readException(Parcel.java:1620)
at android.os.Parcel.readException(Parcel.java:1573)
at android.app.ActivityManagerProxy.updateConfiguration(ActivityManagerNative.java:3935)
at com.android.internal.app.LocalePicker.updateLocale(LocalePicker.java:252)
at android.app.LanguageChanger.updateLocale(LanguageChanger.java:9)
at helper.update.com.myapplication.MainActivity$onCreate$1.onClick(MainActivity.kt:33)
at android.view.View.performClick(View.java:5204)
at android.view.View$PerformClick.run(View.java:21155)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5422)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
根据 Log 可快速锁定为权限问题。ActivityManagerProxy 调用 updateConfiguration 方法,实际上会远程调用 ActivityManagerService 中的同名方法,也就是说 ActivityManagerService 中的 updateConfiguration 方法抛出的权限异常。
enforceCallingPermission 方法中做了权限检查,既然要开放给第三方 APP 使用,直接注释掉即可。
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
public final class ActivityManagerService extends ActivityManagerNative
implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
......
public void updateConfiguration(Configuration values) {
/*enforceCallingPermission(android.Manifest.permission.CHANGE_CONFIGURATION,
"updateConfiguration()");*/
synchronized(this) {
if (values == null && mWindowManager != null) {
// sentinel: fetch the current configuration from the window manager
values = mWindowManager.computeNewConfiguration();
}
if (mWindowManager != null) {
mProcessList.applyDisplaySize(mWindowManager);
}
final long origId = Binder.clearCallingIdentity();
if (values != null) {
Settings.System.clearConfiguration(values);
}
updateConfigurationLocked(values, null, false, false);
Binder.restoreCallingIdentity(origId);
}
}
......
}
再次重新编译 system.img 烧写到设备。点击程序按钮切换日语,发现切换成功了。