diff --git a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt index 2439c20a19f..9b363358fc8 100644 --- a/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt +++ b/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppList.kt @@ -21,12 +21,14 @@ import android.app.AlarmManager import android.app.compat.CompatChanges import android.content.Context import android.content.pm.ApplicationInfo +import android.os.PowerExemptionManager import androidx.compose.runtime.Composable import androidx.compose.runtime.livedata.observeAsState import com.android.settings.R +import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spaprivileged.model.app.AppRecord +import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.model.app.PackageManagers -import com.android.settingslib.spaprivileged.model.app.PackageManagers.hasRequestPermission import com.android.settingslib.spaprivileged.model.app.userHandle import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListModel import com.android.settingslib.spaprivileged.template.app.TogglePermissionAppListProvider @@ -41,12 +43,14 @@ object AlarmsAndRemindersAppListProvider : TogglePermissionAppListProvider { data class AlarmsAndRemindersAppRecord( override val app: ApplicationInfo, + val isTrumped: Boolean, val isChangeable: Boolean, var controller: AlarmsAndRemindersController, ) : AppRecord class AlarmsAndRemindersAppListModel( private val context: Context, + private val packageManagers: IPackageManagers = PackageManagers, ) : TogglePermissionAppListModel { override val pageTitleResId = R.string.alarms_and_reminders_title override val switchTitleResId = R.string.alarms_and_reminders_switch_title @@ -61,8 +65,9 @@ class AlarmsAndRemindersAppListModel( } } - override fun transformItem(app: ApplicationInfo) = + override fun transformItem(app: ApplicationInfo) = with(packageManagers) { createRecord(app = app, hasRequestPermission = app.hasRequestPermission(PERMISSION)) + } override fun filter( userIdFlow: Flow, @@ -73,7 +78,8 @@ class AlarmsAndRemindersAppListModel( @Composable override fun isAllowed(record: AlarmsAndRemindersAppRecord) = - record.controller.isAllowed.observeAsState() + if (record.isTrumped) stateOf(true) + else record.controller.isAllowed.observeAsState() override fun isChangeable(record: AlarmsAndRemindersAppRecord) = record.isChangeable @@ -81,17 +87,38 @@ class AlarmsAndRemindersAppListModel( record.controller.setAllowed(newAllowed) } - private fun createRecord(app: ApplicationInfo, hasRequestPermission: Boolean) = - AlarmsAndRemindersAppRecord( + private fun createRecord( + app: ApplicationInfo, + hasRequestPermission: Boolean, + ): AlarmsAndRemindersAppRecord { + val hasRequestSeaPermission = hasRequestPermission && app.isSeaEnabled() + val isTrumped = hasRequestSeaPermission && app.isTrumped() + return AlarmsAndRemindersAppRecord( app = app, - isChangeable = hasRequestPermission && app.isChangeEnabled(), + isTrumped = isTrumped, + isChangeable = hasRequestPermission && !isTrumped, controller = AlarmsAndRemindersController(context, app), ) + } + + /** + * If trumped, this app will be treated as allowed, and the toggle is not changeable by user. + */ + private fun ApplicationInfo.isTrumped(): Boolean = with(packageManagers) { + val hasRequestUseExactAlarm = hasRequestPermission(Manifest.permission.USE_EXACT_ALARM) && + CompatChanges.isChangeEnabled( + AlarmManager.ENABLE_USE_EXACT_ALARM, packageName, userHandle, + ) + val isPowerAllowListed = context.getSystemService(PowerExemptionManager::class.java) + ?.isAllowListed(packageName, true) ?: false + return hasRequestUseExactAlarm || isPowerAllowListed + } companion object { private const val PERMISSION: String = Manifest.permission.SCHEDULE_EXACT_ALARM - private fun ApplicationInfo.isChangeEnabled(): Boolean = + /** Checks whether [Manifest.permission.SCHEDULE_EXACT_ALARM] is enabled. */ + private fun ApplicationInfo.isSeaEnabled(): Boolean = CompatChanges.isChangeEnabled( AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION, packageName, userHandle, ) diff --git a/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppListTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppListTest.kt new file mode 100644 index 00000000000..8bc67165ed7 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/specialaccess/AlarmsAndRemindersAppListTest.kt @@ -0,0 +1,133 @@ +/* + * 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.spa.app.specialaccess + +import android.Manifest +import android.app.compat.CompatChanges +import android.content.Context +import android.content.pm.ApplicationInfo +import android.os.PowerExemptionManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.settingslib.spaprivileged.model.app.IPackageManagers +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.anyLong +import org.mockito.Mockito.anyString +import org.mockito.MockitoSession +import org.mockito.Spy +import org.mockito.quality.Strictness +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class AlarmsAndRemindersAppListTest { + private lateinit var mockSession: MockitoSession + + @Spy + private val context: Context = ApplicationProvider.getApplicationContext() + + @Mock + private lateinit var powerExemptionManager: PowerExemptionManager + + @Mock + private lateinit var packageManagers: IPackageManagers + + private lateinit var listModel: AlarmsAndRemindersAppListModel + + @Before + fun setUp() { + mockSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .mockStatic(CompatChanges::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + whenever(CompatChanges.isChangeEnabled(anyLong(), anyString(), any())).thenReturn(true) + whenever(context.getSystemService(PowerExemptionManager::class.java)) + .thenReturn(powerExemptionManager) + with(packageManagers) { + whenever(APP.hasRequestPermission(Manifest.permission.SCHEDULE_EXACT_ALARM)) + .thenReturn(true) + } + listModel = AlarmsAndRemindersAppListModel(context, packageManagers) + } + + @After + fun tearDown() { + mockSession.finishMocking() + } + + @Test + fun transformItem_recordHasCorrectApp() { + val record = listModel.transformItem(APP) + + assertThat(record.app).isSameInstanceAs(APP) + } + + @Test + fun transformItem_whenNotRequestScheduleExactAlarm_recordHasCorrectState() { + with(packageManagers) { + whenever(APP.hasRequestPermission(Manifest.permission.SCHEDULE_EXACT_ALARM)) + .thenReturn(false) + } + val record = listModel.transformItem(APP) + + assertThat(record.isTrumped).isFalse() + assertThat(record.isChangeable).isFalse() + } + + @Test + fun transformItem_whenRequestUseExactAlarm_recordHasCorrectState() { + with(packageManagers) { + whenever(APP.hasRequestPermission(Manifest.permission.USE_EXACT_ALARM)) + .thenReturn(true) + } + val record = listModel.transformItem(APP) + + assertThat(record.isTrumped).isTrue() + assertThat(record.isChangeable).isFalse() + } + + @Test + fun transformItem_whenPowerAllowListed_recordHasCorrectState() { + whenever(powerExemptionManager.isAllowListed(PACKAGE_NAME, true)).thenReturn(true) + val record = listModel.transformItem(APP) + + assertThat(record.isTrumped).isTrue() + assertThat(record.isChangeable).isFalse() + } + + @Test + fun transformItem_whenNotTrumped_recordHasCorrectState() { + val record = listModel.transformItem(APP) + + assertThat(record.isTrumped).isFalse() + assertThat(record.isChangeable).isTrue() + } + + private companion object { + const val PACKAGE_NAME = "package.name" + val APP = ApplicationInfo().apply { + packageName = PACKAGE_NAME + } + } +} \ No newline at end of file