前言
上一篇我们通过为Android系统开启模拟辅助设备功能开关,最终实现了将一个Activity显示到多个屏幕的效果。
本篇文章我们具体来分析一下当我们开启模拟辅助设备功能开关的时候,Android系统做了什么哪些操作。
一、模拟辅助设备功能开关应用位置
Android12系统中,车机系统有一个专门的开发者选项页面,其完整名称如下:
com.android.car.developeroptions/com.android.car.developeroptions.CarDevelopmentSettingsDashboardActivity
可以发现此Activity对应的包名为com.android.car.developeroptions,输入adb命令:
adb shell pm path com.android.car.developeroptions
返回的结果是:
/system_ext/priv-app/CarDeveloperOptions/CarDeveloperOptions.apk
可以发现是一个名为CarDeveloperOptions的车机系统应用,直接在aosp源码中进行搜索,搜索结果如下:
可以发现这个系统应用位于/packages/services/Car/packages/CarDeveloperOptions目录。
二、模拟辅助设备功能开关相关源码
2.1 系统开发者选项对应的页面声明
CarDeveloperOptions系统应用的AndroidManifest.xml文件中对开发者选项页面的声明如下。
services/Car/packages/CarDeveloperOptions/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:tools="http://schemas.android.com/tools"
coreApp="true"
package="com.android.car.developeroptions"
android:sharedUserId="android.uid.system">
...代码省略...
<activity
android:name=".CarDevelopmentSettingsDashboardActivity"
android:enabled="false"
android:exported="true"
android:icon="@drawable/ic_settings_development"
android:label="@string/development_settings_title"
android:taskAffinity=""
android:theme="@style/Theme.CarDeveloperOptions">
<intent-filter android:priority="1">
<action android:name="android.settings.APPLICATION_DEVELOPMENT_SETTINGS"/>
<action android:name="com.android.settings.APPLICATION_DEVELOPMENT_SETTINGS"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<meta-data android:name="com.android.settings.summary"
android:resource="@string/summary_empty"/>
<meta-data android:name="com.android.settings.FRAGMENT_CLASS"
android:value="com.android.car.developeroptions.CarDevelopmentSettingsDashboardFragment"/>
<meta-data android:name="com.android.settings.PRIMARY_PROFILE_CONTROLLED"
android:value="true"/>
</activity>
...代码省略...
</manifest>
2.2 系统开发者选项对应的Activity
1、CarDevelopmentSettingsDashboardActivity的系统源码非常简洁。
services/Car/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentSettingsDashboardActivity.java
public class CarDevelopmentSettingsDashboardActivity extends SettingsActivity {
private static final String CAR_DEVELOPMENT_SETTINGS_FRAGMENT =
"com.android.car.developeroptions.CarDevelopmentSettingsDashboardFragment";
@Override
protected boolean isValidFragment(String fragmentName) {
return CAR_DEVELOPMENT_SETTINGS_FRAGMENT.equals(fragmentName);
}
@Override
protected boolean isToolbarEnabled() {
// Disable the default Settings toolbar in favor of a chassis toolbar.
return false;
}
}
此类中的源码非常简单,最关键的就是CAR_DEVELOPMENT_SETTINGS_FRAGMENT 这个字段,该字段指向了一个Fragment,该Fragment才是开发者选项页面的真正载体,
2、想要明白CarDevelopmentSettingsDashboardActivity页面的具体加载流程,我们有必要看下其父类SettingsActivity 。
packages/apps/Settings/src/com/android/settings/SettingsActivity.java
public class SettingsActivity extends SettingsBaseActivity
implements PreferenceManager.OnPreferenceTreeClickListener,
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
ButtonBarHandler, FragmentManager.OnBackStackChangedListener {
@Override
protected void onCreate(Bundle savedState) {
...代码省略...
//加载布局文件
setContentView(R.layout.settings_main_prefs);
...代码省略...
//获取页面参数
final String initialFragmentName = getInitialFragmentName(intent);
...代码省略...
//加载设置模块具体页面对应的fragment
launchSettingFragment(initialFragmentName, intent);
...代码省略...
}
/**
* 将initialFragmentName指向的fragment加载到当前Activity中
*/
void launchSettingFragment(String initialFragmentName, Intent intent) {
if (initialFragmentName != null) {
setTitleFromIntent(intent);
Bundle initialArguments = intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);
switchToFragment(initialFragmentName, initialArguments, true,
mInitialTitleResId, mInitialTitle);
} else {
// Show search icon as up affordance if we are displaying the main Dashboard
mInitialTitleResId = R.string.dashboard_title;
switchToFragment(TopLevelSettings.class.getName(), null /* args */, false,
mInitialTitleResId, mInitialTitle);
}
}
/**
* 将fragmentName指向的fragment加载到当前Activity中
*/
private void switchToFragment(String fragmentName, Bundle args, boolean validate,
int titleResId, CharSequence title) {
Log.d(LOG_TAG, "Switching to fragment " + fragmentName);
if (validate && !isValidFragment(fragmentName)) {
throw new IllegalArgumentException("Invalid fragment for this activity: "
+ fragmentName);
}
Fragment f = Utils.getTargetFragment(this, fragmentName, args);
if (f == null) {
return;
}
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.main_content, f);
if (titleResId > 0) {
transaction.setBreadCrumbTitle(titleResId);
} else if (title != null) {
transaction.setBreadCrumbTitle(title);
}
transaction.commitAllowingStateLoss();
getSupportFragmentManager().executePendingTransactions();
Log.d(LOG_TAG, "Executed frag manager pendingTransactions");
}
}
packages/apps/Settings/res/layout/settings_main_prefs.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_height="match_parent"
android:layout_width="match_parent">
<com.android.settings.widget.SettingsMainSwitchBar
android:id="@+id/switch_bar"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<FrameLayout
android:id="@+id/main_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<RelativeLayout android:id="@+id/button_bar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_weight="0"
android:visibility="gone">
<Button android:id="@+id/back_button"
android:layout_width="150dip"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:layout_alignParentStart="true"
android:text="@*android:string/back_button_label"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true">
<Button android:id="@+id/skip_button"
android:layout_width="150dip"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:text="@*android:string/skip_button_label"
android:visibility="gone"/>
<Button android:id="@+id/next_button"
android:layout_width="150dip"
android:layout_height="wrap_content"
android:layout_margin="5dip"
android:text="@*android:string/next_button_label"/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
上面我们列出了SettingsActivity和UI加载相关的源码,可以发现SettingsActivity先是加载了一个名为settings_main_prefs的布局文件,然后将initialFragmentName指向的fragment添加到了当前页面上。结合CarDevelopmentSettingsDashboardActivity的源码我们可以知道,开发者选项页面的真正载体是CarDevelopmentSettingsDashboardFragment。
packages/services/Car/packages/CarDeveloperOptions/src/com/android/car/developeroptions/CarDevelopmentSettingsDashboardFragment.java
public class CarDevelopmentSettingsDashboardFragment extends DevelopmentSettingsDashboardFragment {
}
packages/apps/Settings/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFragment
implements OnMainSwitchChangeListener, OemUnlockDialogHost, AdbDialogHost,
AdbClearKeysDialogHost, LogPersistDialogHost,
BluetoothA2dpHwOffloadRebootDialog.OnA2dpHwDialogConfirmedListener,
AbstractBluetoothPreferenceController.Callback {
@Override
protected int getPreferenceScreenResId() {
return Utils.isMonkeyRunning() ? R.xml.placeholder_prefs : R.xml.development_settings;//页面对应的布局文件
}
private static List<AbstractPreferenceController> buildPreferenceControllers(Context context,
Activity activity, Lifecycle lifecycle, DevelopmentSettingsDashboardFragment fragment,
BluetoothA2dpConfigStore bluetoothA2dpConfigStore) {
final List<AbstractPreferenceController> controllers = new ArrayList<>();
...代码省略...
controllers.add(new SecondaryDisplayPreferenceController(context));//模拟辅助设备功能组件控制器
controllers.add(new GpuViewUpdatesPreferenceController(context));
controllers.add(new HardwareLayersUpdatesPreferenceController(context));
controllers.add(new DebugGpuOverdrawPreferenceController(context));
controllers.add(new DebugNonRectClipOperationsPreferenceController(context));
controllers.add(new ForceDarkPreferenceController(context));
controllers.add(new EnableBlursPreferenceController(context));
controllers.add(new ForceMSAAPreferenceController(context));
controllers.add(new HardwareOverlaysPreferenceController(context));
controllers.add(new SimulateColorSpacePreferenceController(context));
controllers.add(new UsbAudioRoutingPreferenceController(context));
controllers.add(new StrictModePreferenceController(context));
controllers.add(new ProfileGpuRenderingPreferenceController(context));
controllers.add(new KeepActivitiesPreferenceController(context));
controllers.add(new BackgroundProcessLimitPreferenceController(context));
controllers.add(new CachedAppsFreezerPreferenceController(context));
controllers.add(new ShowFirstCrashDialogPreferenceController(context));
controllers.add(new AppsNotRespondingPreferenceController(context));
controllers.add(new NotificationChannelWarningsPreferenceController(context));
controllers.add(new AllowAppsOnExternalPreferenceController(context));
controllers.add(new ResizableActivityPreferenceController(context));
controllers.add(new FreeformWindowsPreferenceController(context));
controllers.add(new DesktopModePreferenceController(context));
controllers.add(new NonResizableMultiWindowPreferenceController(context));
controllers.add(new ShortcutManagerThrottlingPreferenceController(context));
controllers.add(new EnableGnssRawMeasFullTrackingPreferenceController(context));
controllers.add(new DefaultLaunchPreferenceController(context, "running_apps"));
controllers.add(new DefaultLaunchPreferenceController(context, "demo_mode"));
controllers.add(new DefaultLaunchPreferenceController(context, "quick_settings_tiles"));
controllers.add(new DefaultLaunchPreferenceController(context, "feature_flags_dashboard"));
controllers.add(new DefaultUsbConfigurationPreferenceController(context));
controllers.add(new DefaultLaunchPreferenceController(context, "density"));
controllers.add(new DefaultLaunchPreferenceController(context, "background_check"));
controllers.add(new DefaultLaunchPreferenceController(context, "inactive_apps"));
controllers.add(new AutofillLoggingLevelPreferenceController(context, lifecycle));
controllers.add(new AutofillResetOptionsPreferenceController(context));
controllers.add(new BluetoothCodecDialogPreferenceController(context, lifecycle,
bluetoothA2dpConfigStore, fragment));
controllers.add(new BluetoothSampleRateDialogPreferenceController(context, lifecycle,
bluetoothA2dpConfigStore));
controllers.add(new BluetoothBitPerSampleDialogPreferenceController(context, lifecycle,
bluetoothA2dpConfigStore));
controllers.add(new BluetoothQualityDialogPreferenceController(context, lifecycle,
bluetoothA2dpConfigStore));
controllers.add(new BluetoothChannelModeDialogPreferenceController(context, lifecycle,
bluetoothA2dpConfigStore));
controllers.add(new BluetoothHDAudioPreferenceController(context, lifecycle,
bluetoothA2dpConfigStore, fragment));
controllers.add(new SharedDataPreferenceController(context));
controllers.add(new OverlaySettingsPreferenceController(context));
return controllers;
}
}
2.3 模拟辅助显示设备功能开关
1、由于CarDevelopmentSettingsDashboardFragment构建页面也和其他Settings模块的页面一样,大量使用了Preference这套组件来构建页面,如果对于Preference完全不了解,可以参考一下Android 12系统源码_Settings(一)认识Preference这篇文章。
由于Preference构建视图和常见的Android构建视图的方案有很大差异,要想使用Android那套UI架构来分析Settings模块的源码基本不可行,这里我们直接在aosp中搜索“模拟辅助显示设备”这几个字,搜索结果如下所示。
可以发现“模拟辅助显示设备”这个字符串对应的资源名称为overlay_display_devices_title。
2、继续在aosp中进行类型为.xml,名称为overlay_display_devices_title的资源的搜索,会发现development_settings.xml这个文件有引用。
packages/apps/Settings/res/xml/development_settings.xml
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:key="development_prefs_screen"
android:title="@string/development_settings_title">
...代码省略...
<PreferenceCategory
android:key="debug_drawing_category"
android:title="@string/debug_drawing_category"
android:order="600">
...代码省略...
<!--模拟辅助显示设备功能开关-->
<ListPreference
android:key="overlay_display_devices"
android:title="@string/overlay_display_devices_title"
android:entries="@array/overlay_display_devices_entries"
android:entryValues="@array/overlay_display_devices_values" />
...代码省略...
</PreferenceCategory>
...代码省略...
</PreferenceScreen>
base/packages/SettingsLib/res/values-zh-rCN/arrays.xml
<!-- 模拟辅助设备的条目标题 -->
<string-array name="overlay_display_devices_entries">
<item msgid="4497393944195787240">"无"</item>
<item msgid="8461943978957133391">"480p"</item>
<item msgid="6923083594932909205">"480p(安全)"</item>
<item msgid="1226941831391497335">"720p"</item>
<item msgid="7051983425968643928">"720p(安全)"</item>
<item msgid="7765795608738980305">"1080p"</item>
<item msgid="8084293856795803592">"1080p(安全)"</item>
<item msgid="938784192903353277">"4K"</item>
<item msgid="8612549335720461635">"4K(安全)"</item>
<item msgid="7322156123728520872">"4K(画质提升)"</item>
<item msgid="7735692090314849188">"4K(画质提升、安全)"</item>
<item msgid="7346816300608639624">"720p,1080p(双屏)"</item>
</string-array>
base/packages/SettingsLib/res/values/arrays.xml
<!-- 模拟辅助设备的条目属性值 -->
<string-array name="overlay_display_devices_values" translatable="false" >
<item></item>
<item>720x480/142</item>
<item>720x480/142,secure</item>
<item>1280x720/213</item>
<item>1280x720/213,secure</item>
<item>1920x1080/320</item>
<item>1920x1080/320,secure</item>
<item>3840x2160/320</item>
<item>3840x2160/320,secure</item>
<item>1920x1080/320|3840x2160/640</item>
<item>1920x1080/320|3840x2160/640,secure</item>
<item>1280x720/213;1920x1080/320</item>
</string-array>
结合布局文件可知,key值为overlay_display_devices的ListPreference组件就是我们要找的模拟辅助显示设备功能开关组件,其功能开关子条目标题和属性值刚好对应了
3、进一步在aosp中进行类型为.java,名称为overlay_display_devices的资源的搜索,会发现SecondaryDisplayPreferenceController.java这个类有引用,前面承载开发者设置页面内容的DevelopmentSettingsDashboardFragment里面就有引用到这个类。
/packages/apps/Settings/src/com/android/settings/development/SecondaryDisplayPreferenceController.java
public class SecondaryDisplayPreferenceController extends DeveloperOptionsPreferenceController
implements Preference.OnPreferenceChangeListener, PreferenceControllerMixin {
private static final String OVERLAY_DISPLAY_DEVICES_KEY = "overlay_display_devices";
private final String[] mListValues;
private final String[] mListSummaries;
public SecondaryDisplayPreferenceController(Context context) {
super(context);
mListValues = context.getResources().getStringArray(R.array.overlay_display_devices_values);
mListSummaries = context.getResources().getStringArray(
R.array.overlay_display_devices_entries);
}
@Override
public String getPreferenceKey() {
return OVERLAY_DISPLAY_DEVICES_KEY;//preference组件的唯一key值
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
//用户选择了条目内容,对开关属性进行更新和数据保存
writeSecondaryDisplayDevicesOption(newValue.toString());
return true;
}
@Override
public void updateState(Preference preference) {
//初始化模拟辅助设备功能更开关的属性值
updateSecondaryDisplayDevicesOptions();
}
@Override
protected void onDeveloperOptionsSwitchDisabled() {
super.onDeveloperOptionsSwitchDisabled();
writeSecondaryDisplayDevicesOption(null);
}
private void updateSecondaryDisplayDevicesOptions() {
//从global中获取当前模拟辅助设备功能开关的属性值
final String value = Settings.Global.getString(mContext.getContentResolver(),
Settings.Global.OVERLAY_DISPLAY_DEVICES);
//获取当前选中的条目序列号
int index = 0; // default
for (int i = 0; i < mListValues.length; i++) {
if (TextUtils.equals(value, mListValues[i])) {
index = i;
break;
}
}
final ListPreference listPreference = (ListPreference) mPreference;
//设置模拟辅助设备功能开关菜单条目列表中当前选中的条目
listPreference.setValue(mListValues[index]);
listPreference.setSummary(mListSummaries[index]);
}
private void writeSecondaryDisplayDevicesOption(String newValue) {
//更新模拟辅助设备功能开关的属性值到global里面
Settings.Global.putString(mContext.getContentResolver(),
Settings.Global.OVERLAY_DISPLAY_DEVICES, newValue);
updateSecondaryDisplayDevicesOptions();
}
}
public final class Settings {
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@TestApi
@Readable
public static final String OVERLAY_DISPLAY_DEVICES = "overlay_display_devices";//模拟辅助设备功能开关属性对应的字段
}
用户对模拟辅助设备功能开关的操作最终会触发SecondaryDisplayPreferenceController的onPreferenceChange方法回调,该方法调用writeSecondaryDisplayDevicesOption将当前用户的选择是开关属性值以key值为overlay_display_devices保存到了global里面,这就意味着我们通过模拟辅助设备功能开关,最终就只是将一串字符串存储到了key为overlay_display_devices的值的global内容。
三、模拟辅助设备功能开关监听者
1、OverlayDisplayAdapter类中有对global的overlay_display_devices字段的变化做监听,这样该字段发生变化的时候可以收到回调。
frameworks/base/services/core/java/com/android/server/display/OverlayDisplayAdapter.java
final class OverlayDisplayAdapter extends DisplayAdapter {
private final Handler mUiHandler;
// Called with SyncRoot lock held.
public OverlayDisplayAdapter(DisplayManagerService.SyncRoot syncRoot,
Context context, Handler handler, Listener listener, Handler uiHandler) {
super(syncRoot, context, handler, listener, TAG);
mUiHandler = uiHandler;
}
@Override
public void registerLocked() {
super.registerLocked();
getHandler().post(new Runnable() {
@Override
public void run() {
//注册监听overlay_display_devices字段的内容变化
getContext().getContentResolver().registerContentObserver(
Settings.Global.getUriFor(Settings.Global.OVERLAY_DISPLAY_DEVICES),
true, new ContentObserver(getHandler()) {
@Override
public void onChange(boolean selfChange) {
//触发回调
updateOverlayDisplayDevices();
}
});
updateOverlayDisplayDevices();
}
});
}
private void updateOverlayDisplayDevices() {
synchronized (getSyncRoot()) {
//继续调用updateOverlayDisplayDevicesLocked方法
updateOverlayDisplayDevicesLocked();
}
}
}
2、global的overlay_display_devices字段内容发生变化的时候,会回调OverlayDisplayAdapter的updateOverlayDisplayDevices方法。
该方法上锁之后继续调用updateOverlayDisplayDevicesLocked方法。
final class OverlayDisplayAdapter extends DisplayAdapter {
private final ArrayList<OverlayDisplayHandle> mOverlays =
new ArrayList<OverlayDisplayHandle>();
private String mCurrentOverlaySetting = "";//当前的模拟辅助设备属性值
private void updateOverlayDisplayDevicesLocked() {
String value = Settings.Global.getString(getContext().getContentResolver(),
Settings.Global.OVERLAY_DISPLAY_DEVICES);
if (value == null) {
value = "";
}
if (value.equals(mCurrentOverlaySetting)) {
return;
}
mCurrentOverlaySetting = value;
if (!mOverlays.isEmpty()) {
Slog.i(TAG, "Dismissing all overlay display devices.");
for (OverlayDisplayHandle overlay : mOverlays) {
overlay.dismissLocked();
}
mOverlays.clear();
}
int count = 0;
for (String part : value.split(DISPLAY_SPLITTER)) {
Matcher displayMatcher = DISPLAY_PATTERN.matcher(part);
if (displayMatcher.matches()) {
if (count >= 4) {
Slog.w(TAG, "Too many overlay display devices specified: " + value);
break;
}
String modeString = displayMatcher.group(1);
String flagString = displayMatcher.group(2);
ArrayList<OverlayMode> modes = new ArrayList<>();
for (String mode : modeString.split(MODE_SPLITTER)) {
Matcher modeMatcher = MODE_PATTERN.matcher(mode);
if (modeMatcher.matches()) {
try {
int width = Integer.parseInt(modeMatcher.group(1), 10);
int height = Integer.parseInt(modeMatcher.group(2), 10);
int densityDpi = Integer.parseInt(modeMatcher.group(3), 10);
if (width >= MIN_WIDTH && width <= MAX_WIDTH
&& height >= MIN_HEIGHT && height <= MAX_HEIGHT
&& densityDpi >= DisplayMetrics.DENSITY_LOW
&& densityDpi <= DisplayMetrics.DENSITY_XXXHIGH) {
modes.add(new OverlayMode(width, height, densityDpi));
continue;
} else {
Slog.w(TAG, "Ignoring out-of-range overlay display mode: " + mode);
}
} catch (NumberFormatException ex) {
}
} else if (mode.isEmpty()) {
continue;
}
}
if (!modes.isEmpty()) {
int number = ++count;
String name = getContext().getResources().getString(
com.android.internal.R.string.display_manager_overlay_display_name,
number);
int gravity = chooseOverlayGravity(number);
OverlayFlags flags = OverlayFlags.parseFlags(flagString);
Slog.i(TAG, "Showing overlay display device #" + number
+ ": name=" + name + ", modes=" + Arrays.toString(modes.toArray())
+ ", flags=" + flags);
mOverlays.add(new OverlayDisplayHandle(name, modes, gravity, flags, number));
continue;
}
}
Slog.w(TAG, "Malformed overlay display devices setting: " + value);
}
}
}