【问题标题】:Android: Scoped Storage: getContentResolver().update(...): COLUMN_LAST_MODIFIED: UnsupportedOperationException: Update not supportedAndroid:范围存储:getContentResolver().update(...):COLUMN_LAST_MODIFIED:UnsupportedOperationException:不支持更新
【发布时间】:2020-08-19 21:57:31
【问题描述】:

我正在尝试更新文档/文件的最后修改日期,但我收到“UnsupportedOperationException:不支持更新”

重现步骤:

  1. 选择文档树
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. 在选择的目录中创建一个新文档的活动结果:
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. 尝试更新文档/文件的最后修改日期
ContentValues values = new ContentValues();
values.put(DocumentsContract.Document.COLUMN_LAST_MODIFIED, 1592143965000L);
getContentResolver().update(uri, values, null, null);

也尝试插入,但结果始终相同:

 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)

有没有人遇到同样的问题,各自找到了解决这个问题的方法?

【问题讨论】:

  • 我认为当您修改文档内容时,该值应该由文档提供者处理。您是否遇到了提供者不自行更新此值的问题?
  • 感谢您的反馈。我已经实现了SFTP Server,并且在上传文件时我想保留时间戳(在某些情况下)。使用良好的旧 java.io.File 我可以设置最后修改的时间戳,但最近我必须/必须实现范围存储,并且我遇到了无法找到如何设置最后修改的时间戳的问题。
  • 是的,我不认为范围存储真的是为此而设计的。最简单的思考方式:使用DocumentsContract,就好像您正在与设备上的文件服务器交谈。文件服务器将负责维护这样的时间戳,并且很可能不会公开任何“将时间戳设置为任意值”API。
  • 嗯,我认为 Scoped Storage 在这种情况下设计得并不好。并将其视为文件服务器,如果设置时间戳真的很危险,那么文件服务器可能会要求提升权限。我见过很多文件服务器和文件系统,范围存储确实不是一个好的解决方案。除此之外,我还需要重写我的 java.io.File 和 java.io.RandomAccessFile 代码,如果没有 Google 方面的太多指导/支持,这很痛苦。抱歉有点跑题了。
  • “那么文件服务器可以要求提升权限”——在我的类比中,您的应用程序不是文件服务器。 DocumentsProvider 是文件服务器。它有权将上次修改的时间戳调整为任意值,但它可能不向客户端提供这种能力。

标签: android scoped-storage


【解决方案1】:

您提到了范围存储。我正在寻找一种方法来更新通过存储访问框架复制/创建的文件的某些属性,例如创建日期、修改日期...。 这也需要使用DocumentsContractDocumentFiles

根据doc所有列对客户端应用程序都是只读的

但是,我为更新使用DocumentsContract.createDocument 创建的文件的属性所做的是将 Uri 转换为文件系统上的路径。然后以这种方式直接更新这些文件的属性:


// 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 类(改编自:此 PullRequest https://github.com/nzbget/android/pull/12/files 也引用了 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;
    }
}

2021-03-30 编辑

如果您的 targetSDK >= 29,则在设置时间戳时可能会遇到“AccessDeniedException”。为避免这种情况,您可以查看以下总结的解决方案here on stackoverflow

  • 对于 Android 10 支持,将此 android:requestLegacyExternalStorage="true" 放入清单中。
  • 对于 Android 11,您需要 MANAGE_EXTERNAL_STORAGE 权限和 Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION(一些代码示例在 a github commit

【讨论】:

  • 顺便说一句,以这种方式设置属性需要 WRITE_EXTERNAL_STORAGE 权限(来自 ACTION_OPEN_DOCUMENT_TREE 的权限是不够的)。此外,它还允许将它们设置在辅助外部存储上(通常是作为共享存储安装的外部 sdcard),否则将无法写入 [取决于 android 的版本]。
  • 感谢您的反馈,但不幸的是,您的解决方法只能部分解决我的问题。在我的 [SFTP Server s0 v1] (play.google.com/store/apps/…) 应用程序中,我使用“java.io.*”通过 SFTP 协议远程访问 android 设备上的文件,但不幸的是我没有对某些外部存储的写入权限,因此我实现了一个挂载功能,它实际上是 DocumentContract 之上的 java.io.* 文件包装器,它使我能够写入外部存储,但(正如我所写)不幸的是,我无法更新上次修改日期。
  • 我的应用程序具有 WRITE_EXTERNAL_STORAGE 权限,但如果将 SD 卡配置为“便携式存储”,则无法写入 SD 卡。你可以重现这个:例如使用系统映像创建一个模拟器/虚拟设备:发布名称“Q”、API 级别“29”、ABI“x86”、目标“Android 10.0 (Google Play)”。首次启动时打开通知菜单,点击“虚拟 SD 卡,点击设置”,在“您将如何使用此 SD 卡?”对话框中,点击“用于便携式存储”。现在您只能使用 DocumentsContract 而不能使用 java.io.* 写入设备。在这种情况下,您的解决方案将不起作用。
  • 我又试了一次,它确实有效(模拟器 3A XL、API29、android 10、便携式存储)我既有 WRITE_EXTERNAL_STORAGE 权限,也授予了使用 ACTION_OPEN_DOCUMENT_TREE 选择的目录的持久权限。也许还有另一个原因使它对我有用,而不是对你有用,我远不是 android 方面的专家,只是想提供帮助。 :)
  • 也适用于运行 android 11 的同一模拟设备。
【解决方案2】:

在过去的 3 天里,我在 SAF 主题中汗流浃背。我的努力是将文件从 FTP 下载到 Android API28 上的外部 SD 卡)。我成功了,但是遇到了同样的问题,我无法将文件的 lastmodify 日期修改为 FTP 上的日期。我尝试了与上述相同的方法。 有趣的是,我用旧的 java 文件 utils 方式尝试了它

file.setLastModified(l_lastModified);

令人惊讶的是,这有效!你不能用java文件工具写文件,你必须使用SAF,然后一旦文件下载得足够奇怪,你就可以修改这个文件的属性,这完全没有意义。然而,这解决了我的问题并且它有效。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-09-29
    • 2021-01-02
    • 2012-10-30
    • 2020-08-31
    • 1970-01-01
    • 2016-09-01
    • 1970-01-01
    • 2021-06-03
    相关资源
    最近更新 更多