admin管理员组文章数量:1558939
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程序,参考了下,得到了一个思路:
- 在系统中添加一个服务SilencePackageInstallerService,专门用来负责安装应用,它将得到客户端传过来的Uri,将客户端的程序以流的方式保存到一个PackageManager(最终使用PackageManager的installPackage方法安装应用)可以访问到的地方。
- 客户端调用的接口沿用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/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/download/visionliao/10773350
本文标签: SecurityExceptionPermissionJavalangexported
版权声明:本文标题:java.lang.SecurityException: Permission requires the provider be exported, grantUriPermission() 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/xitong/1727405546a1113332.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论