Android 源码开放语言设置给第三方 APP 实践

501 阅读3分钟

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

  1. source ./build/envsetup.sh
  2. lunch sdk-eng
  3. make -j16 sdk

“-j16”可以根据自己的电脑配置替换,具体数值一般为核心数 * 2,比如 8 核心 cpu,即 16。

1.2 编译 Windows Android SDK

  1. 安装 tofrodos
sudo apt-get install tofrodos
  1. 安装 mingw32
sudo apt-get install mingw32
  1. source ./build/envsetup.sh
  2. lunch sdk-eng
  3. 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

  1. make update-api
  2. 编译 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 验证

  1. make systemimage
  2. adb reboot bootloader
  3. fastboot flash system C:\Users\xxx\Desktop\write\new\system.img
  4. 长按设备电源键关机,开机以后安装 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 烧写到设备。点击程序按钮切换日语,发现切换成功了。