Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
Expand All @@ -39,6 +41,9 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.PermissionStatus
Expand All @@ -59,6 +64,10 @@ internal fun ColumnScope.AccessRightsPage(
onDisEnabledButtonClick: () -> Unit,
) {
val context = LocalContext.current

val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsStateWithLifecycle()

val permissionList = rememberMultiplePermissionsState(
listOfNotNull(
when {
Expand All @@ -70,6 +79,7 @@ internal fun ColumnScope.AccessRightsPage(
READ_CONTACTS
)
)

val galleryPermission = permissionList.permissions
.find {
it.permission in when {
Expand All @@ -78,15 +88,28 @@ internal fun ColumnScope.AccessRightsPage(
else -> setOf(READ_MEDIA_IMAGES, READ_MEDIA_VISUAL_USER_SELECTED)
}
}
val notificationPermission = permissionList.permissions
.find { if (SDK_INT >= TIRAMISU) it.permission == POST_NOTIFICATIONS else true }

val notificationPermission = if (SDK_INT >= TIRAMISU) {
permissionList.permissions.find { it.permission == POST_NOTIFICATIONS }
} else null

val contactsPermission = permissionList.permissions
.find { it.permission == READ_CONTACTS }

BackHandler { onBackClick() }

LaunchedEffect(permissionList) {
permissionList.launchMultiplePermissionRequest()
val isNotificationGranted = remember(
lifecycleState,
notificationPermission?.status
) {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}

LaunchedEffect(Unit) {
val hasDenied = permissionList.permissions.any { it.status != PermissionStatus.Granted }
if (hasDenied) {
permissionList.launchMultiplePermissionRequest()
}
}

PieceSubBackTopBar(
Expand Down Expand Up @@ -128,8 +151,14 @@ internal fun ColumnScope.AccessRightsPage(
icon = R.drawable.ic_permission_alarm,
label = stringResource(R.string.permission_notification),
description = stringResource(R.string.permission_notification_description),
checked = notificationPermission?.status == PermissionStatus.Granted,
onCheckedChange = { handlePermission(context, notificationPermission) },
checked = isNotificationGranted,
onCheckedChange = {
if (SDK_INT >= TIRAMISU) {
handlePermission(context, notificationPermission)
} else {
navigateToAppSettings(context)
}
},
)

PiecePermissionRow(
Expand Down Expand Up @@ -222,16 +251,21 @@ private fun PiecePermissionRow(
internal fun handlePermission(context: Context, permission: PermissionState?) {
permission?.let {
if (it.status == PermissionStatus.Granted || !it.status.shouldShowRationale) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
navigateToAppSettings(context)
} else {
it.launchPermissionRequest()
}
}
}

private fun navigateToAppSettings(context: Context) {
context.startActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
)
}

@Preview
@Composable
private fun AccessRightsPagePreview() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.NotificationManagerCompat
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
Expand Down Expand Up @@ -219,13 +220,30 @@ private fun NotificationBody(
isPushNotificationEnabled: Boolean,
onPushNotificationCheckedChange: () -> Unit,
) {

val context = LocalContext.current

val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleState by lifecycleOwner.lifecycle
.currentStateFlow
.collectAsStateWithLifecycle()

val notificationPermission =
if (SDK_INT >= TIRAMISU) rememberPermissionState(POST_NOTIFICATIONS)
else null
val isPermissionGranted = notificationPermission?.status == PermissionStatus.Granted
|| notificationPermission == null

val context = LocalContext.current
val isPermissionGranted = remember(
lifecycleState,
notificationPermission?.status
) {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}

LaunchedEffect(isPermissionGranted) {
if (isPermissionGranted && !isPushNotificationEnabled) {
onPushNotificationCheckedChange()
}
}
Comment on lines +242 to +246
Copy link

@coderabbitai coderabbitai bot Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential unintended auto-enable of push notifications.

This LaunchedEffect automatically calls onPushNotificationCheckedChange() when permission becomes granted and push notifications are disabled. This could cause issues:

  1. If the user intentionally disabled push notifications in the app but has system permission enabled, this would re-enable them on every screen visit.
  2. On first composition where permission is already granted, this will always trigger.

Consider tracking whether the user explicitly granted permission in this session vs. permission already being granted.

💡 Suggested approach

Track whether this is a fresh permission grant vs. pre-existing:

+    var wasPermissionGrantedOnEntry by remember { mutableStateOf(isPermissionGranted) }
+
     LaunchedEffect(isPermissionGranted) {
-        if (isPermissionGranted && !isPushNotificationEnabled) {
+        if (isPermissionGranted && !wasPermissionGrantedOnEntry && !isPushNotificationEnabled) {
             onPushNotificationCheckedChange()
         }
     }
🤖 Prompt for AI Agents
In @feature/setting/src/main/java/com/puzzle/setting/graph/main/SettingScreen.kt
around lines 242 - 246, The current LaunchedEffect triggers on any
isPermissionGranted truthy state and calls onPushNotificationCheckedChange(),
which can unintentionally re-enable notifications on first composition or when
permission was already granted; change this to detect a transition from
false->true by adding a remembered state like previousPermissionGranted
(rememberSaveable { isPermissionGranted }) or previousPermissionGranted =
remember { mutableStateOf(isPermissionGranted) }, run
onPushNotificationCheckedChange() only when previousPermissionGranted == false
&& isPermissionGranted == true && !isPushNotificationEnabled, and then update
previousPermissionGranted = isPermissionGranted so only fresh grants (not
pre-existing permissions) trigger the callback.

Copy link
Collaborator Author

@comst19 comst19 Jan 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

알람 정책:
시스템 알림 권한을 다시 활성화한 경우, 사용자가 알림 수신 의사를 회복한 것으로 판단하여 앱 및 서버 푸시 설정을 자동으로

설정 화면의 토글은 실제 알림 수신 가능 상태를 나타냄

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@comst19, 이해했습니다! 시스템 알림 권한을 다시 활성화하는 것을 사용자의 명시적인 알림 수신 의사로 간주하여 앱 및 서버 푸시 설정을 자동으로 활성화하는 정책이군요.

이 접근 방식은 사용자가 설정에서 시스템 권한을 다시 켜는 행동 자체를 "알림을 다시 받고 싶다"는 의사 표현으로 해석하는 것이라 합리적입니다. 명확한 설명 감사합니다!


✏️ Learnings added
Learnt from: comst19
Repo: Piece-Puzzly/Piece-Android PR: 203
File: feature/setting/src/main/java/com/puzzle/setting/graph/main/SettingScreen.kt:242-246
Timestamp: 2026-01-10T10:27:20.895Z
Learning: In the Piece-Android project's notification policy: When users re-enable system notification permission (after previously disabling it), the app automatically turns ON both app and server push notification settings, interpreting this as the user's intent to receive notifications again.

Learnt from: comst19
Repo: Piece-Puzzly/Piece-Android PR: 203
File: feature/setting/src/main/java/com/puzzle/setting/graph/main/SettingScreen.kt:242-246
Timestamp: 2026-01-10T10:27:20.895Z
Learning: In the Piece-Android project's SettingScreen.kt and AccessRightsPage.kt: The notification toggle represents the actual notification receivable state (combining system permission and app setting), and when system permission becomes granted, the app automatically enables push notifications through onPushNotificationCheckedChange() callback - this is intentional behavior per the app's notification policy.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


Text(
text = stringResource(R.string.setting_notification),
Expand Down Expand Up @@ -253,7 +271,11 @@ private fun NotificationBody(
if (isPermissionGranted) {
onPushNotificationCheckedChange()
} else {
handlePermission(context, notificationPermission)
if (SDK_INT >= TIRAMISU) {
handlePermission(context, notificationPermission)
} else {
navigateToAppSettings(context)
}
}
}
)
Expand Down Expand Up @@ -399,19 +421,24 @@ private fun SystemSettingBody(
}

@OptIn(ExperimentalPermissionsApi::class)
internal fun handlePermission(context: Context, permission: PermissionState?) {
private fun handlePermission(context: Context, permission: PermissionState?) {
permission?.let {
if (it.status == PermissionStatus.Granted || !it.status.shouldShowRationale) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
context.startActivity(intent)
navigateToAppSettings(context)
} else {
it.launchPermissionRequest()
}
}
}

private fun navigateToAppSettings(context: Context) {
context.startActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
)
}

@Composable
private fun InquiryBody(onContactUsClick: () -> Unit) {
Text(
Expand Down