Android 10.0 适配——文件存储
前言
Android 10.0不需要再动态申请文件读写权限,默认可以读写自己的沙盒文件和公共媒体文件。内部存储路径为/data/data/包名,沙盒路径为/sdcard/Android/data/包名,沙盒路径在不做任何操作时,安装的同时不会立即生成。
10.0以前需要申请的文件读写权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
沙盒
Android 10.0在外部存储设备中为每个应用提供了一个“隔离存储沙盒”(例如 /sdcard)。任何其他应用都无法直接访问自己应用的沙盒文件。由于文件是应用的私有文件,因此不再需要任何权限即可在外部存储设备中访问和保存自己的文件。这个变更可让我们更轻松地保证用户文件的隐私性,并有助于减少应用所需的权限数量。
沙盒,简单而言就是应用专属文件夹,并且访问这个文件夹无需权限。谷歌官方推荐应用在沙盒内存储文件的地址为Context.getExternalFilesDir()下的文件夹。比如要存储一张图片,则应放在Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)中。
官方说明
关于10.0之后的一些变更,具体可以参考地址(需科学上网):
隐私权限变更
https://developer.android.google.cn/about/versions/10/privacy/changes
行为变更
https://developer.android.google.cn/about/versions/10/behavior-changes-all
获取沙盒指定文件夹
获取沙盒下的文件目录,这里指沙盒下的图片文件夹:
File pictures = 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 final String folderName = "myImage";
public void saveImageInSandbox(Context context, String fileName, Bitmap bitmap) {
try {
File pictures = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);//图片沙盒文件夹
File folder = new File(pictures + "/" + folderName);
if (folder.exists()) {
File file = new File(pictures + "/" + folderName + "/" + fileName);
FileOutputStream fos = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos);
fos.flush();
fos.close();
} else if (folder.mkdir()) {//如果该文件夹不存在,则新建
File imageFile = new File(pictures + "/" + folderName + "/" + fileName);
FileOutputStream fos = new FileOutputStream(imageFile);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos);
fos.flush();
fos.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
saveImageInSandbox(this, "ic_launcher", BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
获取沙盒中的文件
private final String folderName = "myImage";
public Bitmap queryImageInSandbox(Context context, String fileName) {
if (TextUtils.isEmpty(fileName))
return null;
Bitmap bitmap = null;
try {
File pictures = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);//图片沙盒文件夹
if (pictures != null && pictures.exists() && pictures.isDirectory()) {
File[] files = pictures.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory() && file.getName().equals(folderName)) {
File[] imageFiles = file.listFiles();
if (imageFiles != null) {
for (File imageFile : imageFiles) {
String imageFileName = imageFile.getName();
if (imageFile.isFile() && fileName.equals(imageFileName)) {
bitmap = BitmapFactory.decodeFile(imageFile.getPath());
}
}
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;
}
Bitmap bitmap = queryImageInSandbox(this, "ic_launcher");
iv.setImageBitmap(bitmap);
保存文件到公共文件夹
这里文件夹路径使用的是相对路径
private final String folderName = "myImage";
private final String DCIM = "DCIM";
public void saveImage(Context context, String fileName, Bitmap bitmap) {
try {
//设置保存参数到ContentValues中
ContentValues cv = new ContentValues();
//设置文件名
cv.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是系统文件夹,关于系统文件夹可以到系统自带的文件管理器中查看,不可以写不存在的名字
cv.put(MediaStore.Images.Media.RELATIVE_PATH, DCIM + "/" + folderName);
} else {
cv.put(MediaStore.Images.Media.DATA, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath());
}
//设置文件类型
cv.put(MediaStore.Images.Media.MIME_TYPE, "image/JPEG");
//执行insert操作,向系统文件夹中添加文件
//EXTERNAL_CONTENT_URI代表外部存储器,该值不变
Uri uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cv);
if (uri != null) {
//若生成了uri,则表示该文件添加成功
//使用流将内容写入该uri中即可
OutputStream os = context.getContentResolver().openOutputStream(uri);
if (os != null) {
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os);
os.flush();
os.close();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
saveImage(this, "ic_launcher", BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
获取公共文件夹的文件
private final String folderName = "myImage";
private final String DCIM = "DCIM";
public Bitmap queryImage(Context context, String fileName) {
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 = context.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[]{DCIM + "/" + folderName + "/"},
null);
//是否查询到了
if (cursor != null && cursor.moveToFirst()) {
//循环取出所有查询到的数据
do {
//一张图片的基本信息
int id = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media._ID));//uri的id,用于获取图片
String name = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME));//图片名字
String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.RELATIVE_PATH));//图片的相对路径
String type = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE));//图片类型
//根据图片id获取uri,这里的操作是拼接uri
Uri uri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, String.valueOf(id));
//官方代码
// Uri contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
if (uri != null && name.contains(fileName)) {
//通过流转化成bitmap对象
InputStream is = context.getContentResolver().openInputStream(uri);
return BitmapFactory.decodeStream(is);
}
} while (cursor.moveToNext());
}
if (cursor != null)
cursor.close();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
Bitmap bitmap = queryImage(this, "ic_launcher");
iv.setImageBitmap(bitmap);
结尾
对于暂时不想适配的,又不影响应用运行的方法,可以把targetSdkVersion设置为29以下,或者在<application>标签中加入android:allowExternalStorageSandbox="false",但这些方法都是暂时的,过后的版本,不论你怎么设置,都无法再使用10.0以前的文件存储方式了。