Search code examples
androidexcelmauiandroid-14

Microsoft Excel changed file's visibility-in-code after saving in Android 14


I find once Microsoft Excel edit an excel file and saved, my app can no longer access that xlsx file by ContentResolver on Android 14.

I am creating a .Net8 MAUI project targeting Android.

I set minSdkVersion as 29, compileSdkVersion and targetSdkVersion as 34.

I want to save an excel file to the Download folder in external storage, so I asked READ_EXTERNAL_STORAGE WRITE_EXTERNAL_STORAGE and MANAGE_EXTERNAL_STORAGE permissions in AndroidManifest.xml.

Here is my sample code.

// When CREATE button clicked.
private async void OnCreateClicked(object sender, EventArgs e) {
#if ANDROID29_0_OR_GREATER
    var activity = Platform.CurrentActivity!;
    var contentResolver = activity.ContentResolver!;

    // Delete file when existed.
    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);
    }

    // Read xlsx file from Assets in "Resources/Raw/myexcel.xlsx"
    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/");
    // Save excel file with ContentResolver
    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
}

// When OPEN button clicked.
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
    // Make sure the app can access all files.
    // After permission granted, need to click open button again.
    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) {
        // Can get in here before MSExcel editing and saving that xlsx file.
        var id = cursor.GetLong(cursor.GetColumnIndex(IBaseColumns.Id));
        var path = cursor.GetString(cursor.GetColumnIndex(MediaStore.IMediaColumns.Data));
        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 {
        // After MSExcel editing and saving that xlsx file, it always get in here
        Toast.MakeText(activity, "File not found.", ToastLength.Long)!.Show();
    }
#endif
}

I first clicked CREATE button to create an xlsx file in Download folder.
Then I clicked OPEN button to open this file using Microsoft Excel.

If I just push back button without editing this xlsx file, the Microsoft Excel will not do file saving progress, so I can click OPEN button in my app to open this file again.

But if I edit something using Microsoft Excel and push back button, the Microsoft Excel will do file saving process and I can NEVER open that xlsx file using my app as it seems like dispeared.

Some wired point:

  1. I can still see this xlsx file with Google Files APP. If I click this file in Files, I can open it, but that is not what I am expecting.
  2. Before editing that xlsx file with Microsoft Excel, I can find that xlsx file with content query command using adb.
    Input:
    adb shell
    content query --projection _id:_data:_display_name:owner_package_name:volume_name --uri content://media/external/downloads --where "_display_name='myexcel.xlsx'"
    Output:
    Row: 0 _id=1000000787, _data=/storage/emulated/0/Download/myexcel/myexcel.xlsx, _display_name=myexcel.xlsx, owner_package_name=com.companyname.filedownloadexcel, volume_name=external_primary
    But after editing and saving that xlsx file, I can no longer find it using adb's content query.
    Input:
    adb shell
    content query --projection _id:_data:_display_name:owner_package_name:volume_name --uri content://media/external/downloads --where "_display_name='myexcel.xlsx'"
    Output:
    No result found.
    But I can find it in Google Files APP using my eyes!
  3. If I rename that xlsx file in Google Files to somename, and rename it back, I can find it using adb and in my app by click OPEN button!
  4. Tried stat command using adb shell before and after Microsoft Excel saving xlsx file and did not find strange things in permissions.
  5. Everything works fine in Android 11.

What I am expecting

  1. Save an xlsx file in Download folder which belongs to external storage.
  2. Click OPEN button to open xlsx file with Microsoft Excel, edit something and push Android back button to save & exit.
  3. Click OPEN button again, and open that xlsx file with Microsoft Excel, showing what I edited just before.
  4. Use ContentResolver#Delete or somehow to delete that xlsx file after Microsoft Excel saving it.

Solution

  • I can see how this can happen, Excel may modify media store metadata (or) delete the original file and create a new one during the saving process. It's hard to say what may happen with any external app like Excel.

    Ultimately, the file you saved to Downloads will become inaccessible using it's original URI inMediaStore.

    What you can do is have a fallback once the ContentResolver couldn't find your excel file, by fetching it by it's absolute path.

    Android.Net.Uri? fileUri = null;
     if (cursor?.MoveToFirst() == true)
     {
       ...
       fileUri = ContentUris.WithAppendedId(MediaStore.Downloads.ExternalContentUri, id);
     }
     else
     {
       //fallback
       var directPath = Path.Combine(Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).AbsolutePath, fileName);
       fileUri = Android.Net.Uri.FromFile(new Java.IO.File(directPath));
    }
    // start your intent with the fileuri