Android 增删改查媒体商店 API 26+

Android CRUD MediaStore API 26+

我申请了 API26+

使用 Android 10 及以上 (API29+) 应该使用 MediaStore 来访问文件,而不是 Environment.getExternalStoragePublicDirectory

通常在创建 if-block 时会创建新方法

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
    // deprecated code
} else {
    // new approach
}

但是:

增删改查:

是否可以与 MediaStorage 和 API26+ 一起工作?

如果是,怎么样?许多属性首先添加在 API29

经过一些自己的研究和测试,我找到了同时使用 MediaStore 和旧设备的方法。

首先需要一些助手class:

使用FileType我们可以同时支持应用程序中的不同文件类型。

public enum FileType {

    File(Environment.DIRECTORY_DOCUMENTS, Constants.VERSION_29_ABOVE ? MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "text/plain"),
    Download(Environment.DIRECTORY_DOWNLOADS, Constants.VERSION_29_ABOVE ? MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "text/plain"),
    Image(Environment.DIRECTORY_PICTURES, Constants.VERSION_29_ABOVE ? MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "image/jpeg"),
    Audio(Environment.DIRECTORY_MUSIC, Constants.VERSION_29_ABOVE ? MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "audio/mpeg"),
    Video(Environment.DIRECTORY_MOVIES, Constants.VERSION_29_ABOVE ? MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL) : null, "video/mpeg");

    private final String directory;
    private final Uri contentUri;
    private String mimeType;

    FileType(String directory, Uri contentUri, String mimeType) {
        this.directory = directory;
        this.contentUri = contentUri;
        this.mimeType = mimeType;
    }

    public String getDirectory() {
        return directory;
    }

    public Uri getContentUri() {
        return contentUri;
    }

    public String getMimeType() {
        return mimeType;
    }

    public void setMimeType(String mimeType) {
        this.mimeType = mimeType;
    }
}

使用 setMimeType 我们可以更改当前文件扩展名并仍然使用其他设置

我们需要简单的回调 class 来获得不同的结果

public interface ObjectCallback<T> {

    void result(T object);
}

当我们想在文件中写入一些数据时,我们需要一个OutputStream

使用内部存储,获取文件的方式没有改变

/**
 * opens OutputStream to write data to file
 *
 * @param context    activity context
 * @param fileName   relative file name
 * @param fileType   file type to get folder specific values for access
 * @param fileStream callback with file output stream to requested file
 * @return true if output stream successful opened, false otherwise
 */
public boolean openOutputStream(Context context, String fileName, FileType fileType, ObjectCallback<OutputStream> fileStream) {
    File internalFolder = context.getExternalFilesDir(fileType.getDirectory());
    File absFileName = new File(internalFolder, fileName);

    try {
        FileOutputStream fOut = new FileOutputStream(absFileName);
        fileStream.result(fOut);
    } catch (Exception e) {
        e.printStackTrace();

        return false;
    }

    return true;
}

通过外部存储,在API 28-和API 29+上访问文件的方式是完全不同的。 在 API 28- 我们可以只使用一个文件。 在 API 29+ 我们应该首先检查文件是否已经存在。如果它(比如 text.txt)存在并且我们将创建 ContentResolver.insert,它将创建 text-1.txt,在最坏的情况下(无限循环)可能会快速填满整个智能手机存储空间。

/**
 * @param context  application context
 * @param fileName relative file name
 * @return uri to file or null if file not exist
 */
public Uri getFileUri(Context context, String fileName) {
    // remove folders from file name
    fileName = fileName.contains("/") ? fileName.substring(fileName.lastIndexOf("/") + 1) : fileName;

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
        // Deprecated in API 29
        File storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), Constants.PUBLIC_STORAGE_FOLDER);

        File file = new File(storageDir, fileName);

        return file.exists() ? Uri.fromFile(file) : null;
    } else {
        String folderName = Environment.DIRECTORY_PICTURES + File.separator + Constants.PUBLIC_STORAGE_FOLDER + File.separator;

        // get content resolver that can interact with public storage
        ContentResolver resolver = context.getContentResolver();

        String selection = MediaStore.MediaColumns.RELATIVE_PATH + "=?";
        String[] selectionArgs = new String[]{folderName};

        Cursor cursor = resolver.query(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL), null, selection, selectionArgs, null);

        Uri uri = null;

        if (cursor.getCount() > 0) {
            while (cursor.moveToNext()) {
                String itemFileName = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME));

                if (itemFileName.equals(fileName)) {
                    long id = cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns._ID));

                    uri = ContentUris.withAppendedId(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL), id);

                    break;
                }
            }
        }

        cursor.close();

        return uri;
    }
}

/**
 * opens OutputStream to write data to file
 *
 * @param context    activity context
 * @param fileName   relative file name
 * @param fileType   file type to get folder specific values for access
 * @param fileStream callback with file output stream to requested file
 * @return true if output stream successful opened, false otherwise
 */
public boolean openOutputStream(Context context, String fileName, FileType fileType, ObjectCallback<OutputStream> fileStream) {
    // remove folders from file name
    fileName = fileName.contains("/") ? fileName.substring(fileName.lastIndexOf("/") + 1) : fileName;

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
        File storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), Constants.PUBLIC_STORAGE_FOLDER);

        if (!storageDir.exists() && !storageDir.mkdir()) {
            // directory for file not exists and not created. return false
            return false;
        }

        File file = new File(storageDir, fileName);

        try {
            FileOutputStream fOut = new FileOutputStream(file);
            fileStream.result(fOut);
        } catch (Exception e) {
            e.printStackTrace();

            return false;
        }
    } else {
        // get content resolver that can interact with public storage
        ContentResolver resolver = context.getContentResolver();

        // always check first if file already exist
        Uri existFile = getFileUri(context, fileName);

        if (existFile == null) {
            ContentValues values = new ContentValues();
            values.put(MediaStore.MediaColumns.MIME_TYPE, fileType.getMimeType());
            // absolute folder name
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, fileType.getDirectory() + File.separator + Constants.PUBLIC_STORAGE_FOLDER);
            // file name
            values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);

            // create uri to the file. If folder not exists, it would be created automatically
            Uri uri = resolver.insert(fileType.getContentUri(), values);

            // open stream from uri and write data to file
            try (OutputStream outputStream = resolver.openOutputStream(uri)) {
                fileStream.result(outputStream);

                // end changing of file
                // when this point reached, no exception was thrown and file write was successful
                values.put(MediaStore.MediaColumns.IS_PENDING, false);
                resolver.update(uri, values, null, null);
            } catch (Exception e) {
                e.printStackTrace();

                if (uri != null) {
                    // Don't leave an orphan entry in the MediaStore
                    resolver.delete(uri, null, null);
                }

                return false;
            }
        } else {
            // open stream from uri and write data to file
            try (OutputStream outputStream = resolver.openOutputStream(existFile)) {
                fileStream.result(outputStream);
            } catch (Exception e) {
                e.printStackTrace();

                // Don't leave an orphan entry in the MediaStore
                resolver.delete(existFile, null, null);

                return false;
            }
        }
    }

    return true;
}

可以在所有设备和不同存储上以相同方式打开InputStream

要删除文件:

  • 内部仍然可以在每个 API
  • 上用 file.delete 删除
  • 具有 API29+ 的外部存储需要用户交互才能删除一些文件

文件提供者

要使用 Intent 显示一些图片、裁剪图片或其他任何东西,我们使用 setDataAndType

Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(photoUri, "image/*");
// allow to read the file
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

在某些情况下我们需要 FileProvider。特别地:

要显示使用该应用程序创建的外部文件,photoUri 看起来像这样

 Uri photoUri = Constants.VERSION_29_ABOVE ? uriToFile : 
      FileProvider.getUriForFile(context,
           context.getApplicationContext().getPackageName() + ".provider",
           new File(uriToFile.getPath()));