Android: Scoped Storage: getContentResolver().update(...): COLUMN_LAST_MODIFIED: UnsupportedOperationException: Update not supported

Viewed 960

I'm trying to update the last modified date of a document/file but I'm getting an "UnsupportedOperationException: Update not supported"

Steps to reproduce:

  1. Picking a document tree
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, 1972);
  1. On Activity Result creating a new document inside the picked directory:
Uri treeUri = resultData.getData();
String treeDocumentId = DocumentsContract.getTreeDocumentId(treeUri);
treeUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, treeDocumentId);
Uri uri = DocumentsContract.createDocument(getContentResolver(), treeUri, "text/plain", "test.txt");
  1. Trying to update the last modified date of the document/file
ContentValues values = new ContentValues();
values.put(DocumentsContract.Document.COLUMN_LAST_MODIFIED, 1592143965000L);
getContentResolver().update(uri, values, null, null);

Tried as well to insert but the result is always the same:

 Caused by: java.lang.UnsupportedOperationException: Update not supported
    at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:172)
    at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:140)
    at android.content.ContentProviderProxy.update(ContentProviderNative.java:578)
    at android.content.ContentResolver.update(ContentResolver.java:2009)

Did anyone experience the same issue respective found a solution for this problem?

2 Answers

You mention scoped storage. I was looking for a way to update some attributes like creation date, modificatio date... of files copied/created through the Storage Access Framework. This requires also the uses of DocumentsContract, DocumentFiles

According to the doc, All columns are read-only to client applications

However, what I did to update the attributes of the files I created with DocumentsContract.createDocument was to convert the Uri to a path on the filesystem. And then to directly update the attributes on these files this way:


// sourceUri & targetUri reference tree Uris

Path inFilePath = Paths.get(FileUtil.getFullDocIdPathFromTreeUri(sourceUri, context));
Path outFilePath = Paths.get(FileUtil.getFullDocIdPathFromTreeUri(targetUri, context));
BasicFileAttributes inAttrs = Files.readAttributes(inFilePath, BasicFileAttributes.class);
Files.getFileAttributeView(outFilePath, BasicFileAttributeView.class).setTimes(inAttrs.lastModifiedTime(), inAttrs.lastAccessTime(), inAttrs.creationTime());

FileUtil class (adapted from: this PullRequest https://github.com/nzbget/android/pull/12/files which references also https://stackoverflow.com/a/36162691)

package com......;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract;

import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;

import java.io.File;
import java.io.FileNotFoundException;
import java.lang.reflect.Array;
import java.lang.reflect.Method;

public final class FileUtil {
    private static final String PRIMARY_VOLUME_NAME = "primary";

    public static String getFullDocIdPathFromTreeUri(@Nullable final Uri treeUri, Context con) {
        if (treeUri == null) return null;
        String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri),con);
        if (volumePath == null) return File.separator;
        if (volumePath.endsWith(File.separator))
            volumePath = volumePath.substring(0, volumePath.length() - 1);

        String documentPath = getDocumentPathFromTreeUri(treeUri);
        if (documentPath.endsWith(File.separator))
            documentPath = documentPath.substring(0, documentPath.length() - 1);

        if (documentPath.length() > 0) {
            if (documentPath.startsWith(File.separator))
                return volumePath + documentPath;
            else
                return volumePath + File.separator + documentPath;
        }
        else return volumePath;
    }

    @SuppressLint("ObsoleteSdkInt")
    private static String getVolumePath(final String volumeId, Context context) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null;
        try {
            StorageManager mStorageManager =
                    (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
            Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
            Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
            Method getUuid = storageVolumeClazz.getMethod("getUuid");
            Method getPath = storageVolumeClazz.getMethod("getPath");
            Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
            Object result = getVolumeList.invoke(mStorageManager);

            final int length = Array.getLength(result);
            for (int i = 0; i < length; i++) {
                Object storageVolumeElement = Array.get(result, i);
                String uuid = (String) getUuid.invoke(storageVolumeElement);
                Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);

                // primary volume?
                if (primary && PRIMARY_VOLUME_NAME.equals(volumeId))
                    return (String) getPath.invoke(storageVolumeElement);

                // other volumes?
                if (uuid != null && uuid.equals(volumeId))
                    return (String) getPath.invoke(storageVolumeElement);
            }
            // not found.
            return null;
        } catch (Exception ex) {
            return null;
        }
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static String getVolumeIdFromTreeUri(final Uri treeUri) {
        final String docId = DocumentsContract.getTreeDocumentId(treeUri);
        final String[] split = docId.split(":");
        if (split.length > 0) return split[0];
        else return null;
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP) 
        private static String getDocumentPathFromTreeUri(final Uri treeUri) {
        //final String docId = DocumentsContract.getTreeDocumentId(treeUri);
        final String docId = DocumentsContract.getDocumentId(treeUri);
        final String[] split = docId.split(":");
        if ((split.length >= 2) && (split[1] != null)) return split[1];
        else return File.separator;
    }
}

Edit of 2021-03-30

If you have targetSDK >= 29, you might encounter an "AccessDeniedException" while setting the timestamps. To avoid this, you may have a look at the solutions here on stackoverflow summarized below:

  • For Android 10 support, put this android:requestLegacyExternalStorage="true" in the Manifest.
  • For Android 11, you need MANAGE_EXTERNAL_STORAGE permission and Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION (some code example is in a github commit

I am sweating through the SAF subject for the last 3 days. My effort was to download files form an FTP to the external SD Card on Android API28). I succeded doing this however was facing the same issue that I could not modify the lastmodify date of the file to the date on the FTP. I tried the same way as above described. Funny enough I tried it the good old java file utils way with

file.setLastModified(l_lastModified);

and surprisingly this works! You cannot write file with java file utils, you have to use SAF, and afterwards once the fil eis downloaded strangely enough you can modify attibutes of this file which makes absolutely no sense. However this solved my problem and it works.

Related