android 11 key changes
One: partition storage
1: It has been changed to mandatory, android:requestLegacyExternalStorage="true" method is invalid
Recommended reading article: Android 11 new features, Scoped Storage has new tricks
android 10 key changes
One: partition storage
internal storage
1 file
You used to be like this:
the path corresponding to the Environment.getExternalStorageDirectory() associated directory is roughly as follows:
/storage/emulated/0
Now you are officially required to do this: mContext.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
The path corresponding to the associated directory is roughly as follows:
/storage/emulated/0/Android/data/<包名>/files/Pictures
Cache type files can be like this: val externalCacheDirPath = externalCacheDir!!.absolutePath
The path corresponding to the associated directory is roughly as follows
/storage/emulated/0/Android/data/<包名>/cache
You are smart, you must be able to see the files we are storing now, all under the package name. Let it take its course, you may think that after uninstalling the app, the files we stored are also deleted. The answer is yes. What are the advantages of doing this? In this blog of Guo Shen , it is better, you can go and have a look.
Environment has the following properties:
- DIRECTORY_PICTURES
- DIRECTORY_DOWNLOADS
- DIRECTORY_MOVIES
- DIRECTORY_AUDIOBOOKS
- DIRECTORY_MUSIC
- etc.
The adaptation code is roughly as follows:
//返回通用图片储存路径,统一在这个方法中,做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;
}
2 media
Get the adaptation of the picture in the album
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()
}
Adaptation for adding pictures to albums
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()
}
}
}
remove picture from album
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
)
}
}
Video and audio files are basically the same, in summary:
The MediaStore API provides interfaces to access the following types of media files:
- Photos: stored in MediaStore.Images.
- Video: Stored in MediaStore.Video.
- Audio files: stored in MediaStore.Audio.
Here is a summary of the places where most applications need to be modified:
(1) Selection of local photos, storage path when saving pictures to local
(2) When processing media files in external storage
(3) Storage when app upgrades and downloads apk to local path
3 Description
Generally, we use the getFilesDir() or getCacheDir() method to obtain the internal storage path of this application. To read and write files under this path, there is no need to apply for storage space read and write permissions, and it will be automatically deleted when the application is uninstalled. The path storage using this method does not need to be adapted!
The corresponding path is roughly as follows:
filesDir.absolutePath
/data/user/0/app的包名/files
cacheDir.absolutePath
/data/user/0/app的包名/cache
external storage
Addition, deletion, modification and query of the Download directory in the public area
Here we will focus on adding, deleting, modifying and checking the Download directory, and the use of contentResolver.
increase
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()
}
}
check
Query all:
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")
}
Search by file name:
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")
}
Check according to relative path:
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: Pay attention to the file path filePath here, and / must be added at the end, otherwise the data cannot be found.
Search according to the file name and relative path:
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")
}
The important thing is said three times: the test found that if the file was not created by you or the file was moved or copied, there will be a problem that the file cannot be found, and of course the program will not report an error. In other words: only the files you created can be found, updated, and deleted.
The important thing is said three times: the test found that if the file was not created by you or the file was moved or copied, there will be a problem that the file cannot be found, and of course the program will not report an error. In other words: only the files you created can be found, updated, and deleted.
The important thing is said three times: the test found that if the file was not created by you or the file was moved or copied, there will be a problem that the file cannot be found, and of course the program will not report an error. In other words: only the files you created can be found, updated, and deleted.
change
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)
}
Description: In this way, the found file can be renamed to 1604584554910ypk.jpg
delete
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)
}
}
Article source code
Reference blog: Recommended: Android 10 adaptation key points, scoped storage
Two: Enhanced user control over location permissions
Foreground-only permissions that give users more control over app access to device location information
Affected Apps: Apps that request access to the user's location while in the background
Note: If you should use the positioning function, you must do adaptation processing. In order to allow users to better control the application's access to location information, Android 10 introduces the ACCESS_BACKGROUND_LOCATION permission. Unlike the ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permissions, the ACCESS_BACKGROUND_LOCATION permission only affects the app's access to location information while it is running in the background. For more instructions visit here .
How to adapt it? Don't worry, let's take our time. In Android 10, not only the ACCESS_COARSE_LOCATION permission must be dynamically applied, but the ACCESS_BACKGROUND_LOCATION permission must also be dynamically applied together! Only requesting the ACCESS_BACKGROUND_LOCATION permission has no effect!
Here I will share a pitfall I encountered when I was doing the adaptation. I hope everyone will not make the same mistake. I integrated the Gaode map. Go directly to the code:
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
)
}
}
Note: It is strongly recommended that these three permissions are applied together, otherwise there will be very strange problems.
Three: The system executes the background Activity
Implemented restrictions on launching activities from the background
Affected Apps: Apps that start an Activity without user interaction
Four: non-resettable hardware identifier
Restrictions are enforced on accessing device serial number and IMEI
Apps affected: Apps that access the device serial number or IMEI
Five: Wireless scanning permissions
Access to certain Wi-Fi, Wi-Fi Sense, and Bluetooth scanning methods requires precise location permission
Affected Apps: Apps using Wi-Fi API and Bluetooth API
Reference article:
The basic method and framework use of android 6.0 dynamic application permission
Basic usage:
First provide the official learning documents of goog:
Ask for permissions at runtime
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();//打开相册
}
The callback that initiates the request:
@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;
}
}
Encapsulation of some frameworks:
Simple use of HiPermission: For more information, see related articles in the reference blog.
This method applies for almost all three permissions necessary. camera, location, sd card write
//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");//用户允许后,会回调该函数 //在此可以做 事件处理啦,因为用户已经同意了,此时已经拿到所需权限啦。
}
});
Since 6.0, what permissions need to be obtained dynamically? List it here:
It is divided into 9 groups. As long as one permission application in each group is successful, the whole group of permissions can be used by default.
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 official document address (need to overturn the wall): https://developer.android.com/guide/topics/security/permissions.html
Reference blog:
RxPermissions has more than 8k stars until 2019/2/23
The star is more than 5K, and it has been updated,
easypermissions is good
One line of code to get the beautiful Android6.0 permission application interface
HiPermission
This attention is relatively high: as of 2019/2/23, there are more than 8.6K stars, and it supports kotlin and can also be used as a backup
PermissionsDispatcher