1:已改为强制,android:requestLegacyExternalStorage=“true” 方法已失效
推荐阅读文章:Android 11新特性,Scoped Storage又有了新花样
官方文档
以前你是这样:Environment.getExternalStorageDirectory()
关联目录对应的路径大致如下:
/storage/emulated/0
现在官方要求你这样:mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
关联目录对应的路径大致如下:
/storage/emulated/0/Android/data/<包名>/files/Pictures
缓存类型文件可这样:val externalCacheDirPath = externalCacheDir!!.absolutePath
关联目录对应的路径大致如下
/storage/emulated/0/Android/data/<包名>/cache
聪明的你,一定能看到我们现在储存的文件,都在包名下啦。顺其自然你可能会想到,卸载app后我们存储的文件,是不是也都删除了呢。答案是肯定的。这样做有什么优点,这篇郭神的博客 中,讲的比较好大家可以去看看。
Environment 有以下属性:
适配代码大概如下:
//返回通用图片储存路径,统一在这个方法中,做android 10 的适配
public static String getCommonSavePath(Context mContext) {
String path = "";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {//适配android 10
path = mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + "";
} else {
path = Environment.getExternalStorageDirectory().getAbsolutePath();
}
return path;
}
获取相册中的图片的适配
val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, "${MediaStore.MediaColumns.DATE_ADDED} desc")
if (cursor != null) {
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
}
cursor.close()
}
将图片添加到相册的适配
fun addBitmapToAlbum(bitmap: Bitmap, displayName: String, mimeType: String, compressFormat: Bitmap.CompressFormat) {
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
} else {
values.put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$displayName")
}
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
if (uri != null) {
val outputStream = contentResolver.openOutputStream(uri)
if (outputStream != null) {
bitmap.compress(compressFormat, 100, outputStream)
outputStream.close()
}
}
}
将图片从相册中删除
private fun deleteImageFromAlbum() {
val imageFileName = "20201130ypk6667.jpg"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val queryPathKey = MediaStore.MediaColumns.DISPLAY_NAME;
val cursor = contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null,
"$queryPathKey =? ",
arrayOf(imageFileName),
null
)
if (cursor != null) {
Log.e("ypkTest", "cursor is ");
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
println("ypkTest.deleteFil11e uri=${uri.toString()}")
deleteDealWith(uri);
}
} else {
Log.e("ypkTest", "cursor is null");
}
/* val where = MediaStore.Images.Media.DISPLAY_NAME + "='" + imageFileName + "'"
//测试发现,只有是自己应用插入的图片,才可以删除。其他应用的Uri,无法删除。卸载app后,再去删除图片,此方法不会抛出SecurityException异常
val result = contentResolver.delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, where, null)
Log.i("ypkTest", "deleteImageFromAlbum1 result=${result}");
if (result > 0) {
Toast.makeText(this, "delete sucess", Toast.LENGTH_LONG).show()
}*/
} else {
val filePath = "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$imageFileName";
val where = MediaStore.Images.Media.DATA + "='" + filePath + "'"
val result = contentResolver.delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, where, null)
Log.i("ypkTest", "result=${result}");
if (result > 0) {
Toast.makeText(this, "delete sucess", Toast.LENGTH_LONG).show()
}
}
}
/**
* 知识补充:
* 开了沙箱之后,之前的媒体库生成的文件在其记录上会打上owner_package的标志,标记这条记录是你的app生成的。
* 当你的app卸载后,MediaStore就会将之前的记录去除owner_package标志,
* 也就是说app卸载后你之前创建的那个文件与你的app无关了(不能证明是你的app创建的)。
* 所以当你再次安装app去操作之前的文件时,媒体库会认为这条数据不是你这个新app生成的,所以无权删除或更改。
* 处理方案:
* 采用此种方法,删除相册图片,会抛出SecurityException异常,捕获后做下面的处理,会出现系统弹框,提示你是否授权删除。
* 点击授权后,我们在onActivityResult回调中,再次做删除处理,理论上就能删除。
*
* 测试发现:小米8,Android10,是有系统弹框提示,提示是否授权,授权后在去删除,删除的result结果也是1,
* 根据result的值判断,确实是删除了。但是相册中,依然存在。不知道为何是这样?
*
* 参考文章:https://blog.csdn.net/flycatdeng/article/details/105586961
*/
@RequiresApi(Build.VERSION_CODES.Q)
private fun deleteDealWith(uri: Uri) {
try {
val result = contentResolver.delete(uri, null, null)
println("ypkTest.deleteImageFromDownLoad result=$result")
if (result > 0) {
Toast.makeText(this, "delete succeeded.", Toast.LENGTH_SHORT).show()
}
} catch (securityException: SecurityException) {
Log.e("ypkTest", "securityException=${securityException.message}");
securityException.printStackTrace()
val recoverableSecurityException =
securityException as? RecoverableSecurityException
?: throw securityException
// 我们可以使用IntentSender向用户发起授权
val intentSender =
recoverableSecurityException.userAction.actionIntent.intentSender
startIntentSenderForResult(
intentSender,
REQUEST_DELETE_PERMISSION,
null,
0,
0,
0,
null
)
}
}
视频和音频文件基本上是同理的,总结:
MediaStore API 提供访问以下类型的媒体文件的接口:
这里总结一下大部分应用都要修改的地方:
(1)本地照片的选择,保存图片到本地时储存路径
(2)处理外部存储中的媒体文件时
(3)app升级下载apk到本地时的储存路径
一般我们使用getFilesDir() 或 getCacheDir() 方法获取本应用的内部储存路径,读写该路径下的文件不需要申请储存空间读写权限,且卸载应用时会自动删除。使用该方法进行的路径储存是不需要做适配的!
对应的路径大概如下:
filesDir.absolutePath
/data/user/0/app的包名/files
cacheDir.absolutePath
/data/user/0/app的包名/cache
公共区域Download目录的增删改查
这里着重讲解一下Download目录的增删改查,contentResolver 的使用。
增
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image)
val displayName = "${System.currentTimeMillis()}.jpg"
val compressFormat = Bitmap.CompressFormat.JPEG
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/pactera/com/TestFile3/")
val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
println("MainActivity.downloadFile1=${uri.toString()}") //content://media/external/downloads/1362855
println("MainActivity.downloadFile2=${uri!!.path}") ///external/downloads/1362855
if (uri != null) {
val outputStream = contentResolver.openOutputStream(uri)
if (outputStream != null) {
bitmap.compress(compressFormat, 100, outputStream)
outputStream.close()
Toast.makeText(this, "Add bitmap to album succeeded.", Toast.LENGTH_SHORT).show()
}
}
查
查询全部:
val cursor = contentResolver.query(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
null,
null,
null,
null
)
if (cursor != null) {
println("MainActivity.deleteFil11e cursor is")
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID))
val name = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME));//图片名字
val relative_path = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.RELATIVE_PATH));//相对路径
//根据图片id获取uri,这里的操作是拼接uri
val uri = ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
println("MainActivity.deleteFil11e id=$id")
println("MainActivity.deleteFil11e name=$name")
println("MainActivity.deleteFil11e relative_path=$relative_path")
println("MainActivity.deleteFil11e uri=${uri.toString()}")
}
cursor.close()
} else {
println("MainActivity.deleteFil11e cursor is null")
}
根据文件名字查:
val fileName = "1604584554910.jpg";
val queryPathKey = MediaStore.MediaColumns.DISPLAY_NAME;
val cursor = contentResolver.query(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
null,
queryPathKey + " =? ",
arrayOf(fileName),
null
)
if (cursor != null) {
println("MainActivity.deleteFil11e cursor is")
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID))
val name = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME));//图片名字
val relative_path = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.RELATIVE_PATH));//相对路径
//根据图片id获取uri,这里的操作是拼接uri
val uri = ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
println("MainActivity.deleteFil11e id=$id")
println("MainActivity.deleteFil11e name=$name")
println("MainActivity.deleteFil11e relative_path=$relative_path")
println("MainActivity.deleteFil11e uri=${uri.toString()}")
}
cursor.close()
} else {
println("MainActivity.deleteFil11e cursor is null")
}
根据相对路径查:
val filePath = Environment.DIRECTORY_DOWNLOADS + "/pactera/com/TestFile/"
val queryPathKey = MediaStore.MediaColumns.RELATIVE_PATH;
val cursor = contentResolver.query(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
null,
queryPathKey + " =? ",
arrayOf(filePath ),
null
)
if (cursor != null) {
println("MainActivity.deleteFil11e cursor is")
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID))
val name = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME));//图片名字
val relative_path = cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.RELATIVE_PATH));//相对路径
//根据图片id获取uri,这里的操作是拼接uri
val uri = ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
println("MainActivity.deleteFil11e id=$id")
println("MainActivity.deleteFil11e name=$name")
println("MainActivity.deleteFil11e relative_path=$relative_path")
println("MainActivity.deleteFil11e uri=${uri.toString()}")
}
cursor.close()
} else {
println("MainActivity.deleteFil11e cursor is null")
}
ps: 注意这里的文件路径 filePath ,末尾要加 / ,不然查不到数据。
根据 文件名字 和 相对路径 查:
val fileName = "1604584554910.jpg";
val filePath = Environment.DIRECTORY_DOWNLOADS + "/pactera/com/TestFile/"
val queryPathKey = MediaStore.MediaColumns.DISPLAY_NAME;
val queryPathKey2 = MediaStore.MediaColumns.RELATIVE_PATH;
val cursor = contentResolver.query(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
null,
queryPathKey + " =? and " + queryPathKey2 + " =?",
arrayOf(fileName, filePath),
null
)
if (cursor != null) {
println("MainActivity.deleteFil11e cursor is")
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID))
val name =
cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME));//图片名字
val relative_path =
cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.RELATIVE_PATH));//图片名字
val uri =
ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
println("MainActivity.deleteFil11e id=$id")
println("MainActivity.deleteFil11e name=$name")
println("MainActivity.deleteFil11e relative_path=$relative_path")
println("MainActivity.deleteFil11e uri=${uri.toString()}")
}
cursor.close()
} else {
println("MainActivity.deleteFil11e cursor is null")
}
重要的事说三遍:测试发现,如果文件不是你创建的 或者 文件是移动,复制过去的,文件会出现查不到的问题,当然程序不会报错。换言之:只有是你创建的文件,你才能查到,更新,删除。
重要的事说三遍:测试发现,如果文件不是你创建的 或者 文件是移动,复制过去的,文件会出现查不到的问题,当然程序不会报错。换言之:只有是你创建的文件,你才能查到,更新,删除。
重要的事说三遍:测试发现,如果文件不是你创建的 或者 文件是移动,复制过去的,文件会出现查不到的问题,当然程序不会报错。换言之:只有是你创建的文件,你才能查到,更新,删除。
改
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID))
val name =
cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME));//图片名字
val relative_path =
cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.RELATIVE_PATH));//图片名字
val uri =
ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
println("MainActivity.deleteFil11e id=$id")
println("MainActivity.deleteFil11e name=$name")
println("MainActivity.deleteFil11e relative_path=$relative_path")
println("MainActivity.deleteFil11e uri=${uri.toString()}")
val values = ContentValues()
values.put(MediaStore.MediaColumns.DISPLAY_NAME, "1604584554910ypk.jpg")
var result = contentResolver.update(uri, values, null, null)
println("MainActivity.deleteFil11e result=" + result)
}
说明:这样就能把查到的文件,重新命名为1604584554910ypk.jpg
删
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID))
val name =
cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.DISPLAY_NAME));//图片名字
val relative_path =
cursor.getString(cursor.getColumnIndex(MediaStore.Downloads.RELATIVE_PATH));//图片名字
val uri =
ContentUris.withAppendedId(MediaStore.Downloads.EXTERNAL_CONTENT_URI, id)
println("MainActivity.deleteFil11e id=$id")
println("MainActivity.deleteFil11e name=$name")
println("MainActivity.deleteFil11e relative_path=$relative_path")
println("MainActivity.deleteFil11e uri=${uri.toString()}")
if (fileName == name) {
var result= contentResolver.delete(uri, null, null)
println("MainActivity.deleteFil11e result="+result)
}
}
仅限前台权限,可让用户更好地控制应用对设备位置信息的访问权限
受影响的应用:在后台时请求访问用户位置信息的应用
说明:如果你的应该用到了定位功能,那就必须做适配处理。为了让用户更好地控制应用对位置信息的访问权限,Android 10 引入了 ACCESS_BACKGROUND_LOCATION 权限。与 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION 权限不同,ACCESS_BACKGROUND_LOCATION 权限仅会影响应用在后台运行时对位置信息的访问权限。更多说明请访问这里。
那如何去适配呢?不急,我们慢慢说来。在Android10中不仅要动态申请ACCESS_COARSE_LOCATION权限,ACCESS_BACKGROUND_LOCATION权限也要 一起 动态申请哦!只请求ACCESS_BACKGROUND_LOCATION权限是没有效果的!
这里我分享一下我在做适配时,遇到的一个坑,希望大家不要犯同样的错误,我集成的是高德地图。直接上代码:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val selfPermission4 = ContextCompat.checkSelfPermission(
activity,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
//如果请求此权限,则还必须请求 ACCESS_FINE_LOCATION 和 ACCESS_COARSE_LOCATION权限。只请求此权限无效果。
if (selfPermission4 != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
activity,
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
),
BACKGROUND_LOCATION_REQUESTCODE
)
}
}
说明:强烈建议,这三个权限一起申请了,不然会出现很奇葩的问题。
针对从后台启动 Activity 实施了限制
受影响的应用:不需要用户互动就启动 Activity 的应用
针对访问设备序列号和 IMEI 实施了限制
受影响的应用:访问设备序列号或 IMEI 的应用
访问某些 WLAN、WLAN 感知和蓝牙扫描方法需要获得精确位置权限
受影响的应用:使用 WLAN API 和蓝牙 API 的应用
参考文章:
Android 10 适配攻略
先提供一下goog官方的学习文档:
权限最佳做法
在运行时请求权限
int selfPermission = ContextCompat.checkSelfPermission(Main2Activity.this, Manifest.permission.CALL_PHONE);
if (selfPermission != PackageManager.PERMISSION_GRANTED) {
/**
* 判断该权限请求是否已经被 Denied(拒绝)过。 返回:true 说明被拒绝过 ; false 说明没有拒绝过
*
* 注意:
* 如果用户在过去拒绝了权限请求,并在权限请求系统对话框中选择了 Don't ask again 选项,此方法将返回 false。
* 如果设备规范禁止应用具有该权限,此方法也会返回 false。
*/
if (ActivityCompat.shouldShowRequestPermissionRationale(Main2Activity.this, Manifest.permission.CALL_PHONE)) {
Log.i(TAG, "onViewClicked: 该权限请求已经被 Denied(拒绝)过。");
//弹出对话框,告诉用户申请此权限的理由,然后再次请求该权限。
//ActivityCompat.requestPermissions(Main2Activity.this, new String[]{Manifest.permission.CALL_PHONE}, 1);
} else {
Log.i(TAG, "onViewClicked: 该权限请未被denied过");
ActivityCompat.requestPermissions(Main2Activity.this, new String[]{Manifest.permission.CALL_PHONE}, 1);
}
} else {
openAlbum();//打开相册
}
发起请求的回调:
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Log.i(TAG, "onRequestPermissionsResult: requestCode=" + requestCode);
switch (requestCode) {
case 1:
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openAlbum();
} else {
Toast.makeText(this, "you denied the permission", Toast.LENGTH_SHORT).show();
Log.i(TAG, "onRequestPermissionsResult: you denied the permission");
}
break;
default:
break;
}
}
HiPermission 的简单使用:更多查看参考博客相关文章。
该方法 申请几乎必要的三个权限。照相,定位,sd卡写
//CAMERA, ACCESS_FINE_LOCATION and WRITE_EXTERNAL_STORAGE
HiPermission.create(this)
.animStyle(R.style.PermissionAnimModal)
//.style(R.style.PermissionDefaultGreenStyle)
.checkMutiPermission(new PermissionCallback() {
@Override
public void onClose() {
Log.i(TAG, "onClose They cancelled our request"); //用户关闭权限申请
}
@Override
public void onFinish() {
Log.i(TAG, "onFinish: All permissions requested completed"); //所有权限申请完成
}
@Override
public void onDeny(String permission, int position) {
Log.i(TAG, "onDeny");//在否认
}
@Override
public void onGuarantee(String permission, int position) {
Log.i(TAG, "onGuarantee");//用户允许后,会回调该函数 //在此可以做 事件处理啦,因为用户已经同意了,此时已经拿到所需权限啦。
}
});
自6.0以后,都有哪些权限需要动态获取呢?在此罗列一下:
共分为9组,每组只要有一个权限申请成功了,就默认整组权限都可以使用了。
group:android.permission-group.CONTACTS
permission:android.permission.WRITE_CONTACTS
permission:android.permission.GET_ACCOUNTS
permission:android.permission.READ_CONTACTS
group:android.permission-group.PHONE
permission:android.permission.READ_CALL_LOG
permission:android.permission.READ_PHONE_STATE
permission:android.permission.CALL_PHONE (打电话)
permission:android.permission.WRITE_CALL_LOG
permission:android.permission.USE_SIP
permission:android.permission.PROCESS_OUTGOING_CALLS
permission:com.android.voicemail.permission.ADD_VOICEMAIL
group:android.permission-group.CALENDAR
permission:android.permission.READ_CALENDAR
permission:android.permission.WRITE_CALENDAR
group:android.permission-group.CAMERA
permission:android.permission.CAMERA ( 相机 )
group:android.permission-group.SENSORS
permission:android.permission.BODY_SENSORS
group:android.permission-group.LOCATION (位置相关)
permission:android.permission.ACCESS_FINE_LOCATION
permission:android.permission.ACCESS_COARSE_LOCATION
group:android.permission-group.STORAGE ( SD卡读写权限 )
permission:android.permission.READ_EXTERNAL_STORAGE
permission:android.permission.WRITE_EXTERNAL_STORAGE
group:android.permission-group.MICROPHONE
permission:android.permission.RECORD_AUDIO
group:android.permission-group.SMS (短信相关)
permission:android.permission.READ_SMS (读取短信)
permission:android.permission.RECEIVE_WAP_PUSH
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS (接受短信)
permission:android.permission.SEND_SMS (发送短信)
permission:android.permission.READ_CELL_BROADCASTS
google官方文档地址(需要):https://developer.android.com/guide/topics/security/permissions.html
参考博客:
RxPermissions 到2019/2/23为止star 8k多
star 5K多,而且一直在更新,不错
easypermissions
一行代码搞定漂亮的Android6.0权限申请界面
HiPermission
这个关注度比较高:到2019/2/23为止,star 8.6K多,并且支持了kotlin 也可作为备用
PermissionsDispatcher