天天看點

實作跨使用者資源分享方案及優化

作者:閃念基因

1、問題背景

我們的雙開方案是基于Google的多使用者空間方案;Google多使用者方案的設計思想,是為了解決工作使用者空間(work profile)和私人使用者空間(personal profile)的資料安全的,是以兩個使用者空間的資料是并行的,互相之間是無法直接通路和分享的。

實作跨使用者資源分享方案及優化

在開發應用雙開的過程中,需要支援在相冊等app裡面分享圖檔到分身微信空間。

實作跨使用者資源分享方案及優化

2、問題描述

在相冊分享圖檔到微信分身,Toast提示“擷取資源失敗”,分享失敗。

這個就是Google原生邏輯中,兩個profile之間不讓直接分享圖檔導緻的;是以接下來要做的是打破兩個使用者空間的資料保護壁壘。

3、初步解決方案

為了盡快解決問題,前期我們發現原生的DocumentUI可以實作跨使用者分享圖檔;經過分析DocumentUI使用的是fileprovider,但是我們沒法直接用,還是沒有解決問題;這個過程不在此贅述了。

我們使用fileprovider,前兩步配置fileprovider和path,都可以在網上查詢到;但是僅僅這兩步無法解決問題;除此之外,還需配置URI權限和APP簽名配置。

前期的代碼分析和日志分析不在此贅述,直接給出解決方案及demo驗證,詳細配置過程如下:

  • 配置fileprovider:

在AndroidMenifest配置,在application的tag内添加provider:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.coolos.myapp.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths" />
</provider>           
  • 配置provider_paths

和res并列,添加xml檔案夾,并添加xml檔案provider_paths.xml:

<?xml version="1.0" encoding="utf-8"?>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
    <root-path name="root" path="." />
    <files-path name="image_files" path="images/" />
    <cache-path name="cache" path="." />
    <external-path name="external" path="." />
    <external-files-path name="external_files" path="." />
    <external-cache-path name="external_cache" path="." />
    <external-media-path name="name" path="." />
</paths>           
  • 配置URI權限:

\frameworks\base\services\core\java\com\android\server\uri\UriGrantsManagerService.java:

// Bail early if system is trying to hand out permissions directly; it
// must always grant permissions on behalf of someone explicit.
final int callingAppId = UserHandle.getAppId(callingUid);
if ((callingAppId == SYSTEM_UID) || (callingAppId == ROOT_UID)) {
    if ("com.android.settings.files".equals(grantUri.uri.getAuthority())
        || "com.android.settings.module_licenses".equals(grantUri.uri.getAuthority())
        || "com.journeyui.cloneit.fileprovider".equals(grantUri.uri.getAuthority())
        || "org.lineageos.setupwizard.files".equals(grantUri.uri.getAuthority())
        || "com.journeyui.filebrowser.provider".equals(grantUri.uri.getAuthority())
        || "com.coolos.myapp.fileprovider".equals(grantUri.uri.getAuthority())
        || "com.journeyui.gallery3d.fileprovider".equals(grantUri.uri.getAuthority())) {
        // Exempted authority for
        // 1. cropping user photos and sharing a generated license html
        //    file in Settings app
        // 2. sharing a generated license html file in TvSettings app
        // 3. Sharing module license files from Settings app
    } else {
        Slog.w(TAG, "For security reasons, the system cannot issue a Uri permission"
        + " grant to " + grantUri + "; use startActivityAsCaller() instead");
        return -1;
    }
}           
  • app系統簽名

在AndroidMenifest配置如下,另外還需要對apk進行platform簽名;

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.coolos.myapp"
    coreApp="true"
    android:sharedUserId="android.uid.system"
>           
  • Demo驗證

以 "external/DCIM/Camera/IMG_20210709_195700.jpg" 為例進行測試:

a.初始化URI:

File imagePath = new File(getExternalStoragePublicDirectory(DIRECTORY_DCIM).getPath(), "/Camera");
File newFile = new File(imagePath, "IMG_20210709_195700.jpg");
Uri contentUri = getUriForFile(getApplicationContext(), "com.coolos.myapp.fileprovider", newFile);


if (newFile.exists()) {
    Log.d(TAG, "START newFile2:" + newFile.getPath());
}           

b.初始化Intent并startActivity:

Intent intent = new Intent(Intent.ACTION_SEND);
intent.setDataAndType(contentUri,"image/*");
intent.putExtra(Intent.EXTRA_STREAM, contentUri);
//intent.setClipData(ClipData.newUri(getContentResolver(), "image", contentUri));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivity(Intent.createChooser(intent, "Select share target"));           

c.build.gradle配置:

implementation 'androidx.core:core:1.3.1'           
  • 方案驗證及缺點

雖然可以實作跨使用者分享圖檔,但是這個方案不完美;需要推動所有相關的app都要做相應的修改,比較麻煩。長期還需有持續優化。

  1. 方案優化

市面上比較主流的媒體分享方式是MediaProvider,是以大多數app都采用這一方式進行媒體檔案的分享,這就導緻FileProvider的分享方式并不完善,需要app同步進行适配。是以我們考慮能不能在不改變分享方式的前提下,在系統側進行适配。經過多種方案的對比和嘗試,我們最終決定在Chooser中進行Uri的轉換和适配,這樣就解決了大部分app媒體資源分享失敗的問題。

對Chooser比較熟悉的同學可能知道,最終Chooser會通過startActivityAsCaller的方式去啟動Activity,而我們就在調用這個接口之前對Intent中的Uri進行适當的轉換,以滿足雙開的需求。

代碼片段:

if(isDoubleOpen()){ //判斷雙開是否打開
    Uri shareUri = (Uri)mResolvedIntent.getParcelableExtra(Intent.EXTRA_STREAM); //從Intent中擷取Uri
    if(shareUri != null){
        String uriScheme = shareUri.getScheme();
        String uriAuthority = shareUri.getAuthority();
        if (getApplicationContext().getContentResolver().SCHEME_CONTENT.equals(uriScheme)
                && uriAuthority != null && uriAuthority.contains(MediaStore.AUTHORITY)) {
            Uri uriWithoutUserid = ContentProvider.getUriWithoutUserId(shareUri); //去掉Uri中的UserId
            if (userId == UserHandle.USER_SYSTEM) {
                mResolvedIntent.putExtra(Intent.EXTRA_STREAM, ContentProvider.maybeAddUserId(uriWithoutUserid, userId));
            } else { //針對非主使用者進行Uri的轉換
                String oldPath = getDataColumn(getApplicationContext(), uriWithoutUserid); //從主使用者Mediaprovider資料庫中查詢資源Path
                String newPath = pathConvert(oldPath); //将主空間的資源Path根據一定的規則轉換成其它空間的資源Path
                Uri uriWithoutid = ContentUris.removeId(uriWithoutUserid);
                Uri mediaCSpaceUri = ContentProvider.maybeAddUserId(uriWithoutid, userId);
                mResolvedIntent.putExtra(Intent.EXTRA_STREAM, getUriFromPath(getApplicationContext(), mediaCSpaceUri, newPath)); //從其他使用者中查詢資源Path對應的Uri并更新Intent
            }
        }
    }
}           

其中,由于用到了卷交叉挂載的技術,導緻多使用者之間的資源Path挂載路徑會有所不同,是以我們需要通過pathConvert()接口,并根據系統中挂載卷的名稱和規則進行Path的轉換,這樣才能在資料庫中query到正确的資源Path,并擷取到對應的資源Uri。

當然,除了根據資源的Path去映射多使用者Mediaprovider資料庫的對應關系,也可以根據資料庫中的其它字段來進行同樣的轉換操作,讀者可以自己去嘗試一下。

還有一點需要注意,為了讓其它使用者的app能去對應的Mediaprovider資料庫查詢資料,我們務必要在Uri中加上對應使用者的UserId,格式如content://UserId@media/external/images/media/328,這樣才能在ContentResolver中擷取到正确的Mediaprovider。

這個方案,經過驗證,不但可以解決上述問題,還不用推動app層進行一些修改,比老的方案完美。

作者:David Yang

來源-微信公衆号:酷派技術團隊

出處:https://mp.weixin.qq.com/s/kWYOaB3eZPLgVHwJYrCj1A

繼續閱讀