<meta charset="utf-8">
一了嚎、概述
私密空間是Android15新特性萎津,涉及了多個模塊。本文從設置递宅,框架娘香,Launcher3個模塊對私密空間主流程進行分析
二、開始
- 設置 ->安全和隱私->私密空間
私密空間數(shù)據(jù)初始化
設置監(jiān)聽開機廣播初始化安全數(shù)據(jù)
public class SafetySourceBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
...
if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
refreshAllSafetySources(context, EVENT_DEVICE_REBOOTED);
}
}
接著PrivateSpaceSafetySource中設置鎖屏安全數(shù)據(jù)
public static void setSafetySourceData(Context context,
SafetyEvent safetyEvent) {
...
//判斷是否滿足了私密空間開啟的前置條件
1:safetyCenter page is enabled
2:PrivateSpaceFeatures is enabled
3:userManager.isMainUser()
// 獲取 PrivateSpaceAuthenticationActivity的PendingIntent
PendingIntent pendingIntent = getPendingIntentForPsDashboard(context);
//設置隱私安全的title办龄,sunnary烘绽,以及 PendingIntent
SafetySourceStatus status = new SafetySourceStatus.Builder(
context.getString(R.string.private_space_title),
context.getString(R.string.private_space_summary),
SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED)
.setPendingIntent(pendingIntent).build();
SafetySourceData safetySourceData =
new SafetySourceData.Builder().setStatus(status).build();
Log.d(TAG, "Setting safety source data:"+ Log.getStackTraceString(new Throwable("setSafetySourceData")));
SafetyCenterManagerWrapper.get().setSafetySourceData(
context,
SAFETY_SOURCE_ID,
safetySourceData,
safetyEvent
);
點擊私密空間后跳轉(zhuǎn)到PrivateSpaceAuthenticationActivity
PrivateSpaceAuthenticationActivity:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
//判斷鎖屏密碼是否設置
if (getKeyguardManager().isDeviceSecure()) {
if (savedInstanceState == null) {
//判斷私密空間是否已存在,存在則進行解鎖并進入私密空間
if (mPrivateSpaceMaintainer.doesPrivateSpaceExist()) {
unlockAndLaunchPrivateSpaceSettings(this);
} else {
//不存在則驗證密碼并創(chuàng)建私密空間
authenticatePrivateSpaceEntry();
}
}
} else {
//彈窗提示用戶設置鎖屏密碼
promptToSetDeviceLock();
}
}
創(chuàng)建私密空間
PrivateSpaceAuthenticationActivity#authenticatePrivateSpaceEntry中
private void authenticatePrivateSpaceEntry() {
//獲取私密空間的鎖屏憑證意圖(Intent)的方法俐填,主要用于管理和訪問 Android 中的私密空間安接。
Intent credentialIntent = mPrivateSpaceMaintainer.getPrivateProfileLockCredentialIntent();
if (credentialIntent != null) {
if (android.multiuser.Flags.usePrivateSpaceIconInBiometricPrompt()) {
credentialIntent.putExtra(CUSTOM_BIOMETRIC_PROMPT_LOGO_RES_ID_KEY,
com.android.internal.R.drawable.stat_sys_private_profile_status);
credentialIntent.putExtra(CUSTOM_BIOMETRIC_PROMPT_LOGO_DESCRIPTION_KEY,
getApplicationContext().getString(
com.android.internal.R.string.private_space_biometric_prompt_title
));
}
//驗證鎖屏密碼
mVerifyDeviceLock.launch(credentialIntent);
} else {
Log.e(TAG, "verifyCredentialIntent is null even though device lock is set");
finish();
}
}
驗證完鎖屏密碼后執(zhí)行PrivateSpaceAuthenticationActivity#onLockAuthentication方法
@VisibleForTesting
public void onLockAuthentication(Context context) {
if (mPrivateSpaceMaintainer.doesPrivateSpaceExist()) {
unlockAndLaunchPrivateSpaceSettings(context);
} else {
//私密空間不存在則進入私密空間引導頁
startActivity(new Intent(context, PrivateSpaceSetupActivity.class));
finish();
}
}
[圖片上傳失敗...(image-a1d04-1732513869780)]
PrivateSpaceEducation中
private View.OnClickListener onSetup() {
return v -> {
mMetricsFeatureProvider.action(
getContext(), SettingsEnums.ACTION_PRIVATE_SPACE_SETUP_START);
Log.i(TAG, "Starting private space setup");
//這里會進入PrivateSpaceCreationFragment進行私密空間的創(chuàng)建
NavHostFragment.findNavController(PrivateSpaceEducation.this)
.navigate(R.id.action_education_to_create);
};
}
private View.OnClickListener onCancel() {
return v -> {
Activity activity = getActivity();
if (activity != null) {
mMetricsFeatureProvider.action(
getContext(), SettingsEnums.ACTION_PRIVATE_SPACE_SETUP_CANCEL);
Log.i(TAG, "private space setup cancelled");
activity.finish();
}
};
}
PrivateSpaceCreationFragment
//首先會再次判斷是否支持私密空間
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
if (android.os.Flags.allowPrivateProfile()
&& android.multiuser.Flags.enablePrivateSpaceFeatures()) {
super.onCreate(savedInstanceState);
}
}
//延遲1s防止阻塞UI,再創(chuàng)建私密空間
@Override
public void onResume() {
super.onResume();
// Ensures screen visibility to user by introducing a 1-second delay before creating private
// space.
sHandler.removeCallbacks(mRunnable);
sHandler.postDelayed(mRunnable, PRIVATE_SPACE_CREATE_POST_DELAY_MS);
}
private Runnable mRunnable =
() -> {
createPrivateSpace();
};
private void createPrivateSpace() {
if (PrivateSpaceMaintainer.getInstance(getActivity()).createPrivateSpace()) {
Log.i(TAG, "Private Space created");
mMetricsFeatureProvider.action(
getContext(), SettingsEnums.ACTION_PRIVATE_SPACE_SETUP_SPACE_CREATED, true);
//這里判斷是否連接網(wǎng)絡,有網(wǎng)絡就需要登錄英融,無網(wǎng)絡就跳轉(zhuǎn)到私密空間密碼鎖定設置頁面
if (isConnectedToInternet()) {
registerReceiver();
sHandler.postDelayed(
mAccountLoginRunnable, PRIVATE_SPACE_ACCOUNT_LOGIN_POST_DELAY_MS);
} else {
NavHostFragment.findNavController(PrivateSpaceCreationFragment.this)
.navigate(R.id.action_set_lock_fragment);
}
}
}
createPrivateSpace源碼
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public final synchronized boolean createPrivateSpace() {
if (!Flags.allowPrivateProfile()
|| !android.multiuser.Flags.enablePrivateSpaceFeatures()) {
return false;
}
// Check if Private space already exists
if (doesPrivateSpaceExist()) {
return true;
}
// a name indicating that the profile was created from the PS Settings page
final String userName = "Private space";
if (mUserHandle == null) {
try {
mUserHandle = mUserManager.createProfile(
userName, USER_TYPE_PROFILE_PRIVATE, new ArraySet<>());
} catch (Exception e) {
Log.e(TAG, "Error creating private space", e);
return false;
}
if (mUserHandle == null) {
Log.e(TAG, "Failed to create private space");
return false;
}
//注冊私密空間Intent.ACTION_PROFILE_REMOVED 廣播
registerBroadcastReceiver();
//啟動私密空間
if (!startProfile()) {
// TODO(b/333884792): Add test to mock when startProfile fails.
Log.e(TAG, "profile not started, created profile is deleted");
deletePrivateSpace();
return false;
}
Log.i(TAG, "Private space created with id: " + mUserHandle.getIdentifier());
//設置顯示設置的入口盏檐,設定自動鎖定策略,設置隱藏通知
resetPrivateSpaceSettings();
//設置完畢
setUserSetupComplete();
//私密空間app跳過用戶提示
setSkipFirstUseHints();
//Disable settings app launcher icon驶悟,胡野,,Disable Shortcut picker
disableComponentsToHidePrivateSpaceSettings();
}
return true;
}
這里梳理一下流程:
1:createPrivateSpace跨進程創(chuàng)建虛擬私密空間痕鳍,得到UserHandle
2:注冊Intent.ACTION_PROFILE_REMOVED廣播硫豆,當收到廣播時移除私密空間配置信息
3:啟動后臺私密空間,啟動失敗則刪除私密空間
4:初始化私密空間配置
這里可以看到私密空間就是基于多用戶笼呆,userName為"Private space",userType為USER_TYPE_PROFILE_PRIVATE熊响,如下圖所示
[圖片上傳失敗...(image-a2def3-1732513869779)]
初始化私密空間應用
桌面應用的LauncherAppState注冊了私密空間的廣播監(jiān)聽,前面說到創(chuàng)建多用戶完成后會發(fā)送Intent.ACTION_PROFILE_ADDED/廣播,刪除私密空間會發(fā)送 Intent.ACTION_PROFILE_REMOVED廣播诗赌,桌面監(jiān)聽到隱私空間用戶創(chuàng)建和銷毀的廣播后會觸發(fā)forceReload()汗茄;
SafeCloseable userChangeListener = UserCache.INSTANCE.get(mContext)
.addUserEventListener(mModel::onUserEvent);
public void onUserEvent(UserHandle user, String action) {
...
else if (UserCache.ACTION_PROFILE_ADDED.equals(action)
|| UserCache.ACTION_PROFILE_REMOVED.equals(action)) {
forceReload();
}
...
}
接著調(diào)用到LauncherModel#startLoader方法,最終mLoaderTask的run方法
private boolean startLoader(@NonNull final Callbacks[] newCallbacks) {
...
stopLoader();
mLoaderTask = new LoaderTask(
mApp, mBgAllAppsList, mBgDataModel, mModelDelegate, launcherBinder);
// Always post the loader task, instead of running directly
// (even on same thread) so that we exit any nested synchronized blocks
MODEL_EXECUTOR.post(mLoaderTask);
}
接著調(diào)用loadAllApps()加載appInfo
public void run() {
...
allActivityList = loadAllApps();
...
}
遍歷私密空間和主空間境肾,mLauncherApps.getActivityList(null, user)獲取指定用戶的LauncherActivityInfo
private List<LauncherActivityInfo> loadAllApps() {
final List<UserHandle> profiles = mUserCache.getUserProfiles();
List<LauncherActivityInfo> allActivityList = new ArrayList<>();
// Clear the list of apps
mBgAllAppsList.clear();
List<IconRequestInfo<AppInfo>> iconRequestInfos = new ArrayList<>();
boolean isWorkProfileQuiet = false;
boolean isPrivateProfileQuiet = false;
for (UserHandle user : profiles) {
// Query for the set of apps
final List<LauncherActivityInfo> apps = mLauncherApps.getActivityList(null, user);
// Fail if we don't have any apps
// TODO: Fix this. Only fail for the current user.
if (apps == null || apps.isEmpty()) {
return allActivityList;
}
boolean quietMode = mUserManagerState.isUserQuiet(user);
if (Flags.enablePrivateSpace()) {
if (mUserCache.getUserInfo(user).isWork()) {
isWorkProfileQuiet = quietMode;
} else if (mUserCache.getUserInfo(user).isPrivate()) {
isPrivateProfileQuiet = quietMode;
}
}
...
mLauncherApps.getActivityList(null, user) 實現(xiàn)是通過ILauncherApps.getLauncherActivities() Binder調(diào)用
@SuppressLint("RequiresPermission")
@RequiresPermission(conditional = true,
anyOf = {ACCESS_HIDDEN_PROFILES_FULL, ACCESS_HIDDEN_PROFILES})
public List<LauncherActivityInfo> getActivityList(String packageName, UserHandle user) {
logErrorForInvalidProfileAccess(user);
try {
return convertToActivityList(mService.getLauncherActivities(mContext.getPackageName(),
packageName, user), user);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
接著看system_process實現(xiàn)端LauncherAppsService邏輯剔难,最終generateLauncherActivitiesForArchivedApp通過PackageManagerService獲取指定user的launcherActivities胆屿,并返回給Launcher
LauncherAppsService:
private ParceledListSlice<LauncherActivityInfoInternal> getActivitiesForArchivedApp(
@Nullable String packageName,
UserHandle user,
ParceledListSlice<LauncherActivityInfoInternal> launcherActivities) {
final List<LauncherActivityInfoInternal> archivedActivities =
generateLauncherActivitiesForArchivedApp(packageName, user);
if (archivedActivities.isEmpty()) {
return launcherActivities;
}
if (launcherActivities == null) {
return new ParceledListSlice(archivedActivities);
}
List<LauncherActivityInfoInternal> result = launcherActivities.getList();
result.addAll(archivedActivities);
return new ParceledListSlice(result);
}
最終Launcher得到私密空間和主空間的最新的appInfo,最后刷新Launcher UI
總結(jié)一下:
可以簡單理解桌面是個容器偶宫,可以加載不同用戶空間的apk launcher數(shù)據(jù)(如主用戶非迹,分身用戶,Android15新增的私密空間用戶)桌面點擊不同用戶apk纯趋,那么這個apk所處的運行環(huán)境就是自身用戶的環(huán)境憎兽,得到的私有路徑也是私密空間的環(huán)境,任務欄中不同應用切換不涉及多用戶的切換吵冒。
私密空間的應用安裝和卸載
點擊安裝會跳轉(zhuǎn)到Google Play才能安裝纯命,這里我們通過adb安裝 adb install -t --user USER_ID xx.apk。
本質(zhì)上跟應用安裝流程一樣這里不多累贅痹栖。主要看看桌面私密空間的刷新邏輯
[圖片上傳失敗...(image-94a445-1732513869779)]
LauncherApps向system_process進程的LauncherAppsService服務注冊了一個Binder接口亿汞,當應用卸載或者安裝時會收到LauncherAppsService的回調(diào)
public void registerCallback(Callback callback, Handler handler) {
synchronized (this) {
if (callback != null && findCallbackLocked(callback) < 0) {
boolean addedFirstCallback = mCallbacks.size() == 0;
addCallbackLocked(callback, handler);
if (addedFirstCallback) {
try {
mService.addOnAppsChangedListener(mContext.getPackageName(),
mAppsChangedListener);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
}
}
}
private final IOnAppsChangedListener.Stub mAppsChangedListener =
new IOnAppsChangedListener.Stub() {
@Override
public void onPackageRemoved(UserHandle user, String packageName)
throws RemoteException {
if (DEBUG) {
Log.d(TAG, "onPackageRemoved " + user.getIdentifier() + "," + packageName);
}
synchronized (LauncherApps.this) {
for (CallbackMessageHandler callback : mCallbacks) {
callback.postOnPackageRemoved(packageName, user);
}
}
}
@Override
public void onPackageAdded(UserHandle user, String packageName) throws RemoteException {
if (DEBUG) {
Log.d(TAG, "onPackageAdded " + user.getIdentifier() + "," + packageName);
}
synchronized (LauncherApps.this) {
for (CallbackMessageHandler callback : mCallbacks) {
callback.postOnPackageAdded(packageName, user);
}
}
}
最終調(diào)用PackageUpdatedTask#execute,安裝應用執(zhí)行appsList.addPackage揪阿,卸載應用執(zhí)行appsList.removePackage疗我,最后刷新Launcher UI
public void execute(@NonNull ModelTaskController taskController, @NonNull BgDataModel dataModel,
@NonNull AllAppsList appsList) {
final LauncherAppState app = taskController.getApp();
final Context context = app.getContext();
...
switch (mOp) {
case OP_ADD: {
for (int i = 0; i < N; i++) {
iconCache.updateIconsForPkg(packages[i], mUser);
if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
if (DEBUG) {
Log.d(TAG, "OP_ADD: PROMISE_APPS_IN_ALL_APPS enabled:"
+ " removing promise icon apps from package=" + packages[i]);
}
appsList.removePackage(packages[i], mUser);
}
activitiesLists.put(packages[i],
appsList.addPackage(context, packages[i], mUser));
}
flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
break;
}
case OP_UNAVAILABLE:
for (int i = 0; i < N; i++) {
if (DEBUG) {
Log.d(TAG, getOpString() + ": removing package=" + packages[i]);
}
appsList.removePackage(packages[i], mUser);
}
flagOp = FlagOp.NO_OP.addFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
break;
...
刪除私密空間
該界面為setting的PrivateSpaceDeleteFragment,點擊刪除需要驗證鎖屏密碼南捂,然后進入PrivateSpaceDeletionProgressFragment執(zhí)行真正的刪除工作
[圖片上傳失敗...(image-b4aa77-1732513869779)]
private Runnable mDeletePrivateSpace =
new Runnable() {
@Override
public void run() {
deletePrivateSpace();
getActivity().finish();
}
};
public void deletePrivateSpace() {
PrivateSpaceMaintainer.ErrorDeletingPrivateSpace error =
mPrivateSpaceMaintainer.deletePrivateSpace();
if (error == DELETE_PS_ERROR_NONE) {
showSuccessfulDeletionToast();
} else if (error == DELETE_PS_ERROR_INTERNAL) {
showDeletionInternalErrorToast();
}
}
public synchronized ErrorDeletingPrivateSpace deletePrivateSpace() {
if (!doesPrivateSpaceExist()) {
return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NO_PRIVATE_SPACE;
}
try {
Log.i(TAG, "Deleting Private space with id: " + mUserHandle.getIdentifier());
if (mUserManager.removeUser(mUserHandle)) {
Log.i(TAG, "Private space deleted");
mUserHandle = null;
return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_NONE;
} else {
Log.e(TAG, "Failed to delete private space");
}
} catch (Exception e) {
Log.e(TAG, "Error deleting private space", e);
}
return ErrorDeletingPrivateSpace.DELETE_PS_ERROR_INTERNAL;
}
可以看到是通過UserManager.removeUser() 刪除私密空間吴裤,桌面收到Intent.ACTION_PROFILE_REMOVED,流程同 Launcher****初始化私密空間應用