访问 public 下载文件 onActivityResult Android 28 Samsung Galaxy S9+ (Verizon)

Accessing public download files onActivityResult Android 28 Samsung Galaxy S9+ (Verizon)


UPDATE

我有一部三星 Galaxy S8+ 运行 8.0.0 T-Mobile,它在 运行 上运行良好 8.0.0

我的 Samsung Galaxy S9+ 运行 8.0.0 Verizon,它每次都因非法参数而失败。

我的 Samsung Galaxy S9+ 运行 8.0.0 T-Mobile 没有问题并且工作正常

所以这可能是 OEM 特定型号问题,但不是 确定如何修复它。我也试过重启 Phone,没有 结果的变化。

此外,我从 Evernote 中打开了 public 下载并保存了 文件作为笔记的附件,它告诉我 Evernote 能够 可以很好地访问 public 目录并附加文件,所以它是 可以在设备上做。让我相信这是代码 相关。


所以我最近升级了一个工作正常的项目,现在它有一个错误,因为它正在使用构建工具 28 编译最新版本的 Android。

所以我一直使用这个 PathUtil 从隐式意图中获取我需要的文件路径,以从用户那里获取文件选择。我将在下面分享一个我长期使用的代码link。

PathUtil

它只是一个实用程序 class,用于检查提供者权限并获取您尝试读取的文件的绝对路径。

当用户从 public 下载目录中选择文件时,它 returns 到 onActivityResult 并带有:

content://com.android.providers.downloads.documents/document/2025

现在 nice 实用程序解析了它并告诉我这是一个下载目录文件并且是一个 ID 为 2025 的文档。谢谢实用程序,这是一个很好的开始。

Next up is to use the content resolver to find the file absolute path. This is what used to work, but no longer does :(.

现在路径实用程序只使用他们最有可能从核心库中获得的合同数据。我试图导入提供程序 class 以避免静态字符串,但它似乎不可用,所以我想简单地使用匹配字符串是目前最好的方法。

这里是供参考的核心DownloadProvider,它为内容解析器提供了所有访问权限。 DownloadProvider

NOTE* This DownloadProvider is Androids, not mine

这是为 contentProvider 构建 Uri 的代码

 val id = DocumentsContract.getDocumentId(uri)
 val contentUri = ContentUris.withAppendedId(Uri.parse(PUBLIC_DOWNLOAD_PATH), id.toLong())
 return getDataColumn(context, contentUri, null, null)

调用参考:

    private fun getDataColumn(context: Context, uri: Uri, selection: String?, selectionArgs: Array<String>?): String? {
        var cursor: Cursor? = null
        val column = "_data"
        val projection = arrayOf(column)
        try {
            cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null)
            if (cursor != null && cursor.moveToFirst()) {
                val column_index = cursor.getColumnIndexOrThrow(column)
                return cursor.getString(column_index)
            }
        }catch (ex: Exception){
            A35Log.e("PathUtils", "Error getting uri for cursor to read file: ${ex.message}")
        } finally {
            if (cursor != null)
                cursor.close()
        }
        return null
    }

基本上要解析的 contentUri 最终是

content://downloads/public_downloads/2025

然后当你调用它抛出的查询方法时:

java.lang.IllegalArgumentException: Unknown URI: content://downloads/public_downloads/2025

我已经确认或尝试过的事情

  1. 读取外部权限(写入时附带,但无论如何都做了)
  2. 写入外部权限
  3. 权限在清单中并在运行时检索
  4. 我选择了多个不同的文件,看看有没有一个很奇怪
  5. 我已确认在应用程序设置中授予了权限
  6. 我已经将 Uri 硬编码为 /1 甚至最后的 /#2052 以尝试各种结尾类型
  7. 我研究了核心库上的 uriMatching 以寻找它期望如何格式化并确保它匹配
  8. 我试过 uri 中的 all_downloads 目录,它解决了!!但是有安全异常,所以解析器必须存在。

我不知道还能尝试什么,如有任何帮助,我们将不胜感激。

So I still have to do some backwards compatible testing, but I have successfully resolved my own problem after many hours of trial and error.

How I resolved it was to modify the isDownloadDirectory path flow of getPath. I don't know all the ripple effects yet though as QA will be starting on it tomorrow, i'll update if I learn anything new from this.

Use the direct URI to get the contentResolver for file name (NOTE* This is not a good way to get file name unless you are certain it is a local file according to Google, but for me, I am certain it is downloaded.)

Then next use the Environment external public download constants combined with the returned content resolver name to get your absolute path. The new code looks like this.

private val PUBLIC_DOWNLOAD_PATH = "content://downloads/public_downloads"
private val EXTERNAL_STORAGE_DOCUMENTS_PATH = "com.android.externalstorage.documents"
private val DOWNLOAD_DOCUMENTS_PATH = "com.android.providers.downloads.documents"
private val MEDIA_DOCUMENTS_PATH = "com.android.providers.media.documents"
private val PHOTO_CONTENTS_PATH = "com.google.android.apps.photos.content"

//HELPER METHODS
    private fun isExternalStorageDocument(uri: Uri): Boolean {
        return EXTERNAL_STORAGE_DOCUMENTS_PATH == uri.authority
    }
    private fun isDownloadsDocument(uri: Uri): Boolean {
        return DOWNLOAD_DOCUMENTS_PATH == uri.authority
    }
    private fun isMediaDocument(uri: Uri): Boolean {
        return MEDIA_DOCUMENTS_PATH == uri.authority
    }
    private fun isGooglePhotosUri(uri: Uri): Boolean {
        return PHOTO_CONTENTS_PATH == uri.authority
    }

 fun getPath(context: Context, uri: Uri): String? {
    if (DocumentsContract.isDocumentUri(context, uri)) {
        if (isExternalStorageDocument(uri)) {
            val docId = DocumentsContract.getDocumentId(uri)
            val split = docId.split(COLON.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
            val type = split[0]
            val storageDefinition: String
            if (PRIMARY_LABEL.equals(type, ignoreCase = true)) {
                return Environment.getExternalStorageDirectory().toString() + FORWARD_SLASH + split[1]
            } else {
                if (Environment.isExternalStorageRemovable()) {
                    storageDefinition = EXTERNAL_STORAGE
                } else {
                    storageDefinition = SECONDARY_STORAGE
                }
                return System.getenv(storageDefinition) + FORWARD_SLASH + split[1]
            }
        } else if (isDownloadsDocument(uri)) {
            //val id = DocumentsContract.getDocumentId(uri) //MAY HAVE TO USE FOR OLDER PHONES, HAVE TO TEST WITH REGRESSION MODELS
            //val contentUri = ContentUris.withAppendedId(Uri.parse(PUBLIC_DOWNLOAD_PATH), id.toLong()) //SAME NOTE AS ABOVE
            val fileName = getDataColumn(context, uri, null, null)
            var uriToReturn: String? = null
            if(fileName != null){
                uriToReturn = Uri.withAppendedPath(Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath), fileName).toString()
            }
            return uriToReturn
        } else if (isMediaDocument(uri)) {
            val docId = DocumentsContract.getDocumentId(uri)
            val split = docId.split(COLON.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
            val type = split[0]
            var contentUri: Uri? = null
            if (IMAGE_PATH == type) {
                contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
            } else if (VIDEO_PATH == type) {
                contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
            } else if (AUDIO_PATH == type) {
                contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
            }
            val selection = "_id=?"
            val selectionArgs = arrayOf(split[1])
            return getDataColumn(context, contentUri!!, selection, selectionArgs)
        }
    } else if (CONTENT.equals(uri.scheme, ignoreCase = true)) {
        return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(context, uri, null, null)
    } else if (FILE.equals(uri.scheme, ignoreCase = true)) {
        return uri.path
    }
    return null
}




    private fun getDataColumn(context: Context, uri: Uri, selection: String?, selectionArgs: Array<String>?): String? {
        var cursor: Cursor? = null
        //val column = "_data" REMOVED IN FAVOR OF NULL FOR ALL   
        //val projection = arrayOf(column) REMOVED IN FAVOR OF PROJECTION FOR ALL 
        try {
            cursor = context.contentResolver.query(uri, null, selection, selectionArgs, null)
            if (cursor != null && cursor.moveToFirst()) {
                val columnIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME) //_display_name
                return cursor.getString(columnIndex) //returns file name
            }
        }catch (ex: Exception){
            A35Log.e(SSGlobals.SEARCH_STRING + "PathUtils", "Error getting uri for cursor to read file: ${ex.message}")
        } finally {
            if (cursor != null)
                cursor.close()
        }
        return null
    }

我的解决方案不同 我试图获取路径,以便将文件复制到我的应用程序文件夹。 找不到答案后,我尝试了以下方法。 我使用 Xamarin 表单

           // DownloadsProvider
            else if (IsDownloadsDocument(uri))
            {
                if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
                {
                    //Hot fix for android oreo
                    bool res = MediaService.MoveAssetFromURI(uri, ctx, ref error);
                    return res ? "copied" : null;
                }
                else
                {
                    string id = DocumentsContract.GetDocumentId(uri);

                    Android.Net.Uri contentUri = ContentUris.WithAppendedId(
                                    Android.Net.Uri.Parse("content://downloads/public_downloads"), long.Parse(id));

                    //System.Diagnostics.Debug.WriteLine(contentUri.ToString());

                    return GetDataColumn(ctx, contentUri, null, null);
                }
            }

 public static bool MoveAssetFromURI(Android.Net.Uri uri, Context ctx, ref string error)
    {           
        string directory = PhotoApp.App.LastPictureFolder;

        var type = ctx.ContentResolver.GetType(uri);

        string assetName = FileName(ctx, uri);

        string extension = System.IO.Path.GetExtension(assetName);

        var filename = System.IO.Path.GetFileNameWithoutExtension(assetName);

        var finalPath = $"{directory}/{filename}{extension}";

        if (File.Exists(finalPath))
        {
            error = "File already exists at the destination";
            return false;
        }

        if (extension != ".pdf" && extension == ".jpg" && extension == ".png")
        {
            error = "File extension not suported";
            return false;
        }

        using (var input = ctx.ContentResolver.OpenInputStream(uri))
        {
            using (var fileStream = File.Create(finalPath))
            {
                //input.Seek(0, SeekOrigin.Begin);
                input.CopyTo(fileStream);
            }
        }

        if (extension == ".pdf")
        {
            var imagePDFIcon = BitmapFactory.DecodeResource(ctx.Resources, Resource.Drawable.icon_pdf);

            var imagePDFPortrait = BitmapFactory.DecodeResource(ctx.Resources, Resource.Drawable.pdf_image);

            using (var stream = new FileStream($"{directory}/{filename}", FileMode.Create))
            {
                imagePDFIcon.Compress(Bitmap.CompressFormat.Jpeg, 90, stream);
            }

            using (var stream = new FileStream($"{directory}/{filename}.jpg", FileMode.Create))
            {
                imagePDFPortrait.Compress(Bitmap.CompressFormat.Jpeg, 90, stream);
            }

            return true;
        }
        else
        {
            if (extension == ".jpg" || extension == ".png")
            {
                MoveImageFromGallery(finalPath);

                File.Delete(finalPath);

                return true;
            }
        }

        return false;

因此,我没有尝试获取路径,而是创建了一个输入流并将该流复制到我想要的位置。 希望这有帮助

感谢山姆。

@Sam 在 java 中的回答帮助了我。

import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Switch;

import java.io.File;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.ListIterator;
import java.util.Objects;

import kotlin.Metadata;
import kotlin.collections.CollectionsKt;
import kotlin.jvm.internal.Intrinsics;
import kotlin.text.Regex;
import kotlin.text.StringsKt;

public class UtilsFile {


    private final static String PUBLIC_DOWNLOAD_PATH = "content://downloads/public_downloads";


    private final static String EXTERNAL_STORAGE_DOCUMENTS_PATH = "com.android.externalstorage.documents";


    private final static String DOWNLOAD_DOCUMENTS_PATH = "com.android.providers.downloads.documents";


    private final static String MEDIA_DOCUMENTS_PATH = "com.android.providers.media.documents";


    private final static String PHOTO_CONTENTS_PATH = "com.google.android.apps.photos.content";


    private Boolean isExternalStorageDocument(Uri uri) {
        return EXTERNAL_STORAGE_DOCUMENTS_PATH.equals(uri.getAuthority());

    }
 private Boolean isPublicDocument(Uri uri) {
        return PUBLIC_DOWNLOAD_PATH.equals(uri.getAuthority());

    }


    private Boolean isDownloadsDocument(Uri uri) {
        return DOWNLOAD_DOCUMENTS_PATH.equals(uri.getAuthority());

    }

    private Boolean isMediaDocument(Uri uri) {
        return MEDIA_DOCUMENTS_PATH.equals(uri.getAuthority());
    }


    private Boolean isGooglePhotosUri(Uri uri) {
        return MEDIA_DOCUMENTS_PATH.equals(uri.getAuthority());

    }
 private Boolean isPhotoContentUri(Uri uri) {
        return PHOTO_CONTENTS_PATH.equals(uri.getAuthority());

    }



    private String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {

        Cursor cursor = null;
        //String column = "_data" REMOVED IN FAVOR OF NULL FOR ALL
        //String projection = arrayOf(column) REMOVED IN FAVOR OF PROJECTION FOR ALL
        try {
            cursor = context.getContentResolver().query(uri, null, selection, selectionArgs, null);
            if (cursor != null && cursor.moveToFirst()) {
                int columnIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME);
                return cursor.getString(columnIndex);
            }
        } catch (Exception e) {
            Log.e("PathUtils", "Error getting uri for cursor to read file: " + e.getMessage());
        } finally {
            assert cursor != null;
            cursor.close();
        }
        return null;

    }

    public  String getFullPathFromContentUri(final Context context, final Uri uri) {

        final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
        String filePath="";
        // DocumentProvider
        if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
            // ExternalStorageProvider
            if (isExternalStorageDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                if ("primary".equalsIgnoreCase(type)) {
                    return Environment.getExternalStorageDirectory() + "/" + split[1];
                }//non-primary e.g sd card
                else {
                    if (Build.VERSION.SDK_INT > 20) {
                        //getExternalMediaDirs() added in API 21
                        File[] extenal = context.getExternalMediaDirs();
                        for (File f : extenal) {
                            filePath = f.getAbsolutePath();
                            if (filePath.contains(type)) {
                                int endIndex = filePath.indexOf("Android");
                                filePath = filePath.substring(0, endIndex) + split[1];
                            }
                        }
                    }else{
                        filePath = "/storage/" + type + "/" + split[1];
                    }
                    return filePath;
                }
            }
            // DownloadsProvider
            else if (isDownloadsDocument(uri)) {
                String fileName = getDataColumn(context,  uri,null, null);
                String uriToReturn = null;
                if (fileName != null) {
                    uriToReturn = Uri.withAppendedPath(
                            Uri.parse(
                                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath()), fileName
                    ).toString();
                }
                return uriToReturn;
            }
            // MediaProvider
            else if (isMediaDocument(uri)) {
                final String docId = DocumentsContract.getDocumentId(uri);
                final String[] split = docId.split(":");
                final String type = split[0];

                Uri contentUri = null;
                if ("image".equals(type)) {
                    contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
                } else if ("video".equals(type)) {
                    contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
                } else if ("audio".equals(type)) {
                    contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
                }

                final String selection = "_id=?";
                final String[] selectionArgs = new String[]{
                        split[1]
                };

                Cursor cursor = null;
                final String column = "_data";
                final String[] projection = {
                        column
                };

                try {
                    cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                            null);
                    if (cursor != null && cursor.moveToFirst()) {
                        final int column_index = cursor.getColumnIndexOrThrow(column);
                        return cursor.getString(column_index);
                    }
                } finally {
                    if (cursor != null)
                        cursor.close();
                }
                return null;
            }

        }
        // MediaStore (and general)
        else if ("content".equalsIgnoreCase(uri.getScheme())) {
            return getDataColumn(context, uri, null, null);
        }
        // File
        else if ("file".equalsIgnoreCase(uri.getScheme())) {
            return uri.getPath();
        }
        else if (isPublicDocument(uri)){
            String id = DocumentsContract.getDocumentId(uri);
            final Uri contentUri = ContentUris.withAppendedId(
                    Uri.parse(PUBLIC_DOWNLOAD_PATH), Long.parseLong(id));
            String[] projection = {MediaStore.Images.Media.DATA};
            @SuppressLint("Recycle") Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, null);

            if (cursor != null && cursor.moveToFirst()) {
                int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
                cursor.moveToFirst();
                return cursor.getString(column_index);
            }
        }

        return null;
    }

}