java.lang.SecurityException: Permission requires the provider be exported, grantUriPermission()

java.lang.SecurityException: Permission Denial: reading … requires the provider be exported, or grantUriPermission()

声明:我没有从本质上解决这个问题,只能通过其它的办法绕开这个问题。

最近开发应用程序静默安装功能,并且不想改动系统原有的安装应用的框架,因为万一有什么问题会影响系统稳定性,这个风险可是很大的。于是想到了PM,当我们使用adb 的pm命令可以很轻松的安装一个应用,并且是没有界面的,符合用户静默安装的要求。但是研究了一下,发现从Android 5.0之后,因为权限管理的原因要采用这种方式也很困难,代码可以参考frameworks/base/cmds/pm/src/com/android/commands/pm/Pm.java的代码,这里截取install方法:

	private int runInstall() throws RemoteException {
        if (isNoInstallApp()) {
            System.err.println("Error: System prohibit installation of third party applications.");
            return 1;
        }
        long startedTime = SystemClock.elapsedRealtime();
        final InstallParams params = makeInstallParams();
        final String inPath = nextArg();
        if (params.sessionParams.sizeBytes == -1 && !STDIN_PATH.equals(inPath)) {
            File file = new File(inPath);
            if (file.isFile()) {
                try {
                    ApkLite baseApk = PackageParser.parseApkLite(file, 0);
                    PackageLite pkgLite = new PackageLite(null, baseApk, null, null, null, null,
                            null, null);
                    params.sessionParams.setSize(
                            PackageHelper.calculateInstalledSize(pkgLite, false,
                            params.sessionParams.abiOverride));
                } catch (PackageParserException | IOException e) {
                    System.err.println("Error: Failed to parse APK file: " + e);
                    return 1;
                }
            } else {
                System.err.println("Error: Can't open non-file: " + inPath);
                return 1;
            }
        }
        final int sessionId = doCreateSession(params.sessionParams,
                params.installerPackageName, params.userId);
        try {
            if (inPath == null && params.sessionParams.sizeBytes == -1) {
                System.err.println("Error: must either specify a package size or an APK file");
                return 1;
            }
            if (doWriteSession(sessionId, inPath, params.sessionParams.sizeBytes, "base.apk",
                    false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) {
                return 1;
            }
            Pair<String, Integer> status = doCommitSession(sessionId, false /*logSuccess*/);
            if (status.second != PackageInstaller.STATUS_SUCCESS) {
                return 1;
            }
            Log.i(TAG, "Package " + status.first + " installed in " + (SystemClock.elapsedRealtime()
                    - startedTime) + " ms");
            System.out.println("Success");
            return 0;
        } finally {
            try {
                mInstaller.abandonSession(sessionId);
            } catch (Exception ignore) {
            }
        }
    }

当把这段代码封装好后,使用第三方应用调用安装方法,代码最终会调用到PackageManagerService,这其中会经过大量的筛选,其中一个筛选就是检测Context(调用方)的权限,只有类似shell这种高权限进程才能使用这种方式。因为我要提供接口给第三方程序调用,所以只能放弃这种方式。然后想到了Package/apps/PackageInstaller程序,参考了下,得到了一个思路:

  1. 在系统中添加一个服务SilencePackageInstallerService,专门用来负责安装应用,它将得到客户端传过来的Uri,将客户端的程序以流的方式保存到一个PackageManager(最终使用PackageManager的installPackage方法安装应用)可以访问到的地方。
  2. 客户端调用的接口沿用PackageInstaller的模式,采用ContentProvider的形式分享Uri给SilencePackageInstallerService。
服务端核心代码
	/** 得到客户端传递过来的Uri, 并且将Uri的data以流的形式拷贝 **/
	private void copyFileToStorage(Uri packageUri) {
        File sourceFile = null;
        try {
            sourceFile = File.createTempFile("package", ".apk", createTargetFolder());
            Log.w(TAG, "install copyFileToStorage sourceFile.path: " + sourceFile.getPath());
            try (
                InputStream in = getContentResolver().openInputStream(packageUri);
                OutputStream out = (in != null) ? new FileOutputStream(
                        sourceFile) : null;
            ) {
                // Despite the comments in ContentResolver#openInputStream
                // the returned stream can be null.
                if (in == null) {
                    return;
                }
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = in.read(buffer)) >= 0) {
                    // Be nice and respond to a cancellation
                    out.write(buffer, 0, bytesRead);
                }
            } 
        } catch (IOException ioe) {
            Log.w(TAG, "Error staging apk from content URI", ioe);
        }
    }

    /** 安装应用 **/
    public void installApp(final String pkgFilePath) {
        PackageManager pm = mContext.getPackageManager();
        File file = new File(pkgFilePath);
        if (!file.exists()) {
            debug("install " + pkgFilePath + " failed, file not exists");
            return;
        }
        debug("start install " + pkgFilePath);
        Uri uri = Uri.fromFile(file);
        pm.installPackage(uri, new IPackageInstallObserver() {
            @Override
            public void packageInstalled(String s, int i) throws RemoteException {
                // 安装结果  s: 应用包名  i: 安装状态
                debug(pkgFilePath + " installed package: " + s + ", status: " + i);
                // 这里可以将安装的结果发送给客户端
                sendBroadcast(...)
            }
            @Override
            public IBinder asBinder() {
                return null;
            }
        }, PackageManager.INSTALL_REPLACE_EXISTING, mContext.getPackageName());
    }
客户端核心代码
		final String[] PATH = {
				"/storage/emulated/0/Pictures/火山小视频.apk",
				"/storage/emulated/0/Pictures/CPUZ_10.apk",
				"/storage/emulated/0/Pictures/PP助手.apk",
				"/storage/emulated/0/Pictures/快视频.apk",
				"/storage/emulated/0/Pictures/墨迹天气.apk",
				"/storage/emulated/0/Pictures/快手.apk",
				"/storage/emulated/0/Pictures/神庙逃亡2.apk",
				"/storage/emulated/0/Pictures/掌阅.apk",
				"/storage/emulated/0/Pictures/美图秀秀.apk",
				"/storage/emulated/0/Pictures/拼多多.apk"
				};
		final String[] PATH2 = {
				getFilesDir().getAbsolutePath() + File.separator + "火山小视频.apk",
				getFilesDir().getAbsolutePath() + File.separator
						+ "CPUZ_10.apk",
				getFilesDir().getAbsolutePath() + File.separator + "PP助手.apk",
				getFilesDir().getAbsolutePath() + File.separator + "快视频.apk",
				getFilesDir().getAbsolutePath() + File.separator + "墨迹天气.apk",
				getFilesDir().getAbsolutePath() + File.separator + "快手.apk",
				getFilesDir().getAbsolutePath() + File.separator + "神庙逃亡2.apk",
				getFilesDir().getAbsolutePath() + File.separator + "掌阅.apk",
				getFilesDir().getAbsolutePath() + File.separator + "美图秀秀.apk",
				getFilesDir().getAbsolutePath() + File.separator + "拼多多.apk"
				};
        /** 模拟下载,将内存中的文件拷贝的应用的私有目录下,通常应用的数据都会保存到自己的私有目录下,以保证数据安全 **/
		for (int i = 0; i < PATH.length; i++) {
			synchronized (this) {
				File file = new File(PATH2[i]);
				if (!file.exists()) {
					copyFile(PATH[i], PATH2[i]);
				}
				Log.d(TAG, "file.getPath: " + file.getPath());
			}
		}
		// 开始安装,一次性将安装PATH数组中的所有应用程序
		for (int i = 0; i < PATH2.length; i++) {
			install(PATH2[i]);
		}

        /** 使用这种方式还需要配置file_paths.xml文件,并在AndroidManifest.xml 中申明,这里只给关键代码 **/
        /**客户端下载地址:https://download.csdn.net/download/visionliao/10773350 **/
		private void install(String filePath) {
			Log.i(TAG, "开始执行安装: " + filePath);
			File apkFile = new File(filePath);
			if (!apkFile.exists()) {
				return;
			}
			Intent intent = new Intent();
			intent.setAction(Intent.ACTION_VIEW);
			intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
			if (Build.VERSION.SDK_INT >= 24) {
				Log.w(TAG, "版本大于 N ,开始使用 fileProvider 进行安装....");
				intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
				Uri contentUri = FileProvider.getUriForFile(mContext,
						"com.prowave.installpackage.fileprovider", apkFile);
				intent.setDataAndType(contentUri,
						"application/vnd.android.package-archive-silence");
			} else {
				Log.w(TAG, "正常进行安装...");
				intent.setDataAndType(Uri.fromFile(apkFile),
						"application/vnd.android.package-archive-silence");
			}
			startActivity(intent);
		}

其实这也相当于一个压力测试程序了,一般没有哪个客户端会发了疯似得一次性安装这么多应用,通常都是添加一个安装队列,一个一个应用安装。但是验证代码的健壮性,越变态的方式效果越好。
经过测试发现,当PATH中的应用越少,比如一个,不会有问题;同时安装的应用越多,越容易出问题:

01-15 03:27:23.955  3670  6926 W System.err: java.lang.SecurityException: Permission Denial: reading android.support.v4.content.FileProvider uri content://com.prowave.installpackage.fileprovider/root_path/data/data/com.prowave.installpackage/files/%E7%A5%9E%E5%BA%99%E9%80%83%E4%BA%A12.apk from pid=3670, uid=1000 requires the provider be exported, or grantUriPermission()
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: Error staging apk from content URI
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: java.lang.SecurityException: Permission Denial: reading android.support.v4.content.FileProvider uri content://com.prowave.installpackage.fileprovider/root_path/data/data/com.prowave.installpackage/files/%E5%88%80%E5%89%91%E6%96%97%E7%A5%9E%E4%BC%A0.apk from pid=3670, uid=1000 requires the provider be exported, or grantUriPermission()
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: 	at android.os.Parcel.readException(Parcel.java:2004)
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: 	at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:183)
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: 	at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:146)
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: 	at android.content.ContentProviderProxy.openTypedAssetFile(ContentProviderNative.java:698)
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: 	at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1412)
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: 	at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:1249)
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: 	at android.content.ContentResolver.openInputStream(ContentResolver.java:969)
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: 	at com.action.tool.mdm.util.install.SilencePackageInstallerService.eb(:150)
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: 	at com.action.tool.mdm.util.install.SilencePackageInstallerService.ef(Unknown Source:0)
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: 	at com.action.tool.mdm.util.install.b.run(:130)
01-15 03:27:23.955  3670  6932 W PackageInstallSilence: 	at java.lang.Thread.run(Thread.java:764)
01-15 03:27:23.955  3670  6926 W System.err: 	at android.os.Parcel.readException(Parcel.java:2004)

报的错误是java.lang.SecurityException: Permission Denial: reading android.support.v4.content.FileProvider uri content://com.prowave.installpackage.fileprovider/root_path/data/data/com.prowave.installpackage/files/%E7%A5%9E%E5%BA%99%E9%80%83%E4%BA%A12.apk from pid=3670, uid=1000 requires the provider be exported, or grantUriPermission()
这是在服务端代码**in = getContentResolver().openInputStream(packageUri)**这一句引发的SecurityException异常, 意思是说传过来的Uri没有read权限,无法解析这个Uri。

于是查看代码,发现客户端的intent确实传递了读的权限,也就是intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)这句代码。而且更诡异的是,并不是每次都会出现,有时候10个应用都可以安装完成。而出现失败的情况下,也不是每个应用都会有异常,有时候是前两个应用没有得到读的权限,有时候是前四个、前六个…

顺便贴出爆出这个异常的代码,位于frameworks/base/core/java/android/content/ContentProvider.java

	/** {@hide} */
    protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken)
            throws SecurityException {
        final Context context = getContext();
        final int pid = Binder.getCallingPid();
        final int uid = Binder.getCallingUid();
        String missingPerm = null;
        int strongestMode = MODE_ALLOWED;

        if (UserHandle.isSameApp(uid, mMyUid)) {
            return MODE_ALLOWED;
        }

        if (mExported && checkUser(pid, uid, context)) {
            final String componentPerm = getReadPermission();
            if (componentPerm != null) {
                final int mode = checkPermissionAndAppOp(componentPerm, callingPkg, callerToken);
                if (mode == MODE_ALLOWED) {
                    return MODE_ALLOWED;
                } else {
                    missingPerm = componentPerm;
                    strongestMode = Math.max(strongestMode, mode);
                }
            }

            // track if unprotected read is allowed; any denied
            // <path-permission> below removes this ability
            boolean allowDefaultRead = (componentPerm == null);

            final PathPermission[] pps = getPathPermissions();
            if (pps != null) {
                final String path = uri.getPath();
                for (PathPermission pp : pps) {
                    final String pathPerm = pp.getReadPermission();
                    if (pathPerm != null && pp.match(path)) {
                        final int mode = checkPermissionAndAppOp(pathPerm, callingPkg, callerToken);
                        if (mode == MODE_ALLOWED) {
                            return MODE_ALLOWED;
                        } else {
                            // any denied <path-permission> means we lose
                            // default <provider> access.
                            allowDefaultRead = false;
                            missingPerm = pathPerm;
                            strongestMode = Math.max(strongestMode, mode);
                        }
                    }
                }
            }

            // if we passed <path-permission> checks above, and no default
            // <provider> permission, then allow access.
            if (allowDefaultRead) return MODE_ALLOWED;
        }

        // last chance, check against any uri grants
        final int callingUserId = UserHandle.getUserId(uid);
        final Uri userUri = (mSingleUser && !UserHandle.isSameUser(mMyUid, uid))
                ? maybeAddUserId(uri, callingUserId) : uri;
        if (context.checkUriPermission(userUri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION,
                callerToken) == PERMISSION_GRANTED) {
            return MODE_ALLOWED;
        }

        // If the worst denial we found above was ignored, then pass that
        // ignored through; otherwise we assume it should be a real error below.
        if (strongestMode == MODE_IGNORED) {
            return MODE_IGNORED;
        }

        final String suffix;
        if (android.Manifest.permission.MANAGE_DOCUMENTS.equals(mReadPermission)) {
            suffix = " requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs";
        } else if (mExported) {
            suffix = " requires " + missingPerm + ", or grantUriPermission()";
        } else {
            suffix = " requires the provider be exported, or grantUriPermission()";
        }
        throw new SecurityException("Permission Denial: reading "
                + ContentProvider.this.getClass().getName() + " uri " + uri + " from pid=" + pid
                + ", uid=" + uid + suffix);
    }

分析这段代码,也没看出有什么问题。如果说是我写的代码有问题,那么应该是每次、每个应用安装都会有问题,不应该出现这种概率情况,为了证明这一点,我将**intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)**这一句去掉,果然,每次、每个应用都异常了。

最后,我只能得出一个结论:多个intent一起传输的时候数据丢失了。这个问题一时无解,只能改进客户端代码,不用等下载完所有的应用再同时去发intent,每下载完成一个应用就马上发安装的intent:

		final String[] PATH = {
				"/storage/emulated/0/Pictures/火山小视频.apk",
				"/storage/emulated/0/Pictures/CPUZ_10.apk",
				"/storage/emulated/0/Pictures/PP助手.apk",
				"/storage/emulated/0/Pictures/快视频.apk",
				"/storage/emulated/0/Pictures/墨迹天气.apk",
				"/storage/emulated/0/Pictures/快手.apk",
				"/storage/emulated/0/Pictures/神庙逃亡2.apk",
				"/storage/emulated/0/Pictures/掌阅.apk",
				"/storage/emulated/0/Pictures/美图秀秀.apk",
				"/storage/emulated/0/Pictures/拼多多.apk"
				};

		final String[] PATH2 = {
				getFilesDir().getAbsolutePath() + File.separator + "火山小视频.apk",
				getFilesDir().getAbsolutePath() + File.separator
						+ "CPUZ_10.apk",
				getFilesDir().getAbsolutePath() + File.separator + "PP助手.apk",
				getFilesDir().getAbsolutePath() + File.separator + "快视频.apk",
				getFilesDir().getAbsolutePath() + File.separator + "墨迹天气.apk",
				getFilesDir().getAbsolutePath() + File.separator + "快手.apk",
				getFilesDir().getAbsolutePath() + File.separator + "神庙逃亡2.apk",
				getFilesDir().getAbsolutePath() + File.separator + "掌阅.apk",
				getFilesDir().getAbsolutePath() + File.separator + "美图秀秀.apk",
				getFilesDir().getAbsolutePath() + File.separator + "拼多多.apk"
				};
        // 模拟下载
		for (int i = 0; i < PATH.length; i++) {
			synchronized (this) {
				File file = new File(PATH2[i]);
				if (file.exists()) {
					file.delete(); // 如果存在就删除
				}
				copyFile(PATH[i], PATH2[i]);
				// 每下载完一个应用马上发intent
				install(PATH2[i]);
			}
		}

这样改了客户端的代码之后再测试,没有遇到intent数据丢失的情况,每次都能全部安装完成,这大概是android 的一个Bug,我没有找到解决办法,有遇到类似问题的欢迎讨论。
客户端代码下载地址:https://download.csdn.net/download/visionliao/10773350

发布了25 篇原创文章 · 获赞 31 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/visionliao/article/details/83864530
今日推荐