工做空間理論上是一種特殊的子用戶Managed Profile(配置子用戶),此子用戶不能單獨存在,必須依附於普通用戶(Primary profile),也不能夠切換到此子用戶。可是,擁有本身的數據目錄。因此,能夠運行本身的應用,配置本身的鈴聲等功能。java
UserManager中提供方法isManagedProfile()來判斷是不是Managed Profile:android
@SystemApi
@RequiresPermission(android.Manifest.permission.MANAGE_USERS)
public boolean isManagedProfile() {
// No need for synchronization. Once it becomes non-null, it'll be non-null forever.
// Worst case we might end up calling the AIDL method multiple times but that's fine.
if (mIsManagedProfileCached != null) {
return mIsManagedProfileCached;
}
try {
mIsManagedProfileCached = mService.isManagedProfile(UserHandle.myUserId());
return mIsManagedProfileCached;
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
}
複製代碼
此方法最關鍵的代碼是調用了UserManagerService的isManagedProfile()方法:數組
@Override
public boolean isManagedProfile(int userId) {
checkManageOrInteractPermIfCallerInOtherProfileGroup(userId, "isManagedProfile");
synchronized (mUsersLock) {
UserInfo userInfo = getUserInfoLU(userId);
return userInfo != null && userInfo.isManagedProfile();
}
}
複製代碼
一樣的,此方法最終會調用到UserInfo的isManagedProfile()方法:緩存
@UnsupportedAppUsage
public boolean isManagedProfile() {
return (flags & FLAG_MANAGED_PROFILE) == FLAG_MANAGED_PROFILE;
}
複製代碼
UserInfo中的isManagedProfile()方法就是最後判斷的地方。經過flags字段來判斷。若是flags包含了FLAG_MANAGED_PROFILE字段,則表示當前用戶是配置子用戶。bash
那麼,這個flags字段從哪裏來的呢?
這個flags字段的賦值是在UserManagerService中,當系統啓動這個服務時,在構造函數中會調用readUserListLP()方法去讀取系統中的多用戶列表,這個列表就是/data/system/users/userlist.xml。再經過這個userlist.xml中的user列表,讀取相同目錄下的用戶詳細列表,如:0.xml。這個xml中的「flags」字段,就是讀取出來的UserInfo的flags值。app
另外,這個字段究竟是哪裏來的呢,那就是在建立的時候,當咱們調用createUser(String name, int flags)或createProfileForUser(String name, int flags, int userId,String[] disallowedPackages)時,所傳入的。其中,createProfileForUser就是建立一個配置子用戶。而每個普通用戶的配置子用戶數量是有限的,這個數量的最大值由UserManagerService的MAX_MANAGED_PROFILES控制,目前這個值爲1。ide
此分析基於Google Android Q的Settings原生代碼。函數
工做空間鈴聲經過DefaultRingtonePreference類來展現和設置。此類中主要使用以下兩個方法來讀取和存儲工做空間鈴聲:ui
@Override
protected void onSaveRingtone(Uri ringtoneUri) {
RingtoneManager.setActualDefaultRingtoneUri(mUserContext, getRingtoneType(), ringtoneUri);
}
複製代碼
@Override
protected Uri onRestoreRingtone() {
return RingtoneManager.getActualDefaultRingtoneUri(mUserContext, getRingtoneType());
}
複製代碼
這裏的mUserContext就是配置子用戶(Managed Profile)的Context。具體獲取後面分析。接下來看看RingtoneManager中存儲鈴聲(由於存儲鈴聲和讀取鈴聲的操做相似,只選取其中一種分析)的過程:spa
public static void setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri) {
String setting = getSettingForType(type);
if (setting == null) return;
final ContentResolver resolver = context.getContentResolver();
if (Settings.Secure.getIntForUser(resolver, Settings.Secure.SYNC_PARENT_SOUNDS, 0,
context.getUserId()) == 1) {
// Parent sound override is enabled. Disable it using the audio service.
disableSyncFromParent(context);
}
if(!isInternalRingtoneUri(ringtoneUri)) {
ringtoneUri = ContentProvider.maybeAddUserId(ringtoneUri, context.getUserId());
}
Settings.System.putStringForUser(resolver, setting,
ringtoneUri != null ? ringtoneUri.toString() : null, context.getUserId());
// Stream selected ringtone into cache so it's available for playback
// when CE storage is still locked
if (ringtoneUri != null) {
final Uri cacheUri = getCacheForType(type, context.getUserId());
try (InputStream in = openRingtone(context, ringtoneUri);
OutputStream out = resolver.openOutputStream(cacheUri)) {
FileUtils.copy(in, out);
} catch (IOException e) {
Log.w(TAG, "Failed to cache ringtone: " + e);
}
}
}
複製代碼
RingtoneManager中主要就是根據Context獲取對應用戶下的ContentProvider,並存入鈴聲的uri字段。這裏的Context就是配置子用戶(Managed Profile)的Context。由於每一個用戶都有本身的數據目錄,因此ContentProvider的目錄也不相同。根據代碼可知,鈴聲的值主要是存儲在/data/system/users/用戶Id/settings_system.xml。
此處的mUserContext經過Utils中createPackageContextAsUser()的方法建立:
public static Context createPackageContextAsUser(Context context, int userId) {
try {
return context.createPackageContextAsUser(
context.getPackageName(), 0 /* flags */, UserHandle.of(userId));
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Failed to create user context", e);
}
return null;
}
複製代碼
這裏傳入的userId是Managed Profile的userId,因此這裏的工做空間鈴聲,也是存放在配置子用戶(Managed Profile)的數據目錄下。可是,既然每一個普通用戶均可以建立本身的配置子用戶,那麼系統又是怎麼區分這些配置子用戶的呢。
這裏的userId的獲取,就是在WorkSoundPreferenceController類中,具體的步驟以下:
首先,在WorkSoundPreferenceController的構造方法中,經過當前Context獲取UserManager服務(這個UserManager在整個系統中都有且只有一個):
private final UserManager mUserManager;
@VisibleForTesting
WorkSoundPreferenceController(Context context, SoundSettings parent, Lifecycle lifecycle,
AudioHelper helper) {
super(context);
mUserManager = UserManager.get(context);
}
複製代碼
而後經過UserManager獲取到當前用戶的配置子用戶的UserId,這裏最終會調用到Utils的getManagedProfileId方法:
public static int getManagedProfileId(UserManager um, int parentUserId) {
int[] profileIds = um.getProfileIdsWithDisabled(parentUserId);
for (int profileId : profileIds) {
if (profileId != parentUserId) {
return profileId;
}
}
return UserHandle.USER_NULL;
}
複製代碼
這裏面會從獲得的用戶id數組中,找到當前用戶的配置子用戶。可是,這裏獲得的id數組,是包含當前用戶的id的,因此須要排除這種狀況。
接下來就進入到UserManager中,通過層層調用,最後調用了UserManagerService的getProfileIds()方法:
@Override
public int[] getProfileIds(int userId, boolean enabledOnly) {
if (userId != UserHandle.getCallingUserId()) {
checkManageOrCreateUsersPermission("getting profiles related to user " + userId);
}
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mUsersLock) {
return getProfileIdsLU(userId, enabledOnly).toArray();
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
複製代碼
此方法中檢查當前用戶是否有管理/建立用戶的權限。主要的執行是調用了getProfilesLU()方法:
@GuardedBy("mUsersLock")
private List<UserInfo> getProfilesLU(int userId, boolean enabledOnly, boolean fullInfo) {
IntArray profileIds = getProfileIdsLU(userId, enabledOnly);
ArrayList<UserInfo> users = new ArrayList<>(profileIds.size());
for (int i = 0; i < profileIds.size(); i++) {
int profileId = profileIds.get(i);
UserInfo userInfo = mUsers.get(profileId).info;
// If full info is not required - clear PII data to prevent 3P apps from reading it
if (!fullInfo) {
userInfo = new UserInfo(userInfo);
userInfo.name = null;
userInfo.iconPath = null;
} else {
userInfo = userWithName(userInfo);
}
users.add(userInfo);
}
return users;
}
複製代碼
這一步主要是調用getProfileIds()方法獲得當前用戶的配置子用戶的Id列表,再到mUsers中獲取UserInfo,這裏的mUsers就是在服務啓動時,從userlist.xml中讀取的用戶列表。主要看一下getProfileIds()方法:
@GuardedBy("mUsersLock")
private IntArray getProfileIdsLU(int userId, boolean enabledOnly) {
UserInfo user = getUserInfoLU(userId);
IntArray result = new IntArray(mUsers.size());
if (user == null) {
// Probably a dying user
//這裏籠統的表示當前用戶不存在,那麼對應的配置子用戶也不應存在
return result;
}
final int userSize = mUsers.size();
for (int i = 0; i < userSize; i++) {
UserInfo profile = mUsers.valueAt(i).info;
if (!isProfileOf(user, profile)) { //此處爲關鍵代碼,判斷配置子用戶是不是當前用戶的配置子用戶。
continue;
}
if (enabledOnly && !profile.isEnabled()) { //是否只是Enable的用戶才能返回,此處enabledOnly的值在調用時傳入。此處爲false。
continue;
}
if (mRemovingUserIds.get(profile.id)) { //mRemovingUserIds中的值表示用戶已被銷燬。可是因爲VFS的緩存機制,尚未被徹底清除。此時對應的配置子用戶也不應存在。
continue;
}
if (profile.partial) { //partial的值表示用戶未建立完成,可能狀況是正在建立,或者建立過程當中發生異常被打斷。這個值在建立用戶時被設置爲true,建立結束時設置爲false。
continue;
}
result.add(profile.id);
}
return result;
}
複製代碼
首先,經過getUserInfoLU(userId)方法獲取當前用戶的UserInfo,而後再遍歷mUsers列表,找出當前用戶的配置子用戶。這裏主要分析一下isProfileOf(user, profile)方法,其餘判斷條件見註釋。
@GuardedBy("mUsersLock")
private UserInfo getUserInfoLU(int userId) {
final UserData userData = mUsers.get(userId);
// If it is partial and not in the process of being removed, return as unknown user.
if (userData != null && userData.info.partial && !mRemovingUserIds.get(userId)) { //partial表示用戶未建立完成。mRemovingUserIds表示用戶已被刪除。
Slog.w(LOG_TAG, "getUserInfo: unknown user #" + userId);
return null;
}
return userData != null ? userData.info : null;
}
複製代碼
private static boolean isProfileOf(UserInfo user, UserInfo profile) {
return user.id == profile.id ||
(user.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID
&& user.profileGroupId == profile.profileGroupId);
}
複製代碼
這裏主要有兩個判斷標準:
1.UserInfo.id,這一步判斷其實會返回當前用戶,由於咱們的for循環並無排除當前用戶,因此當前用戶老是會被加入列表。
2.UserInfo.profileGroupId:這一步是判斷的關鍵,用戶與其配置子用戶的profileGroupId必定是相同的。這個profileGroupId的值,也是存在/data/system/users/用戶Id.xml文件中,字段名稱就是」profileGroupId「。
當用戶沒有建立配置子用戶時,前面獲取的mManagedProfileId是有可能返回NULL的。此時,工做空間鈴聲相關的UI並不會顯示。 配置子用戶添加/移除能夠監聽如下廣播:
Intent.ACTION_MANAGED_PROFILE_ADDED:配置子用戶添加
Intent.ACTION_MANAGED_PROFILE_REMOVED:配置子用戶移除
Linux的原理是一個文件系統。同理,基於Linux內核的Andriod系統,也是一個文件系統。 因此,多用戶的原理就是爲不一樣的用戶,建立不一樣的數據目錄。不一樣用戶的數據目錄相互獨立。而系統運行時,根據不一樣的userId,加載不一樣數據目錄下的文件數據,達到多用戶的效果。
Android系統爲每個用戶分配一個惟一的整型字段做爲userId,userId是系統識別子用戶最重要的依據。 對於主用戶(正常下的默認用戶)來講,userId爲0。
其餘子用戶的userId將從10開始依次遞增。
在Android系統中,應用的安裝是惟一的,每一個系統中只會安裝惟一一個相同的應用。可是,同一個應用能夠在不一樣用戶中被啓動,單獨運行於子用戶的進程中。即便是同一個應用,在不一樣用戶下,能夠有一些差別。這取決於咱們爲不一樣的用戶,準備了不一樣的數據目錄。
多用戶主要的數據目錄包括:
外部存儲目錄:/storage/emulated/用戶Id/ 此目錄用於存儲用戶使用過程當中保存的一些數據。
應用數據目錄:/data/user/用戶Id/ 此目錄用於存儲用戶的應用數據。(/data/data/目錄依然存在,且被連接到/data/user/0)。
系統數據目錄:/data/system/users/ 此目錄用於存儲多用戶的相關信息。以下圖所示:
:/data/system/users # ls
0 0.xml 10 10.xml 11 11.xml userlist.xml
複製代碼
其中,userlist.xml中記錄全部的多用戶信息。用戶Id.xml記錄每個多用戶的的信息。用戶Id/目錄下,存儲了不一樣用戶的配置,如settings_global.xml,settings_secure.xml等配置。不一樣用戶擁有的功能不一樣,因此文件也不盡相同。
:/data/system/users # cat userlist.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<user id="0" serialNumber="0" flags="19" created="0" lastLoggedIn="1546428987814" lastLoggedInFingerprint="***" icon="/data/system/users/0/p hoto.png" profileGroupId="0" profileBadge="0">
<restrictions />
</user>
複製代碼
:/data/system/users # cat 0.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<user id="0" serialNumber="0" flags="19" created="0" lastLoggedIn="1546428987814" lastLoggedInFingerprint="***" icon="/data/system/users/0/p hoto.png" profileGroupId="0" profileBadge="0">
<restrictions />
</user>
複製代碼
:/data/system/users/0 # ls
app_idle_stats.xml package-restrictions.xml registered_services runtime-permissions.xml settings_global.xml settings_ssaid.xml wallpaper_info.xml
appwidgets.xml photo.png roles.xml settings_config.xml settings_secure.xml settings_system.xml
複製代碼
3.1.查看當前系統中的多用戶以及狀態:pm list users
:/ # pm list users
Users:
UserInfo{0:Owner:13} running
UserInfo{10:qygxsq:10}
UserInfo{11:工做資料:30} running
複製代碼
3.2.查看當前系統中的多用戶詳細信息:dumpsys user
:/ # dumpsys user
Users:
UserInfo{0:null:13} serialNo=0
State: RUNNING_UNLOCKED
Created: <unknown>
Last logged in: +2h30m53s134ms ago
Last logged in fingerprint: OPPO/CPH1979/oppo6779:10/QP1A.190711.020/1575528783:user/release-keys
Start time: +2h31m2s549ms ago
Unlock time: +2h30m53s619ms ago
Has profile owner: false
Restrictions:
none
Device policy global restrictions:
null
Device policy local restrictions:
null
Effective restrictions:
none
UserInfo{10:qygxsq:10} serialNo=10
State: -1
Created: +1d0h5m15s625ms ago
Last logged in: +2h18m53s506ms ago
Last logged in fingerprint: OPPO/CPH1979/oppo6779:10/QP1A.190711.020/1575528783:user/release-keys
Start time: <unknown>
Unlock time: <unknown>
Has profile owner: false
Restrictions:
no_record_audio
no_sms
no_outgoing_calls
Device policy global restrictions:
null
Device policy local restrictions:
null
Effective restrictions:
no_record_audio
no_sms
no_outgoing_calls
UserInfo{11:工做資料:30} serialNo=11
State: RUNNING_UNLOCKED
Created: +23h44m24s339ms ago
Last logged in: +2h30m46s110ms ago
Last logged in fingerprint: OPPO/CPH1979/oppo6779:10/QP1A.190711.020/1575528783:user/release-keys
Start time: +2h30m49s181ms ago
Unlock time: +2h30m46s183ms ago
Has profile owner: true
Restrictions:
no_wallpaper
Device policy global restrictions:
null
Device policy local restrictions:
no_bluetooth_sharing
no_install_unknown_sources
Effective restrictions:
no_bluetooth_sharing
no_wallpaper
no_install_unknown_sources
Device owner id:-10000
Guest restrictions:
no_sms
no_install_unknown_sources
no_config_wifi
no_outgoing_calls
Device managed: false
Started users state: {0=3, 11=3}
Max users: 4
Supports switchable users: true
All guests ephemeral: false
複製代碼
3.3.查看當前系統中的多用戶詳細信息:cat /data/system/users/userlist.xml
:/ # cat /data/system/users/userlist.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<users nextSerialNumber="10" version="7">
<guestRestrictions>
<restrictions no_sms="true" no_install_unknown_sources="true" no_config_wifi="true" no_outgoing_calls="true" />
</guestRestrictions>
<deviceOwnerUserId id="-10000" />
<user id="0" />
</users>
複製代碼