Android 5设备旧APP向新APP迁移SD卡数据遇权限拒绝求助
问题背景
运营一款仅以APK分发的旧APP,现计划将改版后的新APP上架Google Play,需要在旧APP中添加迁移按钮,实现将SD卡中旧APP的数据迁移至新APP目录的功能,要求新APP不参与任何迁移操作。
Android 7及以上设备可通过DocumentsContract.moveDocument()实现文件移动,但该方法不适用于Android 5设备。自行实现的替代方案遭遇权限拒绝(Permission Denied),无法完成迁移。
现有代码
迁移处理代码
void forTEST() { File fileDir = getExternalFilesDir(null); File[] fileDirs = getExternalFilesDirs(null); File appAExternalFileDir = null; for (File dir : fileDirs) { if ( dir != null && !fileDir.equals(dir) && Environment.isExternalStorageRemovable(dir)) { appAExternalFileDir = dir; } } if (appAExternalFileDir == null) return; String appAExternalFileDirPath = appAExternalFileDir.getAbsolutePath(); Log.d("testlog", "externalFileDirPath:" + appAExternalFileDir.getAbsolutePath()); // externalFileDirPath:/storage/MicroSD/Android/data/com.example.testAppA/files Log.d("testlog", appAExternalFileDir.canRead() ? "true" : "false"); // true Log.d("testlog", appAExternalFileDir.canWrite() ? "true" : "false"); // true String appBExternalFileDirPath = appAExternalFileDirPath.replace("com.example.testAppA", "testAppB"); File appBExternalFileDir = new File(appBExternalFileDirPath); Log.d("testlog", "externalFileDirPath:" + appBExternalFileDir.getAbsolutePath());// externalFileDirPath:/storage/MicroSD/Android/data/com.example.testAppB/files Log.d("testlog", appBExternalFileDir.canRead() ? "true" : "false"); // true Log.d("testlog", appBExternalFileDir.canWrite() ? "true" : "false"); // true File[] appAFiles = appAExternalFileDir.listFiles(); for(int i=0; i<appAFiles.length; i++) { String fileName = appAFiles[i].getName(); File destFile = new File(appBExternalFileDir, appAFiles[i].getName()); moveFileToDest(appAFiles[i], destFile); // I tried renameTo() but only found true/false, so I created moveFileToDest() and executed it. // renameTo() returned false. // if (appAFiles[i].renameTo(destFile)) { // Log.d("testLog", "success to renameTo()"); // } else { // Log.d("testlog", "failure to renameTo()"); // } } } void moveFileToDest(File sourceFile, File destinationFile) { try { // When I performed the move process described below without creating a new file first, a FileNotFoundException occurred, so I added createNewFile(). if (destinationFile.createNewFile()) { // throw java.io.IOException: open failed: EACCES (Permission denied) Log.d("testlog", "create success!!"); } else { Log.d("testlog", "create failed"); } FileInputStream fis = new FileInputStream(sourceFile); FileOutputStream fos = new FileOutputStream(destinationFile); FileChannel sourceChannel = fis.getChannel(); FileChannel destinationChannel = fos.getChannel(); sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel); sourceChannel.close(); fis.close(); destinationChannel.close(); fos.close(); sourceFile.delete(); } catch (IOException e) { e.printStackTrace(); } }
AndroidManifest.xml(节选)
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
遇到的问题
旧APP目录appAExternalFileDir和新APP目录appBExternalFileDir的canRead()、canWrite()均返回true,但在新APP目录内创建文件时抛出IOException: open failed: EACCES (Permission denied),调用renameTo()也返回false。
解决方案
核心原因
Android 5及以下系统中,WRITE_EXTERNAL_STORAGE权限仅允许访问公共外部存储,其他APP的私有外部存储目录(Android/data/包名/)受系统权限隔离保护,即使路径正确,旧APP也无法直接写入新APP的私有目录。canWrite()返回true仅代表文件系统层面的权限,不通过Android系统的APP级权限校验。
可行方案:使用共享UID
如果旧APP和新APP使用相同的签名密钥(改版APP上架Google Play必须与旧APP签名一致,满足此前提),可通过配置共享UID实现跨APP目录访问:
- 在旧APP和新APP的
AndroidManifest.xml的<manifest>标签中添加相同的共享UID:
建议使用公司域名+唯一标识作为UID值,避免冲突。android:sharedUserId="com.yourcompany.appshared" - 重新编译签名两个APP,安装后旧APP即可正常读写新APP的私有外部存储目录,权限问题解决。
备选方案(若无法使用共享UID)
放弃直接写入新APP私有目录,将数据迁移至SD卡公共目录(如/storage/MicroSD/Documents/你的APP迁移文件夹/),但此方案需要新APP启动时自动读取该目录数据导入,不符合“新APP不参与迁移”的要求,仅作为兜底选择。
内容的提问来源于stack exchange,提问作者shibashiba

