android Q sdcard权限详解

默认情况下,如果应用以 Android Q 为目标平台,则在访问外部存储设备中的文件时会进入过滤视图。应用可以使用 Context.getExternalFilesDir() 将专用于自己的文件存储在特定于自己的目录中。

1. 临时停用分区存储行为:

  • 以 Android 9(API 级别 28)或更低版本为目标平台。
  • 如果您以 Android Q 为目标平台,请在应用的清单文件中将 requestLegacyExternalStorage 的值设为 true。
<manifest ... >
    <!-- This attribute is "false" by default on apps targeting Android Q. -->
    <application android:requestLegacyExternalStorage="true" ... >
    ...
    </application>
</manifest>

2. 如何实现隔离存储:

2.1 ApplicationInfo新增PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE标记

PackageParser.java:

if (sa.getBoolean(
        R.styleable.AndroidManifestApplication_requestLegacyExternalStorage,
        owner.applicationInfo.targetSdkVersion < Build.VERSION_CODES.Q)) {
    ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE;
}

ApplicationInfo.java:

public boolean hasRequestedLegacyExternalStorage() {
    return (privateFlags & PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE) != 0;
}

2.2 grantRuntimePermission()重新挂载视图
apk启动时默认挂载runtime/default视图,grantRuntimePermission()时如果是READ_EXTERNAL_STORAGE或WRITE_EXTERNAL_STORAGE,则会获取挂载模式重新挂载对应视图。

PermissionManagerService.java:

    private void grantRuntimePermission(String permName, String packageName, boolean overridePolicy,
            int callingUid, final int userId, PermissionCallback callback) {
        ......
        if (READ_EXTERNAL_STORAGE.equals(permName)
                || WRITE_EXTERNAL_STORAGE.equals(permName)) {
            final long token = Binder.clearCallingIdentity();
            try {
                if (mUserManagerInt.isUserInitialized(userId)) {
                    StorageManagerInternal storageManagerInternal = LocalServices.getService(
                            StorageManagerInternal.class);
                    storageManagerInternal.onExternalStoragePolicyChanged(uid, packageName);
                }
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }

    }

获取挂载模式这块android10有修改,没有设置Legacy标志的话,总是获取default挂载模式,没有读写权限。
android 10会设置属性[persist.sys.isolated_storage]: [true],因此走到if(ENABLE_ISOLATED_STORAGE)中的getMountMode()。

public static boolean hasIsolatedStorage() {
    //[persist.sys.isolated_storage]: [true]
    //[sys.isolated_storage_snapshot]: [true]
    return SystemProperties.getBoolean("sys.isolated_storage_snapshot",
            SystemProperties.getBoolean("persist.sys.isolated_storage", true));
}

private static final boolean ENABLE_ISOLATED_STORAGE = StorageManager.hasIsolatedStorage();

public int getExternalStorageMountMode(int uid, String packageName) {
+    //android 10新增逻辑
+    if (ENABLE_ISOLATED_STORAGE) {
+        return getMountMode(uid, packageName);
+    }
    ......
    int mountMode = Integer.MAX_VALUE;
    for (ExternalStorageMountPolicy policy : mPolicies) {
        final int policyMode = policy.getMountMode(uid, packageName);
        if (policyMode == Zygote.MOUNT_EXTERNAL_NONE) {
            return Zygote.MOUNT_EXTERNAL_NONE;
        }
        mountMode = Math.min(mountMode, policyMode);
    }
    if (mountMode == Integer.MAX_VALUE) {
        return Zygote.MOUNT_EXTERNAL_NONE;
    }
    return mountMode;
}

正常模式下hasLegacy=false,走到if判断的DEFAULT分支;legacy模式hasLegacy=true,与之前保持一致,有write权限就走到WRITE模式分支。

private int getMountModeInternal(int uid, String packageName) {
    try {
        ......
        final boolean hasRead = StorageManager.checkPermissionAndCheckOp(mContext, false, 0,
                uid, packageName, READ_EXTERNAL_STORAGE, OP_READ_EXTERNAL_STORAGE);
        final boolean hasWrite = StorageManager.checkPermissionAndCheckOp(mContext, false, 0,
                uid, packageName, WRITE_EXTERNAL_STORAGE, OP_WRITE_EXTERNAL_STORAGE);
        ......
        final boolean hasLegacy = mIAppOpsService.checkOperation(OP_LEGACY_STORAGE,
                uid, packageName) == MODE_ALLOWED;
        if (hasLegacy && hasWrite) {
            return Zygote.MOUNT_EXTERNAL_WRITE;
        } else if (hasLegacy && hasRead) {
            return Zygote.MOUNT_EXTERNAL_READ;
        } else {
            return Zygote.MOUNT_EXTERNAL_DEFAULT;
        }
    } catch (RemoteException e) {
        // Should not happen
    }
    return Zygote.MOUNT_EXTERNAL_NONE;
}

2.3 Legacy Storage属性对权限的影响
安装apk时,就会根据requestLegacyExternalStorage属性来对ops state进行设置,修改OP_LEGACY_STORAGE的默认状态。

<manifest ... >
    <application android:requestLegacyExternalStorage="true" ... >
    </application>
</manifest>

//Q 正常模式
LEGACY_STORAGE: mode=ignore
//Q legacy模式
LEGACY_STORAGE: mode=allow

PermissionPolicyService启动时首先进行权限变化监听:

public void onStart() {
    permManagerInternal.addOnRuntimePermissionStateChangedListener(
    this::synchronizePackagePermissionsAndAppOpsAsyncForUser);
}

private void synchronizePackagePermissionsAndAppOpsAsyncForUser(@NonNull String packageName,
        @UserIdInt int changedUserId) {
    if (isStarted(changedUserId)) {
        synchronized (mLock) {
            if (mIsPackageSyncsScheduled.add(new Pair<>(packageName, changedUserId))) {
                FgThread.getHandler().sendMessage(PooledLambda.obtainMessage(
                        PermissionPolicyService
                                ::synchronizePackagePermissionsAndAppOpsForUser,
                        this, packageName, changedUserId));
            }
            ......
        }
    }
}

APK安装时,会根据requestLegacyExternalStorage属性来通知storage权限变化,调用关系如下:

//调用关系:
1.PackageManagerService.java:
    installPackagesLI()
        commitPackagesLocked()
            updateSettingsLI()
                updateSettingsInternalLI()
                    2.PermissionManagerService.java:
                        mPermissionManager.updatePermissions()
                            restorePermissionState()

//关键代码:
private void restorePermissionState(@NonNull PackageParser.Package pkg, boolean replace,
        @Nullable String packageOfInterest, @Nullable PermissionCallback callback) {
......
        //判断requestLegacyExternalStorage属性
        updatedUserIds = checkIfLegacyStorageOpsNeedToBeUpdated(pkg, replace, updatedUserIds);
......
    for (int userId : updatedUserIds) {
        notifyRuntimePermissionStateChanged(pkg.packageName, userId);
    }
}

最终调用到PermissionPolicyService的监听函数synchronizePackagePermissionsAndAppOpsForUser(),进行默认权限获取和设置。
当apk安装时,声明了requestLegacyExternalStorage="true"属性,并且声明了READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE,那么addOpIfRestricted()就会将LEGACY_STORAGE设置为allow模式。

//调用关系:
synchronizePackagePermissionsAndAppOpsForUser():
    synchroniser.addPackage()
        addOpIfRestricted()//LEGACY_STORAGE加入到mOpsToAllow
    synchroniser.syncPackages()
        setUidModeAllowed()
            setUidMode()//设置LEGACY_STORAGE为allow

//关键代码:
private void addOpIfRestricted(@NonNull PermissionInfo permissionInfo,
        @NonNull PackageInfo pkg) {
......
        //forPermission()会根据requestLegacyExternalStorage的值进行返回
        final SoftRestrictedPermissionPolicy policy =
                SoftRestrictedPermissionPolicy.forPermission(mContext, pkg.applicationInfo,
                        mContext.getUser(), permission);

        final int op = policy.resolveAppOp();
        if (op != OP_NONE) {
            switch (policy.getDesiredOpMode()) {
                case MODE_DEFAULT:
                    mOpsToDefault.add(new OpToChange(uid, pkg.packageName, op));
                    break;
                case MODE_ALLOWED:
                    //在声明READ_EXTERNAL_STORAGE权限下,会将LEGACY_STORAGE加入到mOpsToAllow
                    if (policy.shouldSetAppOpIfNotDefault()) {
                        mOpsToAllow.add(new OpToChange(uid, pkg.packageName, op));
                    } else {
                        mOpsToAllowIfDefault.add(
                                new OpToChange(uid, pkg.packageName, op));
                    }
                    break;
......
}

public static @NonNull SoftRestrictedPermissionPolicy forPermission(@NonNull Context context,
        @Nullable ApplicationInfo appInfo, @Nullable UserHandle user,
        @NonNull String permission) {
    switch (permission) {
        case READ_EXTERNAL_STORAGE: {
            if (appInfo != null) {
                boolean hasAnyRequestedLegacyExternalStorage =
                        appInfo.hasRequestedLegacyExternalStorage();

                hasRequestedLegacyExternalStorage = hasAnyRequestedLegacyExternalStorage;
            }

            return new SoftRestrictedPermissionPolicy() {
                @Override
                public int getDesiredOpMode() {
                    if (applyRestriction) {
                        return MODE_DEFAULT;
                    } else if (hasRequestedLegacyExternalStorage) {
                        //声明了requestLegacyExternalStorage就返回allow
                        return MODE_ALLOWED;
                    } else {
                        return MODE_IGNORED;
                    }
                }

                @Override
                public boolean shouldSetAppOpIfNotDefault() {
                    return getDesiredOpMode() != MODE_IGNORED;
                }
            };
        }

3. sdcard路径权限说明:

  • rwx:421,umask默认为八进制022(----w--w-)
  • /mnt/runtime/default的gid为1015,也就是sdcard_rw;mask 为6,八进制006,group sdcard_rw可读写,也就是other没有rw权限
  • /mnt/runtime/read的gid为9997,也就是everybody;mask 为23,八进制027,group everybody可读、不可写,other没有读写执行权限
  • /mnt/runtime/write的gid为9997,也就是everybody;mask 为7,八进制007,group everybody可读写,other没有读写可执行权限
/data/media on /mnt/runtime/default/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=1015,multiuser,mask=6,derive_gid,default_normal)
/data/media on /mnt/runtime/read/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=23,derive_gid,default_normal)
/data/media on /mnt/runtime/write/emulated type sdcardfs (rw,nosuid,nodev,noexec,noatime,fsuid=1023,fsgid=1023,gid=9997,multiuser,mask=7,derive_gid,default_normal)

/mnt/runtime/default:

drwxrwx--x 3 root sdcard_rw 4096 2018-12-18 03:41 Android
drwxrwx--x 3 root sdcard_rw 4096 2018-12-18 06:11 DCIM

/mnt/runtime/read:

drwxr-x--- 3 root everybody 4096 2018-12-18 03:41 Android
drwxr-x--- 3 root everybody 4096 2018-12-18 06:11 DCIM

/mnt/runtime/write:

drwxrwx--- 3 root everybody 4096 2018-12-18 03:41 Android
drwxrwx--- 3 root everybody 4096 2018-12-18 06:11 DCIM

/sdcard/Android/data:

drwxrwx--- 4 u0_a64 everybody 4096 2018-12-18 06:11 com.android.camera2
drwxrwx--- 3 u0_a15 everybody 4096 2018-12-18 03:41 com.google.android.gms
drwxrwx--- 4 u0_a84 everybody 4096 2018-12-18 03:41 com.google.android.youtube

4. sdcard文件存储示例:

4.1 getExternalFilesDir()随卸载而删除

///storage/emulated/0/Android/data/com.xx.xx/files
File file = File(context.getExternalFilesDir(null), "test.txt");

4.2 媒体文件
媒体文件使用MediaStore操作,卸载后不会删除。
访问其他应用生成的照片、视频、音频,需要READ_EXTERNAL_STORAGE权限。

4.3 存储访问框架(SAF)
访问其他应用创建的文件,例如"Download"目录,必须使用存储访问框架,用户通过框架选择特定文件。

4.4 照片中的位置信息
需要ACCESS_MEDIA_LOCATION权限,才能获取元数据中的位置信息。

<permission android:name="android.permission.ACCESS_MEDIA_LOCATION"
    android:permissionGroup="android.permission-group.UNDEFINED"
    android:label="@string/permlab_mediaLocation"
    android:description="@string/permdesc_mediaLocation"
    android:protectionLevel="dangerous" />
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 175,490评论 5 419
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 74,060评论 2 335
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 124,407评论 0 291
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 47,741评论 0 248
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 56,543评论 3 329
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 43,040评论 1 246
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 34,107评论 3 358
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 32,646评论 0 229
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 36,694评论 1 271
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 32,398评论 2 279
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 33,987评论 1 288
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 30,097评论 3 285
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 35,298评论 3 282
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 27,278评论 0 14
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 28,413评论 1 232
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 38,397评论 2 309
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 38,099评论 2 314

推荐阅读更多精彩内容