diff --git a/res/values/strings.xml b/res/values/strings.xml
index 405960bcca8..484c9c4aa8a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -8164,6 +8164,12 @@
Never show notifications in the shade or on peripheral devices
+
+ Allow full screen notifications
+
+
+ Allow notifications to take up the full screen when the device is locked
+
Allow notification dot
diff --git a/res/xml/app_notification_settings.xml b/res/xml/app_notification_settings.xml
index 9857a4d1db7..f96a375224e 100644
--- a/res/xml/app_notification_settings.xml
+++ b/res/xml/app_notification_settings.xml
@@ -59,38 +59,57 @@
+ android:summary="@string/allow_interruption_summary"
+ settings:allowDividerAbove="true"
+ settings:allowDividerBelow="false" />
+ android:title="@string/app_notification_visibility_override_title"
+ settings:allowDividerAbove="true"
+ settings:allowDividerBelow="false" />
+ android:summary="@string/app_notification_override_dnd_summary"
+ settings:allowDividerAbove="true"
+ settings:allowDividerBelow="false" />
+
+
+
+ settings:allowDividerBelow="false" />
+ android:title="@string/app_settings_link"
+ android:order="1003"
+ settings:allowDividerAbove="true"
+ settings:allowDividerBelow="false" />
+ android:order="8000"
+ settings:allowDividerAbove="true"
+ settings:allowDividerBelow="false" />
diff --git a/src/com/android/settings/notification/app/AppNotificationSettings.java b/src/com/android/settings/notification/app/AppNotificationSettings.java
index 4ebac0f497d..ee9ec4565d2 100644
--- a/src/com/android/settings/notification/app/AppNotificationSettings.java
+++ b/src/com/android/settings/notification/app/AppNotificationSettings.java
@@ -74,6 +74,7 @@ public class AppNotificationSettings extends NotificationSettings {
mControllers = new ArrayList<>();
mControllers.add(new HeaderPreferenceController(context, this));
mControllers.add(new BlockPreferenceController(context, mDependentFieldListener, mBackend));
+ mControllers.add(new FullScreenIntentPermissionPreferenceController(context, mBackend));
mControllers.add(new BadgePreferenceController(context, mBackend));
mControllers.add(new AllowSoundPreferenceController(
context, mDependentFieldListener, mBackend));
diff --git a/src/com/android/settings/notification/app/FullScreenIntentPermissionPreferenceController.kt b/src/com/android/settings/notification/app/FullScreenIntentPermissionPreferenceController.kt
new file mode 100644
index 00000000000..ec997498bfe
--- /dev/null
+++ b/src/com/android/settings/notification/app/FullScreenIntentPermissionPreferenceController.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.notification.app
+
+import android.Manifest.permission.USE_FULL_SCREEN_INTENT
+import android.app.AppOpsManager
+import android.app.AppOpsManager.OP_USE_FULL_SCREEN_INTENT
+import android.content.AttributionSource
+import android.content.Context
+import android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET
+import android.content.pm.PackageManager.GET_PERMISSIONS
+import android.os.UserHandle
+import android.permission.PermissionManager
+import android.util.Log
+import androidx.preference.Preference
+import androidx.preference.Preference.OnPreferenceChangeListener
+import com.android.settings.notification.NotificationBackend
+import com.android.settingslib.RestrictedSwitchPreference
+
+class FullScreenIntentPermissionPreferenceController(
+ context: Context,
+ backend: NotificationBackend
+) : NotificationPreferenceController(context, backend), OnPreferenceChangeListener {
+ private val packageManager = mPm!!
+ private val permissionManager = context.getSystemService(PermissionManager::class.java)!!
+ private val appOpsManager = context.getSystemService(AppOpsManager::class.java)!!
+
+ private val packageName get() = mAppRow.pkg
+ private val uid get() = mAppRow.uid
+ private val userHandle get() = UserHandle.getUserHandleForUid(uid)
+
+ override fun getPreferenceKey() = KEY_FSI_PERMISSION
+
+ override fun isAvailable(): Boolean {
+ val inAppWidePreferences = mChannelGroup == null && mChannel == null
+
+ if (!inAppWidePreferences) {
+ Log.wtf(TAG, "Belongs only in app-wide notification preferences!")
+ }
+
+ return super.isAvailable() && inAppWidePreferences && isPermissionRequested()
+ }
+
+ override fun isIncludedInFilter() = false
+
+ override fun updateState(preference: Preference) {
+ check(KEY_FSI_PERMISSION.equals(preference.key))
+ check(preference is RestrictedSwitchPreference)
+
+ preference.setDisabledByAdmin(mAdmin)
+ preference.isEnabled = !preference.isDisabledByAdmin
+ preference.isChecked = isPermissionGranted()
+ }
+
+ override fun onPreferenceChange(preference: Preference, value: Any): Boolean {
+ check(KEY_FSI_PERMISSION.equals(preference.key))
+ check(preference is RestrictedSwitchPreference)
+ check(value is Boolean)
+
+ if (isPermissionGranted() != value) {
+ setPermissionGranted(value)
+ }
+
+ return true
+ }
+
+ private fun isPermissionRequested(): Boolean {
+ val packageInfo = packageManager.getPackageInfo(packageName, GET_PERMISSIONS)
+
+ for (requestedPermission in packageInfo.requestedPermissions) {
+ if (USE_FULL_SCREEN_INTENT.equals(requestedPermission)) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ private fun isPermissionGranted(): Boolean {
+ val attributionSource = AttributionSource.Builder(uid).setPackageName(packageName).build()
+
+ val permissionResult =
+ permissionManager.checkPermissionForPreflight(USE_FULL_SCREEN_INTENT, attributionSource)
+
+ return (permissionResult == PermissionManager.PERMISSION_GRANTED)
+ }
+
+ private fun setPermissionGranted(allowed: Boolean) {
+ val mode = if (allowed) AppOpsManager.MODE_ALLOWED else AppOpsManager.MODE_ERRORED
+ appOpsManager.setUidMode(OP_USE_FULL_SCREEN_INTENT, uid, mode)
+ packageManager.updatePermissionFlags(
+ USE_FULL_SCREEN_INTENT,
+ packageName,
+ FLAG_PERMISSION_USER_SET,
+ FLAG_PERMISSION_USER_SET,
+ userHandle
+ )
+ }
+
+ companion object {
+ const val KEY_FSI_PERMISSION = "fsi_permission"
+ const val TAG = "FsiPermPrefController"
+ }
+}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/notification/app/FullScreenIntentPermissionPreferenceControllerTest.kt b/tests/robotests/src/com/android/settings/notification/app/FullScreenIntentPermissionPreferenceControllerTest.kt
new file mode 100644
index 00000000000..e148f71d1c0
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/notification/app/FullScreenIntentPermissionPreferenceControllerTest.kt
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.notification.app
+
+import android.Manifest.permission.USE_FULL_SCREEN_INTENT
+import android.app.AppOpsManager
+import android.app.AppOpsManager.MODE_ALLOWED
+import android.app.AppOpsManager.MODE_ERRORED
+import android.app.AppOpsManager.OP_USE_FULL_SCREEN_INTENT
+import android.app.NotificationChannel
+import android.app.NotificationChannelGroup
+import android.app.NotificationManager.IMPORTANCE_UNSPECIFIED
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET
+import android.content.pm.PackageManager.GET_PERMISSIONS
+import android.os.UserHandle
+import android.permission.PermissionManager.PERMISSION_GRANTED
+import android.permission.PermissionManager.PERMISSION_HARD_DENIED
+import android.permission.PermissionManager.PERMISSION_SOFT_DENIED
+import android.permission.PermissionManager.PermissionResult
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import androidx.test.core.app.ApplicationProvider
+import com.android.settings.notification.NotificationBackend
+import com.android.settings.notification.NotificationBackend.AppRow
+import com.android.settings.notification.app.FullScreenIntentPermissionPreferenceController.Companion.KEY_FSI_PERMISSION
+import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
+import com.android.settingslib.RestrictedSwitchPreference
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Answers
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowApplicationPackageManager
+import org.robolectric.shadows.ShadowPermissionChecker
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(RobolectricTestRunner::class)
+@Config(shadows = [ShadowApplicationPackageManager::class])
+class FullScreenIntentPermissionPreferenceControllerTest {
+ @JvmField
+ @Rule
+ val mockitoRule = MockitoJUnit.rule()!!
+
+ @Mock
+ private lateinit var packageManager: PackageManager
+
+ @Mock
+ private lateinit var appOpsManager: AppOpsManager
+
+ private lateinit var preference: RestrictedSwitchPreference
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private lateinit var screen: PreferenceScreen
+
+ private lateinit var controller: FullScreenIntentPermissionPreferenceController
+
+ @Before
+ fun setUp() {
+ val context = spy(ApplicationProvider.getApplicationContext())
+
+ whenever(context.packageManager).thenReturn(packageManager)
+ whenever(context.getSystemService(AppOpsManager::class.java)).thenReturn(appOpsManager)
+
+ preference = RestrictedSwitchPreference(context).apply { key = KEY_FSI_PERMISSION }
+
+ whenever(screen.findPreference(KEY_FSI_PERMISSION)).thenReturn(preference)
+
+ controller = FullScreenIntentPermissionPreferenceController(
+ context,
+ mock(NotificationBackend::class.java)
+ )
+ }
+
+ @Test
+ fun testIsAvailable_notWhenPermissionNotRequested() {
+ setPermissionRequestedInManifest(requested = false)
+ initController()
+
+ assertFalse(controller.isAvailable)
+ }
+
+ @Test
+ fun testIsAvailable_notWhenOnChannelScreen() {
+ setPermissionRequestedInManifest()
+ initController(channel = makeTestChannel())
+
+ assertFalse(controller.isAvailable)
+ }
+
+ @Test
+ fun testIsAvailable_notWhenOnGroupScreen() {
+ setPermissionRequestedInManifest()
+ initController(group = makeTestGroup())
+
+ assertFalse(controller.isAvailable)
+ }
+
+ @Test
+ fun testIsAvailable_onAppScreenWhenRequested() {
+ setPermissionRequestedInManifest()
+ initController()
+
+ assertTrue(controller.isAvailable)
+ }
+
+ @Test
+ fun testIsEnabled_notWhenDisabledByAdmin() {
+ setPermissionRequestedInManifest()
+ initController(admin = makeTestAdmin())
+
+ controller.updateState(preference)
+
+ assertFalse(preference.isEnabled)
+ }
+
+ @Test
+ fun testIsEnabled_whenNotDisabledByAdmin() {
+ setPermissionRequestedInManifest()
+ initController()
+
+ controller.updateState(preference)
+
+ assertTrue(preference.isEnabled)
+ }
+
+ @Test
+ fun testIsChecked_notWhenHardDenied() {
+ setPermissionRequestedInManifest()
+ setPermissionResult(PERMISSION_HARD_DENIED)
+ initController()
+
+ controller.updateState(preference)
+
+ assertFalse(preference.isChecked)
+ }
+
+ @Test
+ fun testIsChecked_notWhenSoftDenied() {
+ setPermissionRequestedInManifest()
+ setPermissionResult(PERMISSION_SOFT_DENIED)
+ initController()
+
+ controller.updateState(preference)
+
+ assertFalse(preference.isChecked)
+ }
+
+ @Test
+ fun testIsChecked_whenGranted() {
+ setPermissionRequestedInManifest()
+ setPermissionResult(PERMISSION_GRANTED)
+ initController()
+
+ controller.updateState(preference)
+
+ assertTrue(preference.isChecked)
+ }
+
+ @Test
+ fun testOnPreferenceChange_whenHardDenied() {
+ setPermissionRequestedInManifest()
+ setPermissionResult(PERMISSION_HARD_DENIED)
+ initController()
+ controller.displayPreference(screen)
+ controller.updateState(preference)
+ assertFalse(preference.isChecked)
+
+ setPreferenceChecked(true)
+
+ verifySetAppOpMode(MODE_ALLOWED)
+ verifySetPermissionUserSetFlag()
+ }
+
+ @Test
+ fun testOnPreferenceChange_whenSoftDenied() {
+ setPermissionRequestedInManifest()
+ setPermissionResult(PERMISSION_SOFT_DENIED)
+ initController()
+ controller.displayPreference(screen)
+ controller.updateState(preference)
+ assertFalse(preference.isChecked)
+
+ setPreferenceChecked(true)
+
+ verifySetAppOpMode(MODE_ALLOWED)
+ verifySetPermissionUserSetFlag()
+ }
+
+ @Test
+ fun testOnPreferenceChange_whenGranted() {
+ setPermissionRequestedInManifest()
+ setPermissionResult(PERMISSION_GRANTED)
+ initController()
+ controller.displayPreference(screen)
+ controller.updateState(preference)
+ assertTrue(preference.isChecked)
+
+ setPreferenceChecked(false)
+
+ verifySetAppOpMode(MODE_ERRORED)
+ verifySetPermissionUserSetFlag()
+ }
+
+ private fun setPermissionRequestedInManifest(
+ requested: Boolean = true
+ ) {
+ whenever(packageManager.getPackageInfo(TEST_PACKAGE, GET_PERMISSIONS)).thenReturn(
+ PackageInfo().apply {
+ packageName = TEST_PACKAGE
+ applicationInfo = ApplicationInfo().apply { packageName = TEST_PACKAGE }
+ requestedPermissions = if (requested) arrayOf(USE_FULL_SCREEN_INTENT) else arrayOf()
+ })
+ }
+
+ private fun setPermissionResult(@PermissionResult result: Int) {
+ ShadowPermissionChecker.setResult(TEST_PACKAGE, USE_FULL_SCREEN_INTENT, result)
+ }
+
+ private fun setPreferenceChecked(checked: Boolean) {
+ preference.isChecked = checked
+
+ /* This shouldn't be necessary, but for some reason it's not called automatically when
+ isChecked is changed. */
+ controller.onPreferenceChange(preference, checked)
+ }
+
+ private fun verifySetAppOpMode(@AppOpsManager.Mode expectedMode: Int) {
+ verify(appOpsManager).setUidMode(OP_USE_FULL_SCREEN_INTENT, TEST_UID, expectedMode)
+ }
+
+ private fun verifySetPermissionUserSetFlag() {
+ verify(packageManager).updatePermissionFlags(
+ USE_FULL_SCREEN_INTENT,
+ TEST_PACKAGE,
+ FLAG_PERMISSION_USER_SET,
+ FLAG_PERMISSION_USER_SET,
+ makeTestUserHandle()
+ )
+ }
+
+ private fun initController(
+ channel: NotificationChannel? = null,
+ group: NotificationChannelGroup? = null,
+ admin: EnforcedAdmin? = null
+ ) {
+ controller.onResume(
+ makeTestAppRow(),
+ channel,
+ group,
+ /* conversationDrawable = */null,
+ /* conversationInfo = */null,
+ admin,
+ /* preferenceFilter = */null
+ )
+ }
+
+ private fun makeTestChannel() =
+ NotificationChannel("test_channel_id", "Test Channel Name", IMPORTANCE_UNSPECIFIED)
+
+ private fun makeTestGroup() = NotificationChannelGroup("test_group_id", "Test Group Name")
+
+ private fun makeTestAppRow() = AppRow().apply { pkg = TEST_PACKAGE; uid = TEST_UID }
+
+ private fun makeTestUserHandle() = UserHandle.getUserHandleForUid(TEST_UID)
+
+ private fun makeTestAdmin() = mock(EnforcedAdmin::class.java)
+
+ private companion object {
+ const val TEST_PACKAGE = "test.package.name"
+ const val TEST_UID = 12345
+ }
+}
\ No newline at end of file