This commit is contained in:
HapeLee committed 2025-06-04 13:33:51 +08:00
1 parent b86d90021c
commit 6c0757c91b
972 files changed
+6885 -7321

No files matched your search

@@ -0,0 +1,7 @@
package io.legato.kazusa.lib.permission
interface OnErrorCallback {
fun onError(e: Exception)
}
@@ -0,0 +1,7 @@
package io.legato.kazusa.lib.permission
interface OnPermissionsDeniedCallback {
fun onPermissionsDenied(deniedPermissions: Array<String>)
}
@@ -0,0 +1,7 @@
package io.legato.kazusa.lib.permission
interface OnPermissionsGrantedCallback {
fun onPermissionsGranted()
}
@@ -0,0 +1,11 @@
package io.legato.kazusa.lib.permission
interface OnPermissionsResultCallback {
fun onPermissionsGranted()
fun onPermissionsDenied(deniedPermissions: Array<String>?)
fun onError(e: Exception)
}
@@ -0,0 +1,10 @@
package io.legato.kazusa.lib.permission
interface OnRequestPermissionsResultCallback {
fun onRequestPermissionsResult(permissions: Array<String>, grantResults: IntArray)
fun onSettingActivityResult()
fun onError(e: Exception)
}
@@ -0,0 +1,227 @@
package io.legato.kazusa.lib.permission
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import io.legato.kazusa.R
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.exception.NoStackTraceException
import io.legato.kazusa.utils.registerForActivityResult
import io.legato.kazusa.utils.toastOnUi
import kotlinx.coroutines.launch
class PermissionActivity : AppCompatActivity() {
private var rationaleDialog: AlertDialog? = null
private val settingActivityResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
onRequestPermissionFinish()
}
private val settingActivityResultAwait =
registerForActivityResult(ActivityResultContracts.StartActivityForResult())
private val requestPermissionResult =
registerForActivityResult(ActivityResultContracts.RequestPermission())
private val requestPermissionsResult =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions())
@SuppressLint("BatteryLife")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val rationale = intent.getStringExtra(KEY_RATIONALE)
val requestCode = intent.getIntExtra(KEY_INPUT_PERMISSIONS_CODE, 1000)
val permissions = intent.getStringArrayExtra(KEY_INPUT_PERMISSIONS)!!
when (intent.getIntExtra(KEY_INPUT_REQUEST_TYPE, Request.TYPE_REQUEST_PERMISSION)) {
//权限请求
Request.TYPE_REQUEST_PERMISSION -> showSettingDialog(permissions, rationale) {
lifecycleScope.launch {
try {
val result = requestPermissionsResult.launch(permissions)
if (result.values.all { it }) {
onRequestPermissionFinish()
} else {
openSettingsActivity()
}
} catch (e: Exception) {
AppLog.put("请求权限出错\n$e", e, true)
RequestPlugins.sRequestCallback?.onError(e)
finish()
}
}
}
//跳转到设置界面
Request.TYPE_REQUEST_SETTING -> showSettingDialog(permissions, rationale) {
openSettingsActivity()
}
//所有文件的管理权限
Request.TYPE_MANAGE_ALL_FILES_ACCESS -> showSettingDialog(permissions, rationale) {
try {
if (Permissions.isManageExternalStorage()) {
val settingIntent =
Intent(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
Uri.parse("package:$packageName")
)
settingActivityResult.launch(settingIntent)
} else {
throw NoStackTraceException("no MANAGE_ALL_FILES_ACCESS_PERMISSION")
}
} catch (e: Exception) {
AppLog.put("请求所有文件的管理权限出错\n$e", e, true)
RequestPlugins.sRequestCallback?.onError(e)
finish()
}
}
Request.TYPE_REQUEST_NOTIFICATIONS -> showSettingDialog(permissions, rationale) {
lifecycleScope.launch {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
&& requestPermissionResult.launch(Permissions.POST_NOTIFICATIONS)
) {
onRequestPermissionFinish()
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//这种方案适用于 API 26, 即8.0(含8.0)以上可以用
val intent = Intent()
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, applicationInfo.uid)
settingActivityResult.launch(intent)
} else {
openSettingsActivity()
}
} catch (e: Exception) {
AppLog.put("请求通知权限出错\n$e", e, true)
RequestPlugins.sRequestCallback?.onError(e)
finish()
}
}
}
Request.TYPE_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS -> showSettingDialog(
permissions, rationale
) {
lifecycleScope.launch {
try {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.setData(Uri.parse("package:$packageName"))
val className =
"com.android.settings.fuelgauge.RequestIgnoreBatteryOptimizations"
val activities = packageManager.queryIntentActivities(
intent,
PackageManager.MATCH_DEFAULT_ONLY
)
if (activities.any { it.activityInfo.name == className }) {
val component = intent.resolveActivity(packageManager)
if (component.className != className) {
intent.setClassName("com.android.settings", className)
settingActivityResultAwait.launch(intent)
}
}
intent.component = null
settingActivityResult.launch(intent)
} catch (e: Exception) {
AppLog.put("请求后台权限出错\n$e", e, true)
RequestPlugins.sRequestCallback?.onError(e)
finish()
}
}
}
}
onBackPressedDispatcher.addCallback(this) {
}
}
private fun onRequestPermissionFinish() {
RequestPlugins.sRequestCallback?.onSettingActivityResult()
finish()
}
private fun openSettingsActivity() {
try {
val settingIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
settingIntent.data = Uri.fromParts("package", packageName, null)
settingActivityResult.launch(settingIntent)
} catch (e: Exception) {
toastOnUi(R.string.tip_cannot_jump_setting_page)
RequestPlugins.sRequestCallback?.onError(e)
finish()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
RequestPlugins.sRequestCallback?.onRequestPermissionsResult(
permissions,
grantResults
)
finish()
}
override fun startActivity(intent: Intent) {
super.startActivity(intent)
@Suppress("DEPRECATION")
overridePendingTransition(0, 0)
}
override fun finish() {
super.finish()
@Suppress("DEPRECATION")
overridePendingTransition(0, 0)
}
private fun showSettingDialog(
permissions: Array<String>,
rationale: CharSequence?,
onOk: () -> Unit
) {
rationaleDialog?.dismiss()
if (rationale.isNullOrEmpty()) {
finish()
return
}
rationaleDialog = AlertDialog.Builder(this)
.setTitle(R.string.dialog_title)
.setMessage(rationale)
.setPositiveButton(R.string.dialog_setting) { _, _ ->
onOk.invoke()
}
.setNegativeButton(R.string.dialog_cancel) { _, _ ->
RequestPlugins.sRequestCallback?.onRequestPermissionsResult(
permissions,
IntArray(0)
)
finish()
}.setOnCancelListener {
RequestPlugins.sRequestCallback?.onRequestPermissionsResult(
permissions,
IntArray(0)
)
finish()
}
.show()
}
companion object {
const val KEY_RATIONALE = "KEY_RATIONALE"
const val KEY_INPUT_REQUEST_TYPE = "KEY_INPUT_REQUEST_TYPE"
const val KEY_INPUT_PERMISSIONS_CODE = "KEY_INPUT_PERMISSIONS_CODE"
const val KEY_INPUT_PERMISSIONS = "KEY_INPUT_PERMISSIONS"
}
}
@@ -0,0 +1,90 @@
package io.legato.kazusa.lib.permission
import android.os.Build
@Suppress("unused")
object Permissions {
const val POST_NOTIFICATIONS = "android.permission.POST_NOTIFICATIONS"
const val READ_CALENDAR = "android.permission.READ_CALENDAR"
const val WRITE_CALENDAR = "android.permission.WRITE_CALENDAR"
const val CAMERA = "android.permission.CAMERA"
const val READ_CONTACTS = "android.permission.READ_CONTACTS"
const val WRITE_CONTACTS = "android.permission.WRITE_CONTACTS"
const val GET_ACCOUNTS = "android.permission.GET_ACCOUNTS"
const val ACCESS_FINE_LOCATION = "android.permission.ACCESS_FINE_LOCATION"
const val ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"
const val RECORD_AUDIO = "android.permission.RECORD_AUDIO"
const val READ_PHONE_STATE = "android.permission.READ_PHONE_STATE"
const val CALL_PHONE = "android.permission.CALL_PHONE"
const val READ_CALL_LOG = "android.permission.READ_CALL_LOG"
const val WRITE_CALL_LOG = "android.permission.WRITE_CALL_LOG"
const val ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL"
const val USE_SIP = "android.permission.USE_SIP"
const val PROCESS_OUTGOING_CALLS = "android.permission.PROCESS_OUTGOING_CALLS"
const val BODY_SENSORS = "android.permission.BODY_SENSORS"
const val SEND_SMS = "android.permission.SEND_SMS"
const val RECEIVE_SMS = "android.permission.RECEIVE_SMS"
const val READ_SMS = "android.permission.READ_SMS"
const val RECEIVE_WAP_PUSH = "android.permission.RECEIVE_WAP_PUSH"
const val RECEIVE_MMS = "android.permission.RECEIVE_MMS"
const val READ_EXTERNAL_STORAGE = "android.permission.READ_EXTERNAL_STORAGE"
const val WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE"
const val MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE"
const val ACCESS_MEDIA_LOCATION = "android.permission.ACCESS_MEDIA_LOCATION"
const val REQUEST_IGNORE_BATTERY_OPTIMIZATIONS =
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"
object Group {
val STORAGE = if (isManageExternalStorage()) {
arrayOf(MANAGE_EXTERNAL_STORAGE)
} else {
arrayOf(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE)
}
val CAMERA = arrayOf(Permissions.CAMERA)
val CALENDAR = arrayOf(READ_CALENDAR, WRITE_CALENDAR)
val CONTACTS = arrayOf(READ_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS)
val LOCATION = arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
val MICROPHONE = arrayOf(RECORD_AUDIO)
val PHONE = arrayOf(
READ_PHONE_STATE,
CALL_PHONE,
READ_CALL_LOG,
WRITE_CALL_LOG,
ADD_VOICEMAIL,
USE_SIP,
PROCESS_OUTGOING_CALLS
)
val SENSORS = arrayOf(BODY_SENSORS)
val SMS = arrayOf(
SEND_SMS,
RECEIVE_SMS,
READ_SMS,
RECEIVE_WAP_PUSH,
RECEIVE_MMS
)
}
fun isManageExternalStorage(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
}
}
@@ -0,0 +1,72 @@
package io.legato.kazusa.lib.permission
import androidx.annotation.StringRes
@Suppress("unused")
class PermissionsCompat private constructor() {
private var request: Request? = null
fun request() {
RequestManager.pushRequest(request)
}
class Builder {
private val request: Request = Request()
fun addPermissions(vararg permissions: String): Builder {
request.addPermissions(*permissions)
return this
}
fun onGranted(callback: () -> Unit): Builder {
request.setOnGrantedCallback(object : OnPermissionsGrantedCallback {
override fun onPermissionsGranted() {
callback()
}
})
return this
}
fun onDenied(callback: (deniedPermissions: Array<String>) -> Unit): Builder {
request.setOnDeniedCallback(object : OnPermissionsDeniedCallback {
override fun onPermissionsDenied(deniedPermissions: Array<String>) {
callback(deniedPermissions)
}
})
return this
}
fun onError(callback: (e: Exception) -> Unit): Builder {
request.setOnErrorCallBack(object : OnErrorCallback{
override fun onError(e: Exception) {
callback(e)
}
})
return this
}
fun rationale(rationale: CharSequence): Builder {
request.setRationale(rationale)
return this
}
fun rationale(@StringRes resId: Int): Builder {
request.setRationale(resId)
return this
}
fun build(): PermissionsCompat {
val compat = PermissionsCompat()
compat.request = request
return compat
}
fun request(): PermissionsCompat {
val compat = build()
compat.request()
return compat
}
}
}
@@ -0,0 +1,228 @@
package io.legato.kazusa.lib.permission
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Build
import android.os.Environment
import androidx.annotation.StringRes
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import io.legato.kazusa.utils.startActivity
import splitties.init.appCtx
import splitties.systemservices.powerManager
@Suppress("MemberVisibilityCanBePrivate")
internal class Request : OnRequestPermissionsResultCallback {
internal val requestTime: Long = System.currentTimeMillis()
private var requestCode: Int = TYPE_REQUEST_PERMISSION
private var permissions: ArrayList<String> = ArrayList()
private var grantedCallback: OnPermissionsGrantedCallback? = null
private var deniedCallback: OnPermissionsDeniedCallback? = null
private var errorCallback: OnErrorCallback? = null
private var rationale: CharSequence? = null
private val deniedPermissions: Array<String>?
get() {
return getDeniedPermissions(this.permissions.toTypedArray())
}
fun addPermissions(vararg permissions: String) {
this.permissions.addAll(listOf(*permissions))
}
fun setOnGrantedCallback(callback: OnPermissionsGrantedCallback) {
grantedCallback = callback
}
fun setOnDeniedCallback(callback: OnPermissionsDeniedCallback) {
deniedCallback = callback
}
fun setOnErrorCallBack(callback: OnErrorCallback) {
errorCallback = callback
}
fun setRationale(@StringRes resId: Int) {
rationale = appCtx.getString(resId)
}
fun setRationale(rationale: CharSequence) {
this.rationale = rationale
}
@SuppressLint("ObsoleteSdkInt")
fun start() {
RequestPlugins.setOnRequestPermissionsCallback(this)
val deniedPermissions = deniedPermissions
val rationale = this.rationale
if (deniedPermissions == null) {
onPermissionsGranted()
return
}
if (rationale == null) {
onPermissionsDenied(deniedPermissions)
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
toSetting()
} else {
if (deniedPermissions.contains(Permissions.MANAGE_EXTERNAL_STORAGE)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
toManageFileSetting(deniedPermissions)
}
} else if (deniedPermissions.contains(Permissions.POST_NOTIFICATIONS)) {
toNotificationSetting(deniedPermissions)
} else if (deniedPermissions.contains(Permissions.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
toIgnoreBatterySetting(deniedPermissions)
}
} else if (deniedPermissions.isNotEmpty()) {
appCtx.startActivity<PermissionActivity> {
putExtra(PermissionActivity.KEY_RATIONALE, rationale)
putExtra(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_REQUEST_PERMISSION)
putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode)
putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions)
}
}
}
}
fun clear() {
grantedCallback = null
deniedCallback = null
}
fun getDeniedPermissions(permissions: Array<String>): Array<String>? {
val deniedPermissionList = ArrayList<String>()
for (permission in permissions) {
when (permission) {
Permissions.POST_NOTIFICATIONS -> {
if (!NotificationManagerCompat.from(appCtx).areNotificationsEnabled()) {
deniedPermissionList.add(permission)
}
}
Permissions.MANAGE_EXTERNAL_STORAGE -> {
if (Permissions.isManageExternalStorage()) {
if (!Environment.isExternalStorageManager()) {
deniedPermissionList.add(permission)
}
}
}
Permissions.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!powerManager.isIgnoringBatteryOptimizations(appCtx.packageName)) {
deniedPermissionList.add(permission)
}
}
}
else -> {
if (
ContextCompat.checkSelfPermission(appCtx, permission)
!= PackageManager.PERMISSION_GRANTED
) {
deniedPermissionList.add(permission)
}
}
}
}
val size = deniedPermissionList.size
if (size > 0) {
return deniedPermissionList.toTypedArray()
}
return null
}
private fun onPermissionsGranted() {
try {
grantedCallback?.onPermissionsGranted()
} catch (ignore: Exception) {
}
RequestPlugins.sResultCallback?.onPermissionsGranted()
}
private fun onPermissionsDenied(deniedPermissions: Array<String>) {
try {
deniedCallback?.onPermissionsDenied(deniedPermissions)
} catch (ignore: Exception) {
}
RequestPlugins.sResultCallback?.onPermissionsDenied(deniedPermissions)
}
private fun toSetting() {
appCtx.startActivity<PermissionActivity> {
putExtra(PermissionActivity.KEY_RATIONALE, rationale)
putExtra(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_REQUEST_SETTING)
}
}
private fun toManageFileSetting(deniedPermissions: Array<String>) {
appCtx.startActivity<PermissionActivity> {
putExtra(PermissionActivity.KEY_RATIONALE, rationale)
putExtra(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_MANAGE_ALL_FILES_ACCESS)
putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode)
putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions)
}
}
private fun toNotificationSetting(deniedPermissions: Array<String>) {
appCtx.startActivity<PermissionActivity> {
putExtra(PermissionActivity.KEY_RATIONALE, rationale)
putExtra(PermissionActivity.KEY_INPUT_REQUEST_TYPE, TYPE_REQUEST_NOTIFICATIONS)
putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode)
putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions)
}
}
private fun toIgnoreBatterySetting(deniedPermissions: Array<String>) {
appCtx.startActivity<PermissionActivity> {
putExtra(PermissionActivity.KEY_RATIONALE, rationale)
putExtra(
PermissionActivity.KEY_INPUT_REQUEST_TYPE,
TYPE_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
)
putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS_CODE, requestCode)
putExtra(PermissionActivity.KEY_INPUT_PERMISSIONS, deniedPermissions)
}
}
override fun onRequestPermissionsResult(
permissions: Array<String>,
grantResults: IntArray
) {
val deniedPermissions = getDeniedPermissions(permissions)
if (deniedPermissions != null) {
onPermissionsDenied(deniedPermissions)
} else {
onPermissionsGranted()
}
}
override fun onSettingActivityResult() {
val deniedPermissions = deniedPermissions
if (deniedPermissions == null) {
onPermissionsGranted()
} else {
onPermissionsDenied(deniedPermissions)
}
}
override fun onError(e: Exception) {
errorCallback?.onError(e)
RequestPlugins.sResultCallback?.onError(e)
}
companion object {
const val TYPE_REQUEST_PERMISSION = 1
const val TYPE_REQUEST_SETTING = 2
const val TYPE_MANAGE_ALL_FILES_ACCESS = 3
const val TYPE_REQUEST_NOTIFICATIONS = 4
const val TYPE_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = 5
}
}
@@ -0,0 +1,73 @@
package io.legato.kazusa.lib.permission
import android.os.Handler
import android.os.Looper
import java.util.*
internal object RequestManager : OnPermissionsResultCallback {
private var requests: Stack<Request>? = null
private var request: Request? = null
private val handler = Handler(Looper.getMainLooper())
private val requestRunnable = Runnable {
request?.start()
}
private val isCurrentRequestInvalid: Boolean
get() = request?.let { System.currentTimeMillis() - it.requestTime > 5 * 1000L } ?: true
init {
RequestPlugins.setOnPermissionsResultCallback(this)
}
fun pushRequest(request: Request?) {
if (request == null) return
if (requests == null) {
requests = Stack()
}
requests?.let {
val index = it.indexOf(request)
if (index >= 0) {
val to = it.size - 1
if (index != to) {
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
Collections.swap(requests, index, to)
}
} else {
it.push(request)
}
if (!it.empty() && isCurrentRequestInvalid) {
this.request = it.pop()
handler.post(requestRunnable)
}
}
}
private fun startNextRequest() {
request?.clear()
request = null
requests?.let {
request = if (it.empty()) null else it.pop()
request?.let { handler.post(requestRunnable) }
}
}
override fun onPermissionsGranted() {
startNextRequest()
}
override fun onPermissionsDenied(deniedPermissions: Array<String>?) {
startNextRequest()
}
override fun onError(e: Exception) {
startNextRequest()
}
}
@@ -0,0 +1,20 @@
package io.legato.kazusa.lib.permission
internal object RequestPlugins {
@Volatile
var sRequestCallback: OnRequestPermissionsResultCallback? = null
@Volatile
var sResultCallback: OnPermissionsResultCallback? = null
fun setOnRequestPermissionsCallback(callback: OnRequestPermissionsResultCallback) {
sRequestCallback = callback
}
fun setOnPermissionsResultCallback(callback: OnPermissionsResultCallback) {
sResultCallback = callback
}
}