Search code examples
androidscoped-storage

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


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?


Solution

  • 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