工做空間鈴聲原理

工做空間原理

一.工做空間原理:

1.原理:

工做空間理論上是一種特殊的子用戶Managed Profile(配置子用戶),此子用戶不能單獨存在,必須依附於普通用戶(Primary profile),也不能夠切換到此子用戶。可是,擁有本身的數據目錄。因此,能夠運行本身的應用,配置本身的鈴聲等功能。java

2.判斷配置子用戶的方式:

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原生代碼。函數

1.工做空間鈴聲的讀取和設置:

工做空間鈴聲經過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「。

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系統多用戶

相關文章
相關標籤/搜索