Skip to content

Commit

Permalink
adapt to api 33 (andrid 13)
Browse files Browse the repository at this point in the history
  • Loading branch information
ichenhe committed Nov 11, 2023
2 parents b57d007 + ab64181 commit 564c5b1
Show file tree
Hide file tree
Showing 13 changed files with 623 additions and 108 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ android {
defaultConfig {
applicationId = "cc.chenhe.qqnotifyevo"
minSdk = 26
targetSdk = 31
targetSdk = 33
versionCode = vCode
versionName = vName
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<queries>
<package android:name="com.tencent.mobileqq" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package cc.chenhe.qqnotifyevo.core

import android.Manifest
import android.annotation.SuppressLint
import android.app.Notification
import android.content.Context
import android.service.notification.StatusBarNotification
Expand All @@ -9,6 +11,7 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import cc.chenhe.qqnotifyevo.utils.NotifyChannel
import cc.chenhe.qqnotifyevo.utils.Tag
import cc.chenhe.qqnotifyevo.utils.hasPermission
import kotlinx.coroutines.CoroutineScope
import timber.log.Timber
import java.util.*
Expand Down Expand Up @@ -60,8 +63,11 @@ class InnerNotificationProcessor(
}

private fun sendNotification(context: Context, tag: Tag, id: Int, notification: Notification) {
NotificationManagerCompat.from(context).notify(id, notification)
addNotifyId(tag, id)
@SuppressLint("MissingPermission")
if (context.hasPermission(Manifest.permission.POST_NOTIFICATIONS)) {
NotificationManagerCompat.from(context).notify(id, notification)
addNotifyId(tag, id)
}
}

override fun renewQzoneNotification(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package cc.chenhe.qqnotifyevo.core

import android.Manifest
import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
Expand Down Expand Up @@ -51,7 +53,8 @@ class NevoNotificationProcessor(context: Context, scope: CoroutineScope) :
// 目前关联账号的消息都会合并
return
}
if (nevoMultiMsgTip(ctx)) {
@SuppressLint("MissingPermission")
if (nevoMultiMsgTip(ctx) && ctx.hasPermission(Manifest.permission.POST_NOTIFICATIONS)) {
val dontShow = PendingIntent.getBroadcast(
ctx, REQ_MULTI_MSG_DONT_SHOW,
Intent(ctx, StaticReceiver::class.java).also {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ private fun NotificationGroup(
onIntent: (AdvancedOptionsIntent) -> Unit,
) {
PreferenceGroup(groupTitle = stringResource(id = R.string.pref_cate_advanced_notify)) {
// 显式特别关心前缀
// 显示特别关心前缀
PreferenceItem(title = stringResource(id = R.string.pref_advanced_show_special_prefix),
icon = Icons.Rounded.Favorite,
description = stringResource(id = R.string.pref_advanced_show_special_prefix_summary),
Expand Down Expand Up @@ -298,7 +298,7 @@ private fun OtherGroup(
}
)
PreferenceDivider()
// 在最近应用列表显式
// 在最近应用列表显示
PreferenceItem(
title = stringResource(id = R.string.pref_show_in_recent),
icon = Icons.Rounded.TableRows,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,16 @@ private fun PreferencePreview() {
}

@Composable
internal fun PreferenceGroup(groupTitle: String, content: @Composable ColumnScope.() -> Unit) {
internal fun PreferenceGroup(groupTitle: String?, content: @Composable ColumnScope.() -> Unit) {
Column {
Text(
text = groupTitle,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(start = 24.dp, bottom = 8.dp),
color = MaterialTheme.colorScheme.secondary
)
if (groupTitle != null) {
Text(
text = groupTitle,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.padding(start = 24.dp, bottom = 8.dp),
color = MaterialTheme.colorScheme.secondary
)
}
Card(content = content, modifier = Modifier.animateContentSize())
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package cc.chenhe.qqnotifyevo.ui.common.permission

import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.os.SystemClock
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.core.app.ActivityCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import cc.chenhe.qqnotifyevo.utils.getActivity

/**
* If the time (ms) between requesting permission and being rejected is less than this threshold,
* it may be permanently rejected.
*/
private const val ALWAYS_DENY_THRESHOLD = 200


/**
* Creates a [MutablePermissionState] that is remembered across compositions.
*
* It's recommended that apps exercise the permissions workflow as described in the
* [documentation](https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions).
*
* @param permission the permission to control and observe.
* @param onPermissionResult will be called with whether or not the user granted the permission
* after [PermissionState.launchPermissionRequest] is called.
* @param onAlwaysDenied will be called if the user denied the permission and
* `shouldShowRationale=false` after [PermissionState.launchPermissionRequest] is called.
* It doesn't affect the calling of [onPermissionResult].
* @param permissionChecker can custom the logic of permission checking.
* @param alwaysRefreshPermissionStatus refresh the permission status, even if current status is
* [PermissionStatus.Granted]. Normally it is unnecessary because denying a permission triggers a
* process restart.
*/
@Composable
internal fun rememberMutablePermissionState(
permission: String,
onPermissionResult: (Boolean) -> Unit,
onAlwaysDenied: () -> Unit,
permissionChecker: ((permission: String) -> PermissionStatus)?,
alwaysRefreshPermissionStatus: Boolean = false,
): MutablePermissionState {
val ctx = LocalContext.current
val inspectMode = LocalInspectionMode.current
val permissionState = remember(permission, permissionChecker) {
val activity = try {
ctx.getActivity()
} catch (e: IllegalStateException) {
if (inspectMode) {
null
} else {
throw e
}
}
MutablePermissionState(permission, ctx, activity, permissionChecker)
}

// Refresh the permission status when the lifecycle is resumed
PermissionLifecycleCheckerEffect(
permissionState = permissionState,
alwaysRefreshPermissionStatus = alwaysRefreshPermissionStatus
)

val launcher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) {
permissionState.refreshPermissionStatus()
if (!it && !(permissionState.status as PermissionStatus.Denied).shouldShowRationale
&& SystemClock.elapsedRealtime() - permissionState.launchTime < ALWAYS_DENY_THRESHOLD
) {
onAlwaysDenied()
}
onPermissionResult(it)
}
DisposableEffect(permissionState, launcher) {
permissionState.launcher = launcher
onDispose {
permissionState.launcher = null
}
}

return permissionState
}

/**
* Effect that updates the `hasPermission` state of a revoked [MutablePermissionState] permission
* when the lifecycle gets called with [lifecycleEvent].
*
* @param alwaysRefreshPermissionStatus refresh the permission status, even if current status is
* [PermissionStatus.Granted]. Normally it is unnecessary because denying a permission triggers a
* process restart.
*/
@Composable
internal fun PermissionLifecycleCheckerEffect(
permissionState: MutablePermissionState,
lifecycleEvent: Lifecycle.Event = Lifecycle.Event.ON_RESUME,
alwaysRefreshPermissionStatus: Boolean = false,
) {
val observer = remember(permissionState) {
LifecycleEventObserver { _, event ->
if (event == lifecycleEvent) {
// We don't check if the permission was denied as that triggers a process restart.
if (alwaysRefreshPermissionStatus || permissionState.status != PermissionStatus.Granted) {
permissionState.refreshPermissionStatus()
}
}
}
}
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(key1 = lifecycle, observer) {
lifecycle.addObserver(observer)
onDispose {
lifecycle.removeObserver(observer)
}
}
}

/**
* A mutable state object that can be used to control and observe permission status changes.
*
* In most cases, this will be created via [rememberMutablePermissionState].
*
* @param permission the permission to control and observe.
* @param context to check the status of the [permission].
* @param activity to check if the user should be presented with a rationale for [permission].
* should never be null unless in compose preview.
* @param permissionChecker can custom the logic of permission checking.
*/
@Stable
internal class MutablePermissionState(
override val permission: String,
private val context: Context,
private val activity: Activity?,
private val permissionChecker: ((permission: String) -> PermissionStatus)?,
) : PermissionState {
override var status: PermissionStatus by mutableStateOf(getPermissionStatus())

internal var launcher: ActivityResultLauncher<String>? = null

internal var launchTime: Long = 0
override fun launchPermissionRequest() {
launchTime = SystemClock.elapsedRealtime()
launcher?.launch(permission)
?: throw IllegalStateException("ActivityResultLauncher cannot be null")
}

internal fun refreshPermissionStatus() {
status = getPermissionStatus()
}

private fun getPermissionStatus(): PermissionStatus {
if (permissionChecker != null) {
return permissionChecker.invoke(permission)
}
val hasPermission =
context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
return if (hasPermission) {
PermissionStatus.Granted
} else {
if (activity == null) {
PermissionStatus.Denied(false)
} else {
PermissionStatus.Denied(
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
)
}
}
}
}
Loading

0 comments on commit 564c5b1

Please sign in to comment.