2

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?

becke-ch
  • 413
  • 4
  • 8
  • I think that value is supposed to be handled by the documents provider, when you modify the document content. Are you running into problems where the provider is not updating this value on its own? – CommonsWare Aug 19 '20 at 22:39
  • Thank you for your feedback. I've implemented an [SFTP Server](https://play.google.com/store/apps/details?id=ch.becke.sftp_server__s0_v1) and when uploading files I want to preserve the timestamp (in certain situations). With good old java.io.File I could set the last-modified timestamp but recently I have/had to implement scoped storage and I have issues respective could not find out how to set the last modified time-stamp. – becke-ch Aug 20 '20 at 17:31
  • Yeah, I don't think scoped storage is really designed for that. The simplest way to think of it: with `DocumentsContract`, it is as if you are talking to an on-device file server. The file server would be responsible for maintaining timestamps like that and might well not expose any "set the timestamp to an arbitrary value" API. – CommonsWare Aug 20 '20 at 20:30
  • Hmmm I think Scoped Storage is really not well designed in this context. And thinking of it as a file server and if setting the time-stamp is really so dangerous then the file-server could ask for elevated privileges. I've seen many file-server and file-systems and scoped storage is really not a good solution. Besides it required for me to rewrite my java.io.File and java.io.RandomAccessFile code which was painful without much guidance/support from Google side. Sorry getting a little bit off topic. – becke-ch Aug 21 '20 at 13:09
  • "then the file-server could ask for elevated privileges" -- in my analogy, your app is not the file server. The `DocumentsProvider` is the file server. It has the rights to adjust last-modified timestamps to arbitrary values, but it might not offer that ability to clients. – CommonsWare Aug 21 '20 at 13:18
  • Is there any chance that the `DocumentsProvider` implementation will be improved? I think it is ok that the default behavior is that the time-stamp is set by the `DocumentsProvider` but it has to offer the possibility that the time-stamp can be set by the application. (Most file-server application will as well have this issue when moving files accross document providers i.e. moving within the same document provider preserves the time stamp but moving between different document providers is not possible and time stamp will be lost) – becke-ch Aug 21 '20 at 23:02
  • "Is there any chance that the DocumentsProvider implementation will be improved?" -- to allow clients to set arbitrary last-modified dates? I doubt it. You're certainly welcome to file a feature request, but what you want runs counter to Google's tendencies nowadays. "but it has to offer the possibility that the time-stamp can be set by the application" -- you are assuming a certain implementation. My guess is that they expect your app to *be* a `DocumentsProvider`, just as Google Drive is. – CommonsWare Aug 21 '20 at 23:09

2 Answers2

1

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
usilo
  • 305
  • 2
  • 10
  • BTW, setting the attributes this way requires the WRITE_EXTERNAL_STORAGE persmission (permissions from ACTION_OPEN_DOCUMENT_TREE are not sufficient). Moreover, it permits to set them also on secondary external storage (typically external sdcard mounted as shared storage), which would otherwise not be writable [depending on the version of android]. – usilo Mar 19 '21 at 21:30
  • thank you for your feedback but unfortunately your workaround solves my problem only partially. In my [SFTP Server s0 v1] (https://play.google.com/store/apps/details?id=ch.becke.sftp_server__s0_v1) app I remote access via the SFTP Protocol the files on the android device using "java.io.*" but unfortunately I don't have write access to certain external storage and therefore I implemented a mount feature which is actually a java.io.* file wrapper on top of DocumentContract which enables me to write to external storage but (as I wrote) unfortunately I cannot update last modified date. – becke-ch Mar 21 '21 at 21:44
  • my app has WRITE_EXTERNAL_STORAGE permission but write to SD Cards is not working, if configured SD Card as "portable storage". You can reproduce this: E.g. create an emulator/virtual-device with system image: Release Name "Q", API Level "29", ABI "x86", Target "Android 10.0 (Google Play)". On first startup open the notification menu, click on "Virtual SD Card, Tap to set up", in the dialog "How will you use this SD card?", click on "Use for portable storage". Now you can only write to the device using DocumentsContract and not with java.io.* . In this case your solution won't work. – becke-ch Mar 21 '21 at 21:44
  • I just tried again and it does work (emulator 3A XL, API29, android 10, portable storage) I have both WRITE_EXTERNAL_STORAGE permission and also granted persistant permission to the dir selected with ACTION_OPEN_DOCUMENT_TREE. Maybe there is another reason that makes it work for me and not for you, I'm far from an expert on the android thing, just wanted to help. :) – usilo Mar 22 '21 at 10:47
  • works also on same emulated device running android 11. – usilo Mar 22 '21 at 11:25
  • you were right and I was wrong :-). Actually even I cannot create a new file (getting IOException: Permission denied), I can still update the attributes of an existing file (which I think is strange). The code you're referring seems a little bit shaky i.e. the internal formatting and reflection used might change in future and the code might break. I had to make a small change to the code to make it work: method: "getDocumentPathFromTreeUri(...)": Used "getDocumentId(...)" (and not "getTreeDocumentId(...)"). But besides that it is working and I would mark it as accepted answer if you agree. – becke-ch Mar 22 '21 at 22:27
  • Oh I just noticed I missed part of the class. Could you suggest an update? – usilo Mar 23 '21 at 11:28
  • the missing part/method in the code is: @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; } – becke-ch Mar 23 '21 at 18:40
  • Thanks I added it :) – usilo Mar 24 '21 at 19:44
  • and last but not least one more thing: The invocation is "getDocumentPathFromTreeUri(treeUri)" and not "getDocumentIdFromTreeUri(treeUri)" (I changed the method name because I took over most of the code from the link you mentioned: "https://github.com/nzbget/android/pull/12/files" except for one change I already mentioned: Used "getDocumentId(...)" (and not "getTreeDocumentId(...)"). – becke-ch Mar 24 '21 at 20:19
  • I've finished my unit tests and they failed for Android 11 - it looks that your code is still incomplete. I could fix it by looking at the article you've referenced: "https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri" and implementing the method "getVolumePathForAndroid11AndAbove(...)" – becke-ch Apr 04 '21 at 17:36
  • Ok, would you edit the answer with what's missing then ? Thanks – usilo Apr 07 '21 at 05:49
0

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.

Wolfgang
  • 21
  • 1