工作空间原理
一.工作空间原理:
1.原理:
工作空间理论上是一种特殊的子用户Managed Profile(配置子用户),此子用户不能单独存在,必须依附于普通用户(Primary profile),也不可以切换到此子用户。但是,拥有自己的数据目录。所以,可以运行自己的应用,配置自己的铃声等功能。
2.判断配置子用户的方式:
UserManager中提供方法isManagedProfile()来判断是否是Managed Profile:
@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字段,则表示当前用户是配置子用户。
那么,这个flags字段从哪里来的呢?
这个flags字段的赋值是在UserManagerService中,当系统启动这个服务时,在构造函数中会调用readUserListLP()方法去读取系统中的多用户列表,这个列表就是/data/system/users/userlist.xml。再通过这个userlist.xml中的user列表,读取相同目录下的用户详细列表,如:0.xml。这个xml中的“flags”字段,就是读取出来的UserInfo的flags值。
另外,这个字段到底是哪里来的呢,那就是在创建的时候,当我们调用createUser(String name, int flags)或createProfileForUser(String name, int flags, int userId,String[] disallowedPackages)时,所传入的。其中,createProfileForUser就是创建一个配置子用户。而每一个普通用户的配置子用户数量是有限的,这个数量的最大值由UserManagerService的MAX_MANAGED_PROFILES控制,目前这个值为1。
二.工作空间铃声的实现:
此分析基于Google Android Q的Settings原生代码。
1.工作空间铃声的读取和设置:
工作空间铃声通过DefaultRingtonePreference类来展示和设置。此类中主要使用如下两个方法来读取和存储工作空间铃声:
@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中存储铃声(因为存储铃声和读取铃声的操作类似,只选取其中一种分析)的过程:
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“。
2.监听配置子用户的添加移除广播:
当用户没有创建配置子用户时,前面获取的mManagedProfileId是有可能返回NULL的。此时,工作空间铃声相关的UI并不会显示。
配置子用户添加/移除可以监听以下广播:
Intent.ACTION_MANAGED_PROFILE_ADDED:配置子用户添加
Intent.ACTION_MANAGED_PROFILE_REMOVED:配置子用户移除
三.Android多用户相关概念:
1.多用户原理:
Linux的原理是一个文件系统。同理,基于Linux内核的Andriod系统,也是一个文件系统。 所以,多用户的原理就是为不同的用户,创建不同的数据目录。不同用户的数据目录相互独立。而系统运行时,根据不同的userId,加载不同数据目录下的文件数据,达到多用户的效果。
2.相关概念:
2.1.userId:
Android系统为每一个用户分配一个唯一的整型字段作为userId,userId是系统识别子用户最重要的依据。
对于主用户(正常下的默认用户)来说,userId为0。
其他子用户的userId将从10开始依次递增。
2.2.数据目录:
在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.调试方法:
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>