You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android14下Microsoft Excel编辑保存后xlsx文件无法被ContentResolver访问

问题:Android 14下Microsoft Excel编辑保存后无法通过ContentResolver访问XLSX文件

环境配置

  • .NET 8 MAUI项目
  • minSdkVersion=29,compileSdkVersion=34,targetSdkVersion=34
  • 已申请权限:READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE、MANAGE_EXTERNAL_STORAGE

问题复现代码

// CREATE按钮点击事件
private async void OnCreateClicked(object sender, EventArgs e) {
#if ANDROID29_0_OR_GREATER
    var activity = Platform.CurrentActivity!;
    var contentResolver = activity.ContentResolver!;

    // 删除已存在的文件
    var cursor = contentResolver.Query(MediaStore.Downloads.ExternalContentUri,
        [IBaseColumns.Id, MediaStore.IMediaColumns.DisplayName, MediaStore.IMediaColumns.Data],
        MediaStore.IMediaColumns.DisplayName + " = ?",
        ["myexcel.xlsx"],
        null);

    if (cursor?.MoveToFirst() == true) {
        var id = cursor.GetLong(cursor.GetColumnIndex(IBaseColumns.Id));
        var uri = ContentUris.WithAppendedId(MediaStore.Downloads.ExternalContentUri, id);
        contentResolver.Delete(uri, null, null);
    }

    // 从Assets读取模板文件
    using var stream = await FileSystem.OpenAppPackageFileAsync("myexcel.xlsx");
    using var memory = new MemoryStream();
    stream.CopyTo(memory);
    var myexcel = memory.ToArray();

    var extType = MimeTypeMap.Singleton!.GetMimeTypeFromExtension("xlsx");

    var storedValue = new ContentValues();
    storedValue.Put(MediaStore.IMediaColumns.DisplayName, "myexcel.xlsx");
    storedValue.Put(MediaStore.IMediaColumns.MimeType, extType);
    storedValue.Put(MediaStore.IMediaColumns.RelativePath, Android.OS.Environment.DirectoryDownloads + "/myexcel/");
    // 保存文件到MediaStore
    var fileUri = contentResolver.Insert(MediaStore.Downloads.ExternalContentUri, storedValue)!;

    using var writeStream = contentResolver.OpenOutputStream(fileUri, "rwt")!;
    writeStream.Write(myexcel);
    Toast.MakeText(activity, "File created.", ToastLength.Long)!.Show();
#endif
}

// OPEN按钮点击事件
private void OnOpenClicked(object sender, EventArgs e) {
#if ANDROID29_0_OR_GREATER
    var activity = Platform.CurrentActivity!;
    var contentResolver = activity.ContentResolver!;

#if ANDROID30_0_OR_GREATER
    // 检查MANAGE_EXTERNAL_STORAGE权限
    if (!Android.OS.Environment.IsExternalStorageManager) {
        try {
            var packageUri = Android.Net.Uri.Parse($"package:{activity.PackageName}");
            var intent = new Intent(Settings.ActionManageAppAllFilesAccessPermission, packageUri);
            activity.StartActivity(intent);
        } catch (Exception ex) {
            Toast.MakeText(activity, ex.Message, ToastLength.Long)!.Show();
        }
    }
#endif

    var cursor = contentResolver.Query(MediaStore.Downloads.ExternalContentUri,
        [IBaseColumns.Id, MediaStore.IMediaColumns.DisplayName, MediaStore.IMediaColumns.Data],
        MediaStore.IMediaColumns.DisplayName + " = ?",
        ["myexcel.xlsx"],
        null);

    if (cursor?.MoveToFirst() == true) {
        var id = cursor.GetLong(cursor.GetColumnIndex(IBaseColumns.Id));
        var uri = ContentUris.WithAppendedId(MediaStore.Downloads.ExternalContentUri, id);
        var intent = new Intent(Intent.ActionView, uri);
        intent.AddFlags(ActivityFlags.NewTask)
                    .AddFlags(ActivityFlags.ClearTop)
                    .AddFlags(ActivityFlags.GrantReadUriPermission)
                    .AddFlags(ActivityFlags.GrantWriteUriPermission);
        try {
            activity.StartActivity(intent);
        } catch (Exception ex) {
            Toast.MakeText(activity, ex.Message, ToastLength.Long)!.Show();
        }
    } else {
        Toast.MakeText(activity, "File not found.", ToastLength.Long)!.Show();
    }
#endif
}

操作流程

  • 点击CREATE按钮,在Download文件夹下的myexcel子目录创建myexcel.xlsx
  • 点击OPEN按钮用Microsoft Excel打开文件
    • 未编辑直接返回:应用可再次正常打开文件
    • 编辑内容后保存返回:应用无法通过MediaStore查询到该文件

异常现象

  • 文件仍存在于Google Files中,可正常打开
  • 编辑前执行adb content query能查到文件记录,编辑后无查询结果
  • 在Google Files中重命名文件再改回原名,可重新被adb和应用识别
  • 编辑前后文件权限无异常
  • Android 11系统下无此问题

期望效果

  • 文件稳定保存到外部存储的Download文件夹
  • 编辑保存后应用可再次打开文件并显示修改内容
  • 可通过ContentResolver或其他方式删除编辑后的文件

解决方案

原因分析

Android 14中Microsoft Excel采用原子保存机制:编辑时创建临时文件,保存时删除原文件,再将临时文件重命名为原文件名。但Android 14的MediaStore对第三方应用修改的文件索引更新不及时,且Excel未主动触发MediaStore扫描,导致原文件的MediaStore记录被删除后,新文件未被重新索引。

解决办法

1. 直接通过文件路径访问(依赖MANAGE_EXTERNAL_STORAGE权限)

既然已申请全文件访问权限,可绕过MediaStore,直接通过文件路径操作:

private void OnOpenClicked(object sender, EventArgs e) {
#if ANDROID29_0_OR_GREATER
    var activity = Platform.CurrentActivity!;

#if ANDROID30_0_OR_GREATER
    if (!Android.OS.Environment.IsExternalStorageManager) {
        try {
            var packageUri = Android.Net.Uri.Parse($"package:{activity.PackageName}");
            var intent = new Intent(Settings.ActionManageAppAllFilesAccessPermission, packageUri);
            activity.StartActivity(intent);
            return;
        } catch (Exception ex) {
            Toast.MakeText(activity, ex.Message, ToastLength.Long)!.Show();
            return;
        }
    }
#endif

    // 直接构建文件路径
    var filePath = Path.Combine(
        Android.OS.Environment.ExternalStorageDirectory.AbsolutePath,
        Android.OS.Environment.DirectoryDownloads,
        "myexcel",
        "myexcel.xlsx"
    );

    if (File.Exists(filePath)) {
        var fileUri = Android.Net.Uri.FromFile(new Java.IO.File(filePath));
        var intent = new Intent(Intent.ActionView, fileUri);
        intent.AddFlags(ActivityFlags.NewTask)
              .AddFlags(ActivityFlags.GrantReadUriPermission)
              .AddFlags(ActivityFlags.GrantWriteUriPermission);
        try {
            activity.StartActivity(intent);
        } catch (Exception ex) {
            Toast.MakeText(activity, ex.Message, ToastLength.Long)!.Show();
        }
    } else {
        Toast.MakeText(activity, "File not found.", ToastLength.Long)!.Show();
    }
#endif
}

2. 触发MediaStore重新扫描文件

如果坚持使用MediaStore,可在查询前强制触发文件扫描,更新索引:

private void TriggerMediaScan(string filePath) {
#if ANDROID29_0_OR_GREATER
    var scannerClient = new MediaScannerClient(filePath);
    var scannerConnection = new MediaScannerConnection(Platform.CurrentActivity!, scannerClient);
    scannerConnection.Connect();
#endif
}

private class MediaScannerClient : Java.Lang.Object, MediaScannerConnection.IMediaScannerConnectionClient {
    private readonly string _filePath;

    public MediaScannerClient(string filePath) => _filePath = filePath;

    public void OnMediaScannerConnected() {
        MediaScannerConnection.ScanFile(Platform.CurrentActivity!, new[] { _filePath }, null, null);
    }

    public void OnScanCompleted(string path, Android.Net.Uri uri) {
        MediaScannerConnection.Disconnect();
    }
}

在OnOpenClicked中,先调用TriggerMediaScan(filePath),再执行MediaStore查询。

3. 使用存储访问框架(SAF)获取持久化权限

若不想依赖MANAGE_EXTERNAL_STORAGE(Google Play审核可能受限),可通过SAF让用户选择文件,获取持久化URI权限:

private const int OpenDocumentRequestCode = 1001;

private void OpenWithSAF() {
    var intent = new Intent(Intent.ActionOpenDocument);
    intent.AddCategory(Intent.CategoryOpenable);
    intent.SetType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    // 申请持久化权限
    intent.AddFlags(ActivityFlags.GrantPersistableUriPermission);
    intent.AddFlags(ActivityFlags.GrantReadUriPermission);
    intent.AddFlags(ActivityFlags.GrantWriteUriPermission);
    Platform.CurrentActivity!.StartActivityForResult(intent, OpenDocumentRequestCode);
}

// 在Activity中处理SAF返回结果
protected override void OnActivityResult(int requestCode, Result resultCode, Intent? data) {
    base.OnActivityResult(requestCode, resultCode, data);
    if (requestCode == OpenDocumentRequestCode && resultCode == Result.Ok && data?.Data != null) {
        // 持久化URI权限
        var takeFlags = ActivityFlags.GrantReadUriPermission | ActivityFlags.GrantWriteUriPermission;
        Platform.CurrentActivity!.ContentResolver.TakePersistableUriPermission(data.Data, takeFlags);
        // 保存URI到本地,后续可直接使用
        var prefs = GetSharedPreferences("FilePrefs", FileCreationMode.Private);
        prefs.Edit().PutString("ExcelFileUri", data.Data.ToString()).Apply();
        // 打开文件
        var intent = new Intent(Intent.ActionView, data.Data);
        intent.AddFlags(ActivityFlags.NewTask);
        StartActivity(intent);
    }
}

这种方式不受MediaStore索引问题影响,符合Android 13+存储最佳实践。

内容的提问来源于stack exchange,提问作者WJC

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 10:09:58