Delphi 12 FMX Android 文件复制权限问题求助
问题分析与解决方案
核心问题根源
- Android 13+ 存储权限机制变更:你的
targetSdkVersion设为34,而READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE在Android 13(API33)及以上已被废弃,且Manifest中给这两个权限加了android:maxSdkVersion="32",导致API34设备上系统完全忽略这两个权限,PermissionsService请求时自然不会弹出授权框。 - MANAGE_EXTERNAL_STORAGE权限使用错误:该权限无法通过普通权限请求流程触发授权,必须引导用户到应用设置页面手动开启,且Google Play对该权限的审核极为严格,非必要场景不建议使用。
- 公共Documents目录访问方式错误:Android 13+ 限制直接通过文件路径访问公共Documents目录,即使拥有全存储权限也可能触发权限拒绝,需通过存储访问框架(SAF)或MediaStore进行操作。
分步解决方案
方案一:存储访问框架(SAF)推荐,符合Google Play政策
无需特殊权限,通过系统文件选择器让用户主动选择目标文件,从根源避免权限问题:
uses Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.JavaTypes, Androidapi.JNI.Provider, Androidapi.Helpers, FMX.DialogService, System.IOUtils; // 触发系统文件选择器 procedure TfrmMain.CopyDBFromDocuments; var Intent: JIntent; begin {$IFDEF ANDROID} Intent := TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_OPEN_DOCUMENT); Intent.addCategory(TJIntent.JavaClass.CATEGORY_OPENABLE); Intent.setType('application/sqlite'); // 指定数据库文件类型,更精准 TAndroidHelper.Activity.startActivityForResult(Intent, 100); // 100为自定义请求码 {$ENDIF} end; // 处理文件选择结果 procedure TfrmMain.ActivityResult(RequestCode, ResultCode: Integer; Data: JIntent); var SourceUri: JUri; SourceStream, DestStream: TStream; DestPath: string; begin inherited; if (RequestCode = 100) and (ResultCode = TJActivity.JavaClass.RESULT_OK) and (Data <> nil) then begin SourceUri := Data.getData; DestPath := TPath.Combine(TPath.GetDocumentsPath, 'calorie_consumption.db'); try dmCC.FDConnection.Connected := False; // 通过ContentResolver获取文件输入流 SourceStream := TAndroidHelper.ContentResolver.openInputStream(SourceUri); try DestStream := TFileStream.Create(DestPath, fmCreate or fmShareDenyWrite); try DestStream.CopyFrom(SourceStream, SourceStream.Size); memSQLTest.Lines.Add('文件复制完成:' + DestPath); finally DestStream.Free; end; finally SourceStream.Free; end; dmCC.FDConnection.Connected := True; except on E: Exception do TDialogService.ShowMessage('复制失败:' + E.Message); end; end; end;
方案二:MANAGE_EXTERNAL_STORAGE权限仅适合特殊场景
若必须直接访问公共Documents目录,按以下步骤修改:
- 修正Manifest权限配置:
移除READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE的android:maxSdkVersion="32"属性,或直接删除这两个废弃权限,保留MANAGE_EXTERNAL_STORAGE。 - 修改权限请求逻辑:
uses Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.JavaTypes, Androidapi.Helpers, FMX.DialogService, System.IOUtils; procedure TfrmMain.RequestFullStoragePermission; var Intent: JIntent; begin {$IFDEF ANDROID} if TAndroidHelper.CheckSelfPermission(TJManifest_permission.JavaClass.MANAGE_EXTERNAL_STORAGE) <> TPermissionStatus.Granted then begin // 引导用户到应用设置页面开启权限 Intent := TJIntent.JavaClass.init(TJSettings.JavaClass.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); Intent.setData(TJUri.JavaClass.parse('package:' + TAndroidHelper.Context.getPackageName)); TAndroidHelper.Activity.startActivity(Intent); end else begin // 权限已授权,执行复制逻辑 DoCopyDB(True); end; {$ENDIF} end; procedure TfrmMain.DoCopyDB(direction2download: Boolean); var f1, f2: String; begin dmCC.FDConnection.Connected := False; f1 := TPath.Combine(TPath.GetDocumentsPath, 'calorie_consumption.db'); f2 := TPath.Combine(TPath.GetSharedDocumentsPath, 'calorie_consumption.db'); try if direction2download then begin TFile.Copy(f2, f1, True); memSQLTest.Lines.Add(f2 + ' -> ' + f1); end else begin TFile.Copy(f1, f2, True); memSQLTest.Lines.Add(f1 + ' -> ' + f2); end; except on E: Exception do TDialogService.ShowMessage('复制失败:' + E.Message); end; dmCC.FDConnection.Connected := True; end;
- 注意事项:提交Google Play时必须详细说明应用需要全存储权限的原因,否则会被审核拒绝。
方案三:临时降级targetSdkVersion不推荐
仅用于测试场景,将targetSdkVersion改为32,此时原有的READ/WRITE_EXTERNAL_STORAGE权限可正常生效,PermissionsService也能弹出授权框,但不符合Google Play最新政策,无法用于正式上架。
关键注意点
- 禁止硬编码文件路径,必须使用
TPath.GetSharedDocumentsPath()、TPath.GetDocumentsPath()等官方API获取路径。 - Android 10+ 推荐使用Scoped Storage,应用私有目录(
TPath.GetDocumentsPath())无需任何权限即可自由读写。
内容的提问来源于stack exchange,提问作者sax
相关产品推荐
相关产品推荐

