Android Q(10) 文件存储适配

  • Post author:
  • Post category:其他



Android Q官方文档


Android Q 分区存储

  1. Android Q文件存储机制修改成了沙盒模式,和IOS神似
  2. 应用只能访问自己沙盒下的文件和公共媒体文件
  3. 对于Android Q以下,还是使用老的文件存储方式



权限

Android Q不再需要申请文件读写权限,默认可以读写自己沙盒文件和公共媒体文件。所以,Q以上不需要再动态申请文件读写权限。



沙盒存储/读写



获取沙盒指定文件夹

//废弃方法
//不再用以下代码获取文件根目录了
Environment.getExternalStorageDirectory();
Environment.getExternalStoragePublicDirectory();

//获取沙盒下的文件目录
//沙盒下的图片文件夹
File filePictures = getExternalFilesDir(Environment.DIRECTORY_PICTURES);

//Environment下的文件夹名称常量
//只需要调用,不需要创建,如果手机中没有对应的文件夹,则系统会自动生成
//以下为源码中的各个文件夹名称描述
 /**
     * Standard directory in which to place any audio files that should be
     * in the regular list of music for the user.
     * This may be combined with
     * {@link #DIRECTORY_PODCASTS}, {@link #DIRECTORY_NOTIFICATIONS},
     * {@link #DIRECTORY_ALARMS}, and {@link #DIRECTORY_RINGTONES} as a series
     * of directories to categories a particular audio file as more than one
     * type.
     */
    public static String DIRECTORY_MUSIC = "Music";

    /**
     * Standard directory in which to place any audio files that should be
     * in the list of podcasts that the user can select (not as regular
     * music).
     * This may be combined with {@link #DIRECTORY_MUSIC},
     * {@link #DIRECTORY_NOTIFICATIONS},
     * {@link #DIRECTORY_ALARMS}, and {@link #DIRECTORY_RINGTONES} as a series
     * of directories to categories a particular audio file as more than one
     * type.
     */
    public static String DIRECTORY_PODCASTS = "Podcasts";

    /**
     * Standard directory in which to place any audio files that should be
     * in the list of ringtones that the user can select (not as regular
     * music).
     * This may be combined with {@link #DIRECTORY_MUSIC},
     * {@link #DIRECTORY_PODCASTS}, {@link #DIRECTORY_NOTIFICATIONS}, and
     * {@link #DIRECTORY_ALARMS} as a series
     * of directories to categories a particular audio file as more than one
     * type.
     */
    public static String DIRECTORY_RINGTONES = "Ringtones";

    /**
     * Standard directory in which to place any audio files that should be
     * in the list of alarms that the user can select (not as regular
     * music).
     * This may be combined with {@link #DIRECTORY_MUSIC},
     * {@link #DIRECTORY_PODCASTS}, {@link #DIRECTORY_NOTIFICATIONS},
     * and {@link #DIRECTORY_RINGTONES} as a series
     * of directories to categories a particular audio file as more than one
     * type.
     */
    public static String DIRECTORY_ALARMS = "Alarms";

    /**
     * Standard directory in which to place any audio files that should be
     * in the list of notifications that the user can select (not as regular
     * music).
     * This may be combined with {@link #DIRECTORY_MUSIC},
     * {@link #DIRECTORY_PODCASTS},
     * {@link #DIRECTORY_ALARMS}, and {@link #DIRECTORY_RINGTONES} as a series
     * of directories to categories a particular audio file as more than one
     * type.
     */
    public static String DIRECTORY_NOTIFICATIONS = "Notifications";

    /**
     * Standard directory in which to place pictures that are available to
     * the user.  Note that this is primarily a convention for the top-level
     * public directory, as the media scanner will find and collect pictures
     * in any directory.
     */
    public static String DIRECTORY_PICTURES = "Pictures";

    /**
     * Standard directory in which to place movies that are available to
     * the user.  Note that this is primarily a convention for the top-level
     * public directory, as the media scanner will find and collect movies
     * in any directory.
     */
    public static String DIRECTORY_MOVIES = "Movies";

    /**
     * Standard directory in which to place files that have been downloaded by
     * the user.  Note that this is primarily a convention for the top-level
     * public directory, you are free to download files anywhere in your own
     * private directories.  Also note that though the constant here is
     * named DIRECTORY_DOWNLOADS (plural), the actual file name is non-plural for
     * backwards compatibility reasons.
     */
    public static String DIRECTORY_DOWNLOADS = "Download";

    /**
     * The traditional location for pictures and videos when mounting the
     * device as a camera.  Note that this is primarily a convention for the
     * top-level public directory, as this convention makes no sense elsewhere.
     */
    public static String DIRECTORY_DCIM = "DCIM";

    /**
     * Standard directory in which to place documents that have been created by
     * the user.
     */
    public static String DIRECTORY_DOCUMENTS = "Documents";

    /**
     * Standard directory in which to place screenshots that have been taken by
     * the user. Typically used as a secondary directory under
     * {@link #DIRECTORY_PICTURES}.
     */
    public static String DIRECTORY_SCREENSHOTS = "Screenshots";

    /**
     * Standard directory in which to place any audio files which are
     * audiobooks.
     */
    public static String DIRECTORY_AUDIOBOOKS = "Audiobooks";



沙盒里新建文件夹和新建文件

private String signImage = "signImage";
//将文件保存到沙盒中
//注意:
//1. 这里的文件操作不再需要申请权限
//2. 沙盒中新建文件夹只能再系统指定的子文件夹中新建
public void saveSignImageBox(String fileName, Bitmap bitmap) {
    try {
        //图片沙盒文件夹
        File PICTURES = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
        File imageFileDirctory = new File(PICTURES + "/" + signImage);
        if (imageFileDirctory.exists()) {
            File imageFile = new File(PICTURES + "/" + signImage + "/" + fileName);
            FileOutputStream fileOutputStream = new FileOutputStream(imageFile);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream);
            fileOutputStream.flush();
            fileOutputStream.close();
        } else if (imageFileDirctory.mkdir()) {//如果该文件夹不存在,则新建
            //new一个文件
            File imageFile = new File(PICTURES + "/" + signImage + "/" + fileName);
            //通过流将图片写入
            FileOutputStream fileOutputStream = new FileOutputStream(imageFile);
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fileOutputStream);
            fileOutputStream.flush();
            fileOutputStream.close();
        }
    } catch (Exception e) {
    }
}



沙盒中查询指定文件

//查询沙盒中的指定图片
//先指定哪个沙盒子文件夹,再指定名称
public Bitmap querySignImageBox(String environmentType,String fileName) {
    if (TextUtils.isEmpty(fileName)) return null;
    Bitmap bitmap = null;
    try {
        //指定沙盒文件夹
        File picturesFile = getExternalFilesDir(environmentType);
        if (picturesFile != null && picturesFile.exists() && picturesFile.isDirectory()) {
            File[] files = picturesFile.listFiles();
            if (files != null) {
                for (File file : files) {
                    if (file.isDirectory() && file.getName().equals(signImage)) {
                        File[] signImageFiles = file.listFiles();
                        if (signImageFiles != null) {
                            for (File signImageFile : files) {
                                String signFileName = signImageFile.getName();
                                if (signImageFile.isFile() && fileName.equals(signFileName)) {
                                    bitmap = BitmapFactory.decodeFile(signImageFile.getPath());
                                }
                            }
                        }
                    }
                }
            }
        }
    } catch (Exception e) {
    }
    return bitmap;
}



公共文件的增、删、改、查

公共文件的操作需要用到ContentResolver和Cursor



向公共文件夹添加文件

//将文件保存到公共的媒体文件夹
//这里的filepath不是绝对路径,而是某个媒体文件夹下的子路径,和沙盒子文件夹类似
//这里的filename单纯的指文件名,不包含路径
public void saveSignImage(String filePath,String fileName, Bitmap bitmap) {
    try {
        //设置保存参数到ContentValues中
        ContentValues contentValues = new ContentValues();
        //设置文件名
        contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
        //兼容Android Q和以下版本
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            //android Q中不再使用DATA字段,而用RELATIVE_PATH代替
            //RELATIVE_PATH是相对路径不是绝对路径
            //DCIM是系统文件夹,关于系统文件夹可以到系统自带的文件管理器中查看,不可以写没存在的名字
            contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/signImage");
            //contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Music/signImage");
        } else {
            contentValues.put(MediaStore.Images.Media.DATA, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath());
        }
        //设置文件类型
        contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/JPEG");
        //执行insert操作,向系统文件夹中添加文件
        //EXTERNAL_CONTENT_URI代表外部存储器,该值不变
        Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
        if (uri != null) {
            //若生成了uri,则表示该文件添加成功
            //使用流将内容写入该uri中即可
            OutputStream outputStream = getContentResolver().openOutputStream(uri);
            if (outputStream != null) {
                bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream);
                outputStream.flush();
                outputStream.close();
            }
        }
    } catch (Exception e) {
    }
}



查询公共文件夹下的文件

//在公共文件夹下查询图片
//这里的filepath在androidQ中表示相对路径
//在androidQ以下是绝对路径
public Bitmap querySignImage(String filePath) {
    try {
        //兼容androidQ和以下版本
        String queryPathKey = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q ? MediaStore.Images.Media.RELATIVE_PATH : MediaStore.Images.Media.DATA;
        //查询的条件语句
        String selection = queryPathKey + "=? ";
        //查询的sql
        //Uri:指向外部存储Uri
        //projection:查询那些结果
        //selection:查询的where条件
        //sortOrder:排序
        Cursor cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                new String[]{MediaStore.Images.Media._ID, queryPathKey, MediaStore.Images.Media.MIME_TYPE, MediaStore.Images.Media.DISPLAY_NAME},
                selection,
                new String[]{filePath},
                null);

        //是否查询到了
        if (cursor != null && cursor.moveToFirst()) {
            //循环取出所有查询到的数据
            do {
                //一张图片的基本信息
                int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));//uri的id,用于获取图片
                String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH));//图片的相对路径
                String type = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE));//图片类型
                String name = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME));//图片名字
                //根据图片id获取uri,这里的操作是拼接uri
                Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "" + id);
                //官方代码:
                 Uri contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
                if (uri != null) {
                    //通过流转化成bitmap对象
                    InputStream inputStream = getContentResolver().openInputStream(uri);
                    return BitmapFactory.decodeStream(inputStream);
                }
            } while (cursor.moveToNext());
        }
        if (cursor != null)
            cursor.close();
    } catch (Exception e) {
    }
    return null;
}



其他操作

增删改查操作都是使用:ContentResolver去操作

官方文档中也说明了,想要操作公共目录,使用ContentResolver去进行一切增删改查:

在 Android 9(API 级别 28)及更低版本中,保存到外部存储设备上的所有文件都显示在名为

external

的单个卷下。但是,Android Q 为每个外部存储设备都提供唯一的卷名称。这一新的命名系统可帮助您高效地整理内容并将内容编入索引,还可让您控制新内容的存储位置。

主要共享存储设备始终称为

VOLUME_EXTERNAL_PRIMARY

。您可以通过调用

MediaStore.getExternalVolumeNames()

发现其他卷。

要查询、插入、更新或删除特定卷,请将卷名称传递到


MediaStore


API 中的任何

getContentUri()

方法,如以下代码段中所示:

// Publish an audio file onto a specific external storage device.

val values = ContentValues().apply {


put(MediaStore.Audio.Media.RELATIVE_PATH, “Music/My Album/My Song”)

put(MediaStore.Audio.Media.DISPLAY_NAME, “My Song.mp3”)

}

// Assumes that the storage device of interest is the 2nd one

// that your app recognizes.

val volumeNames = MediaStore.getExternalVolumeNames(context)

val selectedVolumeName = volumeNames[1]

val collection = MediaStore.Audio.Media.getContentUri(selectedVolumeName)

val item = resolver.insert(collection, values)



版权声明:本文为yehui928186846原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。