首先,我们要知道对于开发者来说,android应用存储空间从逻辑上划分为内部存储和外部存储。而分区存储就是对外部存储的进一步严格要求。
分区存储机制会让APP在外部存储空间有属于自己的专属存储区域,这块区域是私有的,因此,这块区域非常适用于保存用户的私有数据。一般情况下这块私有区域不会暴露给其他应用。
每个应用向自己的私有目录读写文件,不需要读写权限。
应用即使获取了读写权限,也无法访问其他应用的私有目录。
私有文件目录具体路径:storage/emulated/0/android/data/packageName/…
访问方式:
this.getExternalMediaDirs() ==[/storage/emulated/0/Android/media/com.yoshin.tspsdk]
this.getExternalCacheDir() ==/storage/emulated/0/Android/data/com.yoshin.tspsdk/cache
this.getExternalFilesDir(Environment.DIRECTORY_SCREENSHOTS) ==/storage/emulated/0/Android/data/com.yoshin.tspsdk/files/Screenshots
其中,这几个方法在调用的时候,如果还没有对应文件夹,都会进行创建。
Environment.DIRECTORY_SCREENSHOTS 是可以被替他参数代替的,就会访问其他文件夹了,然后低版本会有异常,不建议这么用。
访问其他应用的专属目录并不需要权限,但是需要其他应用共享,使用:
ParcelFileDescriptor 与 FileDescriptor
就可以实现
Android 11 访问自己的媒体文件并不需要权限,但是其他应用的媒体文件就需要READ_EXTERNAL_STORAGE权限,否则查询不到其他媒体文件。
Android 低版本查询媒体文件就需要权限,所以最好一直设置READ_EXTERNAL_STORAGE权限
访问方式:MediaStore API
接口定义地址:
https://developer.android.google.cn/reference/android/provider/MediaStore
如何获取音频文件:
ContentResolver contentResolver = this.getContentResolver();
Cursor cursor = contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
null, null, null, null);
if (cursor != null) {
if (cursor.getCount() >0){
LogUtils.i(TAG, "audio > 0");
}
while (cursor.moveToNext()) {
long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID));
Uri uri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id);
LogUtils.i(TAG, "audio uri is = " + uri);
}
cursor.close();
}
Log
StroageActivity: audio > 0
StroageActivity: audio uri is = content://media/external/audio/media/432
StroageActivity: audio uri is = content://media/external/audio/media/434
StroageActivity: audio uri is = content://media/external/audio/media/441
使用uri即可播放音频文件
图片:MediaStore.Images.Media.EXTERNAL_CONTENT_URI
视频:MediaStore.Video.Media.EXTERNAL_CONTENT_URI
音乐:MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
如果是查询图片,使用MediaStore.Images.Media.EXTERNAL_CONTENT_URI
查询即可,若要使用查询出来uri,需要将它转换成io,再转换成bitmap
((ImageView) findViewById(R.id.iv)).setImageBitmap(getBitmapFormUri(StroageActivity.this, uri));
转换和压缩的方法,转载自:https://www.cnblogs.com/exmyth/p/8420112.html
/**
* 通过uri获取图片并进行压缩
*
* @param uri
*/
public static Bitmap getBitmapFormUri(Activity ac, Uri uri) throws FileNotFoundException, IOException {
InputStream input = ac.getContentResolver().openInputStream(uri);
BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
onlyBoundsOptions.inJustDecodeBounds = true;
onlyBoundsOptions.inDither = true;//optional
onlyBoundsOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional
BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
input.close();
int originalWidth = onlyBoundsOptions.outWidth;
int originalHeight = onlyBoundsOptions.outHeight;
if ((originalWidth == -1) || (originalHeight == -1))
return null;
//图片分辨率以480x800为标准
float hh = 800f;//这里设置高度为800f
float ww = 480f;//这里设置宽度为480f
//缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
int be = 1;//be=1表示不缩放
if (originalWidth > originalHeight && originalWidth > ww) {
//如果宽度大的话根据宽度固定大小缩放
be = (int) (originalWidth / ww);
} else if (originalWidth < originalHeight && originalHeight > hh) {
//如果高度高的话根据宽度固定大小缩放
be = (int) (originalHeight / hh);
}
if (be <= 0) {
be = 1;
}
//比例压缩
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inSampleSize = be;//设置缩放比例
bitmapOptions.inDither = true;//optional
bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional
input = ac.getContentResolver().openInputStream(uri);
Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
input.close();
return compressImage(bitmap);//再进行质量压缩
}
/**
* 质量压缩方法
*
* @param image
* @return
*/
public static Bitmap compressImage(Bitmap image) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
int options = 100;
while (baos.toByteArray().length / 1024 > 100) {
//循环判断如果压缩后图片是否大于100kb,大于继续压缩
baos.reset();//重置baos即清空baos
//第一个参数 :图片格式 ,第二个参数: 图片质量,100为最高,0为最差 ,第三个参数:保存压缩后的数据的流
image.compress(Bitmap.CompressFormat.JPEG, options, baos);//这里压缩options%,把压缩后的数据存放到baos中
options -= 10;//每次都减少10
}
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把压缩后的数据baos存放到ByteArrayInputStream中
Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream数据生成图片
return bitmap;
}
如果对图片品质要求不高,Bitmap.Config.ARGB_8888 替换成 565,可以节省内存占用。
SAF地址:https://developer.android.google.cn/training/data-storage/shared/documents-files
对文件和目录访问使用 SAF (存储访问框架–Storage Access Framework),SAF访问方式不需要申请权限
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, 101);
@RequiresApi(Build.VERSION_CODES.KITKAT)
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data == null || resultCode != Activity.RESULT_OK) {
return;
}
if (requestCode == 101) {
Uri uri = data.getData();
}
}
如下是访问图片资源
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("image/*");
startActivityForResult(intent, 100);
@RequiresApi(Build.VERSION_CODES.KITKAT)
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data == null || resultCode != Activity.RESULT_OK) {
return;
}
if (requestCode == 100) {
Uri uri = data.getData();
try {
((ImageView) findViewById(R.id.iv_2)).setImageBitmap(getBitmapFormUri(StroageActivity.this, uri));
} catch (IOException e) {
e.printStackTrace();
}
}
}
除此之外,资源文件还有很多种可以访问:
以下类转载自:https://blog.csdn.net/smallbabylong/article/details/105574848
enum class MimeType(val value: String) {
//todo 发现新的音频文件有aac格式,这里没有
_png("image/png"),
_jpeg("image/jpeg"),
_jpg("image/jpeg"),
_webp("image/webp"),
_gif("image/gif"),
_bmp("image/bmp"),
_3gp("video/3gpp"),
_apk("application/vnd.android.package-archive"),
_asf("video/x-ms-asf"),
_avi("video/x-msvideo"),
_bin("application/octet-stream"),
_c("text/plain"),
_class("application/octet-stream"),
_conf("text/plain"),
_cpp("text/plain"),
_doc("application/msword"),
_docx("application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
_xls("application/vnd.ms-excel"),
_xlsx("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
_exe("application/octet-stream"),
_gtar("application/x-gtar"),
_gz("application/x-gzip"),
_h("text/plain"),
_htm("text/html"),
_html("text/html"),
_jar("application/java-archive"),
_java("text/plain"),
_js("application/x-javascript"),
_log("text/plain"),
_m3u("audio/x-mpegurl"),
_m4a("audio/mp4a-latm"),
_m4b("audio/mp4a-latm"),
_m4p("audio/mp4a-latm"),
_m4u("video/vnd.mpegurl"),
_m4v("video/x-m4v"),
_mov("video/quicktime"),
_mp2("audio/x-mpeg"),
_mp3("audio/x-mpeg"),
_mp4("video/mp4"),
_mpc("application/vnd.mpohun.certificate"),
_mpe("video/mpeg"),
_mpeg("video/mpeg"),
_mpg("video/mpeg"),
_mpg4("video/mp4"),
_mpga("audio/mpeg"),
_msg("application/vnd.ms-outlook"),
_ogg("audio/ogg"),
_pdf("application/pdf"),
_pps("application/vnd.ms-powerpoint"),
_ppt("application/vnd.ms-powerpoint"),
_pptx("application/vnd.openxmlformats-officedocument.presentationml.presentation"),
_prop("text/plain"),
_rc("text/plain"),
_rmvb("audio/x-pn-realaudio"),
_rtf("application/rtf"),
_sh("text/plain"),
_tar("application/x-tar"),
_tgz("application/x-compressed"),
_txt("text/plain"),
_wav("audio/x-wav"),
_wma("audio/x-ms-wma"),
_wmv("audio/x-ms-wmv"),
_wps("application/vnd.ms-works"),
_xml("text/plain"),
_z("application/x-compress"),
_zip("application/x-zip-compressed"),
_0("*/*"),
;
companion object {
fun isImage(mimeType: String?): Boolean {
return mimeType?.let {
_webp.value == mimeType ||
_png.value == mimeType ||
_jpeg.value == mimeType ||
_jpg.value == mimeType ||
_bmp.value == mimeType ||
_gif.value == mimeType
} ?: false
}
fun isGif(mimeType: String?): Boolean {
return mimeType?.let {
_gif.value == mimeType
} ?: false
}
fun isApk(mimeType: String?) = mimeType?.let {
_apk.value == mimeType
} ?: false
fun isVideo(mimeType: String?) = mimeType?.let {
_m3u.value == mimeType || _avi.value == mimeType
} ?: false
}
}
targetSdk = 30,限制了SAF的一些操作
访问目录
使用 ACTION_OPEN_DOCUMENT_TREE intent 操做来请求访问目录:
无法获取 Downloads 根目录
无法获取根目录
Android/data/ 目录及其全部子目录不可见,所以更无法获取
Android/obb/ 目录及其全部子目录不可见,所以更无法获取
访问文件
使用 ACTION_OPEN_DOCUMENT intent 操做来请求选择文件:
Android/data/ 目录及其全部子目录不可见,所以更无法选择
Android/obb/ 目录及其全部子目录不可见,所以更无法选择
如果您的应用需要访问单个文件,比如文字处理应用,则应该使用 Storage Access Framework (SAF)。
像文件管理器、手机助手之类的APP依然会需要访问所有文件,虽然手机要求分区存储,但是依然有提供申请全部文件的权限和方法。当然,肯定还是有些不同的,这里提供的所有文件不再是手机内的全部文件,现在的所有文件是:
使用共享存储的用户数据,可以或应该被其他应用程序访问,并保存,即使用户卸载你的应用程序。
Android提供api来存储和访问以下类型的可共享数据:
媒体内容:系统为这些类型的文件提供了标准的公共目录,因此用户有一个用于存放所有照片的公共位置,另一个用于存放所有音乐和音频文件的公共位置,等等。你的应用程序可以使用平台的MediaStore API访问这些内容。
文档和其他文件:系统有一个特殊的目录用于包含其他类型的文件,例如使用EPUB格式的PDF文档和书籍。你的应用程序可以使用平台的存储访问框架访问这些文件。
数据集:在Android 11 (API级别30)和更高,系统缓存大的数据集,多个应用程序可能使用。这些数据集可以支持机器学习和媒体回放等用例。应用程序可以使用BlobStoreManager API访问这些共享数据集。
申请“全部文件”的方法:
//申请
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
//有没有全部的权限
boolean isExternalStorageManager = Environment.isExternalStorageManager();
//没有的话,让用户去授权,跳转到授权页面
Intent intent = new Intent();
intent.setAction(ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
当你申请完全部文件的权限,访问的时候,通过:
都可以访问上述“所有文件”资源,但是其他应用的私有路径依然不可访问。
我这里理解是即使有所有的权限,4.3.SAF的限制性依然存在
注:对于全部文件的访问权限具体和读写权限差在哪里,不清楚,希望可以留言交流,文章持续更新
博客是在环境:
android = [
compileSdkVersion: 30,
buildToolsVersion: “30.0.2”,
minSdkVersion : 21,
targetSdkVersion : 30,
versionCode : 1,
versionName : “1.0”
]
下进行测试和验证。
Android11新特性及部分适配
https://www.jianshu.com/p/a228f6a46354
Android11新特性
https://blog.csdn.net/generallizhong/article/details/108511747
Android11最全适配实践指南–应用端https://www.jianshu.com/p/f5796aead731
https://developer.android.google.cn/about/versions/11/behavior-changes-all
解析Android内部存储、外部存储的区别:https://blog.csdn.net/baidu_36385172/article/details/79695308