一手遮天 Android - 存储: Android 11 通过 MediaStore 管理文件

项目地址 https://github.com/webabcd/AndroidDemo
作者 webabcd

一手遮天 Android - 存储: Android 11 通过 MediaStore 管理文件

示例如下:

/storage/Android11Demo2.java

/**
 * 本例用于演示 Android 11 通过 MediaStore 管理文件
 *
 * 通过 MediaStore 可以在指定的公共目录中管理文件,包括图片目录,视频目录,音频目录,下载目录
 *     文档目录等其他公共目录是不能通过 MediaStore 管理的
 * 通过 MediaStore 保存的数据,卸载 app 后不会被删除
 * 通过 MediaStore 保存或读取数据全部由程序管理,不需要用户选择(通过 Storage Access Framework 管理文件是需要用户选择的)
 * 通过 MediaStore 保存文件时要注意,系统是通过你指定的 mime 来判断文件类型的
 *     图片目录只能保存图片类型的文件
 *     视频目录只能保存视频类型的文件
 *     音频目录只能保存音频类型的文件
 *     下载目录可以保存任意类型的文件
 *
 * 注:
 * 1、如果你卸载 app 后再重装 app,则会失去卸载 app 之前通过 MediaStore 创建的文件的所有权
 *    对于你有所有权的文件可以通过 MediaStore 访问和删除
 *    对于你没有所有权的文件,不可以通过 MediaStore 删除,但是可以通过 MediaStore 访问(前提是先要申请 READ_EXTERNAL_STORAGE 权限)
 * 2、各种公共目录并不是物理文件夹,而是将其他相关文件夹中的相关文件集中管理
 *    比如视频目录会包括 DCIM 文件夹, Movies 文件夹, Pictures 文件夹中的视频文件
 */

package com.webabcd.androiddemo.storage;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

import com.webabcd.androiddemo.R;

import java.io.OutputStream;
import java.util.Locale;

public class Android11Demo2 extends AppCompatActivity {

    private final String LOG_TAG = "Android11Demo2";

    private Button _button0;
    private Button _button1;
    private Button _button2;
    private Button _button3;
    private Button _button4;
    private ImageView _imageView1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_storage_android11demo2);

        _button0 = findViewById(R.id.button0);
        _button1 = findViewById(R.id.button1);
        _button2 = findViewById(R.id.button2);
        _button3 = findViewById(R.id.button3);
        _button4 = findViewById(R.id.button4);
        _imageView1 = findViewById(R.id.imageView1);

        sample();
    }

    private void sample() {
        _button0.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                requestPermission();
            }
        });

        _button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                createFile();
            }
        });

        _button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                queryFileFirst();
            }
        });

        _button3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                showFile();
            }
        });

        _button4.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                deleteFileFirst();
            }
        });
    }

    // 请求“通过 MediaStore 读取非本 app 创建的文件”的权限
    // 先在 AndroidManifest.xml 中配置 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    // 然后按照如下方式请求(注:WRITE_EXTERNAL_STORAGE 权限已经无效了)
    private void requestPermission() {
        String[] storagePermissions = { Manifest.permission.READ_EXTERNAL_STORAGE };
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, storagePermissions, 123);
        }
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);

        for (int i = 0; i < permissions.length; i++) {
            Log.i(LOG_TAG, "申请的权限为:" + permissions[i] + ",申请结果:" + grantResults[i]);
        }
    }

    // 通过 MediaStore 在图片目录新建一个图片文件
    private void createFile() {

        // 需要创建的图片对象
        BitmapDrawable bitmapDrawable = (BitmapDrawable) getResources().getDrawable(R.drawable.son01, null);
        Bitmap bitmap = bitmapDrawable.getBitmap();

        ContentValues contentValues = new ContentValues();
        // 指定文件保存的文件夹名称
        // Environment.DIRECTORY_PICTURES 值为 Pictures
        // Environment.DIRECTORY_DCIM 值为 DCIM
        // Environment.DIRECTORY_MOVIES 值为 Movies
        // Environment.DIRECTORY_MUSIC 值为 Music
        // Environment.DIRECTORY_DOWNLOADS 值为 Download
        // 如果想获取上述文件夹的真实地址可以通过这样的方式 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() 获取,他返回的值类似 /storage/emulated/0/Pictures
        contentValues.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/wanglei_test/");
        // 指定文件名
        contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, "son");
        // 指定文件的 mime(比如 image/jpeg, application/vnd.android.package-archive 之类的)
        contentValues.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/jpeg");
        contentValues.put(MediaStore.Images.ImageColumns.WIDTH, bitmap.getWidth());
        contentValues.put(MediaStore.Images.ImageColumns.HEIGHT, bitmap.getHeight());

        ContentResolver contentResolver = this.getContentResolver();
        // 通过 ContentResolver 在指定的公共目录下按照指定的 ContentValues 创建文件,会返回文件的 content uri(类似这样的地址 content://media/external/images/media/102)
        // 可以通过 MediaStore 保存文件的公共目录有:
        // MediaStore.Images.Media.EXTERNAL_CONTENT_URI - 图片目录,只能保存 mime 为图片类型的文件
        //     图片目录包括 Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_DCIM 文件夹
        // MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - 音频目录,只能保存 mime 为音频类型的文件
        //     音频目录包括 Environment.DIRECTORY_MUSIC, Environment.DIRECTORY_RINGTONES 等等文件夹
        // MediaStore.Video.Media.EXTERNAL_CONTENT_URI - 视频目录,只能保存 mime 为视频类型的文件
        //     视频目录包括 DIRECTORY_MOVIES, Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_DCIM 文件夹
        // MediaStore.Downloads.EXTERNAL_CONTENT_URI - 下载目录,可以保存任意类型的文件
        //     下载目录包括 Environment.DIRECTORY_DOWNLOADS 文件夹
        Uri uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);

        if (uri == null) {
            Log.d(LOG_TAG, "创建文件失败");
        } else {
            Log.d(LOG_TAG, "创建文件成功:" + uri.toString());
        }

        // 写入图片数据
        OutputStream outputStream = null;
        try {
            outputStream = contentResolver.openOutputStream(uri);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 85, outputStream);
        } catch (Exception ex) {
            Log.d(LOG_TAG, "写入数据失败:" + ex.toString());
        } finally {
            try {
                if (outputStream != null) {
                    outputStream.flush();
                    outputStream.close();
                }
            } catch (Exception ex) {

            }
        }
    }

    // 通过 MediaStore 在图片目录遍历图片文件
    // 注:
    // 1、如果你想遍历出非本 app 创建的文件,则需要先申请 READ_EXTERNAL_STORAGE 权限
    // 2、如果你的 app 卸载后再重装的话系统不会认为是同一个 app(也就是你卸载之前创建的文件,再次安装 app 后必须先申请 READ_EXTERNAL_STORAGE 权限后才能获取到)
    private int queryFileFirst() {

        int contentId = -1;

        // 通过 ContentResolver 遍历指定公共目录中的文件
        ContentResolver contentResolver = this.getContentResolver();
        // 第 2 个参数(projection):指定需要返回的字段,null 会返回所有字段
        // 第 3 个参数(selection):查询的 where 语句,类似 xxx = ?
        // 第 4 个参数(selectionArgs):查询的 where 语句的值,类似 new String[] { xxx }
        // 第 5 个参数(selectionArgs):排序语句,类似 xxx DESC
        Cursor cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);

        Log.d(LOG_TAG, "count:" + cursor.getCount());
        while (cursor.moveToNext()) {
            // 比如这个地址 content://media/external/images/media/102 它的后面的 102 就是它的 id
            int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
            // 获取文件名
            String title = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.TITLE));
            // 获取文件的真实路径,类似 /storage/emulated/0/Pictures/wanglei_test/son.jpg
            String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));

            Log.d(LOG_TAG, String.format("id:%d, title:%s, path:%s", id, title, path));
            contentId = id;
        }
        cursor.close();

        // 返回指定的公共目录中的第一个文件的 id
        return contentId;
    }

    // 通过 MediaStore 读取图片目录中的图片,并显示
    private void showFile() {
        // 先拿到需要显示的图片的 content uri(类似这样的地址 content://media/external/images/media/102)
        int contentId = queryFileFirst();
        Uri contentUri = Uri.parse(MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "/" + contentId);
        Log.d(LOG_TAG, "show uri:" + contentUri);

        Cursor cursor = null;
        try {
            String[] projection = { MediaStore.Images.Media.DATA };
            cursor = this.getContentResolver().query(contentUri, projection, null, null, null);
            cursor.moveToFirst();
            // 拿到指定的 content uri 的真实路径
            String path =  cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
            Log.d(LOG_TAG, String.format(Locale.ENGLISH, "contentUri:%s, path:%s", contentUri, path));

            // 显示指定路径的图片
            Bitmap bitmap = BitmapFactory.decodeFile(path);
            _imageView1.setImageBitmap(bitmap);
        } catch (Exception ex) {
            Log.e(LOG_TAG, ex.toString());
        }
        finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    // 通过 MediaStore 删除图片目录中的图片文件(只能删除本 app 创建的文件)
    // 注:如果你的 app 卸载后再重装的话系统不会认为是同一个 app(也就是你卸载之前创建的文件,再次安装 app 后是无法通过这种方式删除它的)
    private void deleteFileFirst() {

        try {
            // 先拿到需要删除的图片的 content uri(类似这样的地址 content://media/external/images/media/102)
            int contentId = queryFileFirst();
            Uri contentUri = Uri.parse(MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "/" + contentId);
            Log.d(LOG_TAG, "delete uri:" + contentUri);

            // 通过 ContentResolver 删除指定的 content uri 的文件
            ContentResolver contentResolver = this.getContentResolver();
            int deleteCount = contentResolver.delete(contentUri, null);
            Log.d(LOG_TAG, "delete count:" + deleteCount);
        } catch (Exception ex) {
            // 如果你想删除非本 app 创建的文件,就会收到类似这样的异常 android.app.RecoverableSecurityException: com.webabcd.androiddemo has no access to content://media/external/images/media/102
            Log.e(LOG_TAG, "delete error:" + ex.toString());
        }
    }
}

/layout/activity_storage_android11demo2.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:id="@+id/button0"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:text="请求“通过 MediaStore 读取非本 app 创建的文件”的权限" />

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:text="通过 MediaStore 在图片目录新建一个图片文件" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:text="通过 MediaStore 在图片目录遍历图片文件" />

    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:text="通过 MediaStore 读取图片目录中的图片,并显示" />

    <Button
        android:id="@+id/button4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:text="通过 MediaStore 删除图片目录中的图片文件" />

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="5dp" />

</LinearLayout>

项目地址 https://github.com/webabcd/AndroidDemo
作者 webabcd

posted @ 2021-09-22 10:21  webabcd  阅读(5669)  评论(0编辑  收藏  举报