工作空间铃声原理

3,164 阅读10分钟

工作空间原理

一.工作空间原理:

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>

4.参考文档:

深入理解Android系统多用户