Android多用戶的一些坑 android:singleUser配置 android:exported被自動關閉 全局浮動框在子用戶不顯示 結尾吐槽

最近關於多用戶功能報了幾個bug,我覺得蠻有意思的這裏記錄一下。

起因是是測試報了打開了多用戶功能並且切到另外一個用戶之後,系統功能異常。調試發現我們的中間層服務啓動了兩個進程:

system         6074   2524 14649520 96244 SyS_epoll_wait      0 S me.linjw.demo.multiuser
u10_system     7991   2524 14582664 94148 SyS_epoll_wait      0 S me.linjw.demo.multiuser

從上面可以看出me.linjw.demo.multiuser這個應用分別在USER爲system和u10_system各起了一個進程。查找了下資料發現正常情況下一個應用進程的確是不能跨用戶訪問的,會在不同的用戶下啓動新的進程。

android:singleUser配置

由於歷史代碼原因,我們系統上的硬件操控接口的確不支持多個進程訪問,也不好修改。只能靠我們應用做規避。使用ps命令查看了下,發現像system_server這樣的系統服務在多用戶下也只有一個進程。谷歌應該會考慮到這種多用戶共用一個進程的場景,於是在開發者文檔中找到多用戶相關文檔:

如需將應用識別爲單例,請將 android:singleUser=”true” 添加至 Android 清單中的服務、接收器或提供程序。

由於現象是多個進程,我下意識認爲這個singleUser配置是針對應用的,所以在AndroidManifest.xml的application標籤中配置上去,但是發現沒有作用:

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:singleUser="true"
        android:theme="@style/Theme.MultiuserDemo">

由於文檔裏面沒有其他信息,從網上搜索找到的類似需要系統簽名、android:persistent需要爲true這樣的信息也確認過沒有效果。本來還懷疑是我們framework裏面做了什麼修改導把這塊改壞了。

於是去看這部分相關的framework源碼,主要邏輯在ActiveServices.retrieveServiceLocked裏面,子用戶裏啓動服務的時候會去通過isSingleton判斷是否使用主用戶的進程,如果是的話就使用主用戶的進程,不需要新啓動一個進程::

// frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
private ServiceLookupResult retrieveServiceLocked(Intent service,
        String instanceName, String resolvedType, String callingPackage,
        int callingPid, int callingUid, int userId,
        boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
        boolean allowInstant) {
    ...
    // 這裏在不同userId下查詢出來的rInfo就是不一樣的
    ResolveInfo rInfo = mAm.getPackageManagerInternalLocked().resolveService(service,
            resolvedType, flags, userId, callingUid);
    ServiceInfo sInfo = rInfo != null ? rInfo.serviceInfo : null;
    ...
    // userId不爲0代表子用戶下運行
    if (userId > 0) {
        if (mAm.isSingleton(sInfo.processName, sInfo.applicationInfo,
                sInfo.name, sInfo.flags)
                && mAm.isValidSingletonCall(callingUid, sInfo.applicationInfo.uid)) {
            // 如果組件isSingleton判斷爲true
            // 則將userId改成0,並使用clearCallingIdentity清除調用進程的用戶信息,重新查詢
            // 則查出來的rInfoForUserId0爲主用戶的緩存  
            userId = 0;
            smap = getServiceMapLocked(0);
            // Bypass INTERACT_ACROSS_USERS permission check
            final long token = Binder.clearCallingIdentity();
            try {
                ResolveInfo rInfoForUserId0 = mAm.getPackageManagerInternalLocked().resolveService(service,
                                resolvedType, flags, userId, callingUid);
                if (rInfoForUserId0 == null) {
                    Slog.w(TAG_SERVICE,
                            "Unable to resolve service " + service + " U=" + userId
                                    + ": not found");
                    return null;
                }
                // 然後用這個rInfoForUserId0.serviceInfo去替換之前查出來的rInfo.serviceInfo,保證多用戶下都用主用戶下的同一個進程
                sInfo = rInfoForUserId0.serviceInfo;
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }
        sInfo = new ServiceInfo(sInfo);
        sInfo.applicationInfo = mAm.getAppInfoForUser(sInfo.applicationInfo, userId);
    }
    ...
}

判斷是否在多用戶下只啓動單個進程主要靠isSingleton這個方法:

// frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
boolean isSingleton(String componentProcessName, ApplicationInfo aInfo,
        String className, int flags) {
    boolean result = false;
    // For apps that don't have pre-defined UIDs, check for permission
    if (UserHandle.getAppId(aInfo.uid) >= FIRST_APPLICATION_UID) {
        if ((flags & ServiceInfo.FLAG_SINGLE_USER) != 0) {
            if (ActivityManager.checkUidPermission(
                    INTERACT_ACROSS_USERS,
                    aInfo.uid) != PackageManager.PERMISSION_GRANTED) {
                ComponentName comp = new ComponentName(aInfo.packageName, className);
                String msg = "Permission Denial: Component " + comp.flattenToShortString()
                        + " requests FLAG_SINGLE_USER, but app does not hold "
                        + INTERACT_ACROSS_USERS;
                Slog.w(TAG, msg);
                throw new SecurityException(msg);
            }
            // Permission passed
            result = true;
        }
    } else if ("system".equals(componentProcessName)) {
        result = true;
    } else if ((flags & ServiceInfo.FLAG_SINGLE_USER) != 0) {
        // Phone app and persistent apps are allowed to export singleuser providers.
        result = UserHandle.isSameApp(aInfo.uid, PHONE_UID)
                || (aInfo.flags & ApplicationInfo.FLAG_PERSISTENT) != 0;
    }
    if (DEBUG_MU) Slog.v(TAG_MU,
            "isSingleton(" + componentProcessName + ", " + aInfo + ", " + className + ", 0x"
            + Integer.toHexString(flags) + ") = " + result);
    return result;
}

打開DEBUG_MU之後查看打印,發現singleUser是按組件來配置的:

08-03 13:45:20.092  3289  4023 V ActivityManager_MU: isSingleton(me.linjw.demo.multiuser, ApplicationInfo{417a0ea me.linjw.demo.multiuser}, me.linjw.demo.multiuser.TestService, 0x0) = false

所以應該在service裏面配置:

<service
    android:name=".TestService"
    android:exported="true"
    android:singleUser="true">

實際上如果我一開始看到是英文文檔,應該就不會出現這樣的誤解了:

To identify an app as a singleton, add android:singleUser=”true” to your service, receiver, or provider in the Android manifest.

android:exported被自動關閉

修改完成自檢通過,開開心心上傳代碼原本以爲問題已經解決。沒想到一天之後另外一個客戶的軟件報了連接不上我們的Service的問題:

08-03 12:13:22.108  3185  3557 W ActivityManager: Permission Denial: Accessing service me.linjw.demo.multiuser/.TestService from pid=5994, uid=10055 that is not exported from uid 1000
08-03 12:13:22.112  5994  5994 E AndroidRuntime: Caused by: java.lang.SecurityException: Not allowed to bind to service Intent { act=me.linjw.multiuser.service pkg=me.linjw.demo.multiuser }

從日誌上來看TestService沒有export,但是從AndroidManifest.xml上看android:exported的確設置成true了。而且嘗試把android:singleUser改成fasle又能連上。這就意味着android:singleUser陪着會影響到android:exported。

但這裏又有個問題,當初我修改完android:singleUser="true"之後是有自檢通過的,如果exported爲false,那自檢爲什麼能通過?

最終排查發現我們的應用設置了sharedUserId聲明爲系統進程:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="me.linjw.demo.multiuser"
    android:sharedUserId="android.uid.system">

當初自檢的時候的那個應用的sharedUserId也是android.uid.system,所以相當於他們是同一個應用,可以相互訪問exported爲false的組件。

原因排查清楚了,那要怎麼解決呢?還是隻能從framework源碼開始翻起,先去報錯的地方看起,找找爲什麼exported會被自動改成false:

// frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
private ServiceLookupResult retrieveServiceLocked(Intent service,
          String instanceName, String resolvedType, String callingPackage,
          int callingPid, int callingUid, int userId,
          boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal,
          boolean allowInstant) {
      ServiceRecord r = null;
      ...
      if (mAm.checkComponentPermission(r.permission,
              callingPid, callingUid, r.appInfo.uid, r.exported) != PERMISSION_GRANTED) {
          if (!r.exported) {
              Slog.w(TAG, "Permission Denial: Accessing service " + r.shortInstanceName
                      + " from pid=" + callingPid
                      + ", uid=" + callingUid
                      + " that is not exported from uid " + r.appInfo.uid);
              return new ServiceLookupResult(null, "not exported from uid "
                      + r.appInfo.uid);
          }
          Slog.w(TAG, "Permission Denial: Accessing service " + r.shortInstanceName
                  + " from pid=" + callingPid
                  + ", uid=" + callingUid
                  + " requires " + r.permission);
          return new ServiceLookupResult(null, r.permission);
      }
      ...
}

從這裏看r的exported爲false導致了這個異常,我們需要在retrieveServiceLocked裏面一路追蹤r的exported是怎麼被singleUser影響的,由於這部分代碼比較曲折我也找了很久才找到關鍵代碼。

PackageManagerService掃描應用信息的時候,會判斷SCAN_AS_PRIVILEGED這個flag,如果沒有設置就會執行markNotActivitiesAsNotExportedIfSingleUser

// frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
if ((scanFlags & SCAN_AS_PRIVILEGED) == 0) {
    parsedPackage
            .markNotActivitiesAsNotExportedIfSingleUser();
}

markNotActivitiesAsNotExportedIfSingleUser顧名思義,就會在配置了SingleUser的時候去修改exported,實際上它裏面除了Activity不修改,其他的三個組件都修改了:

public PackageImpl markNotActivitiesAsNotExportedIfSingleUser() {
    // ignore export request for single user receivers
    int receiversSize = receivers.size();
    for (int index = 0; index < receiversSize; index++) {
        ParsedActivity receiver = receivers.get(index);
        if ((receiver.getFlags() & ActivityInfo.FLAG_SINGLE_USER) != 0) {
            receiver.setExported(false);
        }
    }

    // ignore export request for single user services
    int servicesSize = services.size();
    for (int index = 0; index < servicesSize; index++) {
        ParsedService service = services.get(index);
        if ((service.getFlags() & ActivityInfo.FLAG_SINGLE_USER) != 0) {
            service.setExported(false);
        }
    }

    // ignore export request for single user providers
    int providersSize = providers.size();
    for (int index = 0; index < providersSize; index++) {
        ParsedProvider provider = providers.get(index);
        if ((provider.getFlags() & ActivityInfo.FLAG_SINGLE_USER) != 0) {
            provider.setExported(false);
        }
    }

    return this;
}

那問題就在於我們的應用沒有攜帶SCAN_AS_PRIVILEGED,所以在singleUser爲true的時候exported會被改成false。那我們要怎麼帶上這個flag呢?搜索了下資料發現這個flag代表着特權應用,只要預裝到下面目錄的就能成爲特權應用

/system/framework
/system/priv-app
/vendor/priv-app
/odm/priv-app
/product/priv-app
/system_ext/priv-app

最終將預裝路徑從/syste/app改到/syste/priv-app解決問題。

全局浮動框在子用戶不顯示

沒想到過了兩天又報了另外一個問題,我們通過WindowManager.addView添加的全局浮動框在子用戶不顯示,在主用戶是好的。又踩了一個隱藏的坑。

既然不顯示,那麼首先考慮是不是addView失敗了,於是用dumpsys window看看有沒有add成功:

console:/ # dumpsys window | grep me.linjw.demo.multiuser
    mPackageName=me.linjw.demo.multiuser
  Window #0 Window{a03fb0 u0 me.linjw.demo.multiuser}:
    mOwnerUid=1000 showForAllUsers=false package=me.linjw.demo.multiuser appop=SYSTEM_ALERT_WINDOW

從打印上來看是add成功的,但是裏面有個showForAllUsers引起了我的注意,大概猜測是addView的時候有個showForAllUsers的flag沒有設置,於是在源碼裏面搜索還真找到了:

// android/view/WindowManager.java
@SystemApi
@RequiresPermission(permission.INTERNAL_SYSTEM_WINDOW)
public static final int SYSTEM_FLAG_SHOW_FOR_ALL_USERS = 0x00000010;

但是它的值和FLAG_NOT_TOUCHABLE重複了:

public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;

於是從搜索了下它,發現需要設置到WindowManager.LayoutParams.privateFlags而不是WindowManager.LayoutParams.flags

可惜的是無論是privateFlags還是SYSTEM_FLAG_SHOW_FOR_ALL_USERS都是系統api,所以只能用反射去設置:

private int SYSTEM_FLAG_SHOW_FOR_ALL_USERS = 0x00000010;

// 多用戶下需要設置這個flag才能在其他用戶顯示
Field privateFlags = null;
try {
    privateFlags = WindowManager.LayoutParams.class.getDeclaredField("privateFlags");
    privateFlags.set(wmParams, SYSTEM_FLAG_SHOW_FOR_ALL_USERS);
} catch (Exception e) {
    Log.e("testtest", "err", e);
}

設置之後的確在子用戶下也顯示成功了,用dumpsys window查看showForAllUsers也變成了true:

console:/ # dumpsys window | grep me.linjw.demo.multiuser
    mPackageName=me.linjw.demo.multiuser
  Window #0 Window{3655208 u0 me.linjw.demo.multiuser}:
    mOwnerUid=1000 showForAllUsers=true package=me.linjw.demo.multiuser appop=SYSTEM_ALERT_WINDOW

結尾吐槽

這系列問題前前後後差不多一個月才弄完,framework部分源碼的源碼看得人都暈了,也不知道還會不會有其他意料之外的坑。當個安卓應用開發太難了...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章