- Android Q文件存储机制修改成了沙盒模式,和IOS神似
- 应用只能访问自己沙盒下的文件和公共媒体文件
- 对于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)