From 3f5bd0931e657b7ce83294dc7fefdc22b70cf6d2 Mon Sep 17 00:00:00 2001 From: Suprabh Shukla Date: Thu, 10 Jun 2021 17:25:09 -0700 Subject: [PATCH] Changes to 'Alarms & reminders' permission setting Not showing apps that are targeting SDK < 31, because the change is not enabled for them. Now alarm manager service manages killing the process whenever the permission gets revoked, so we don't need to do it here. This also lets us kill the app on "Reset app preferences" if needed. Adding the preference under "Advanced" in the app info page so it appears for apps that have requested this permission. Test: atest SettingsUnitTests:AppStateAlarmsAndRemindersBridgeTest make -j64 RunSettingsRoboTests \ ROBOTEST_FILTER="AlarmsAndRemindersDetailsTest| AlarmsAndRemindersDetailPreferenceControllerTest" Manually: - There should be no observable difference in behavior when toggling the setting. ActivityManager logs should still indicate that the app is killed when the permission is revoked. - "Alarms & Reminders" should appear under "Advanced" when looking at the app info detail of an app that appears under "Alarms & reminders" special app access page. Bug: 179541791 Bug: 190070171 Change-Id: I2d437cec10ee10e4326fb25b2820de9ef9c31c67 --- res/xml/app_info_settings.xml | 6 ++ .../AppStateAlarmsAndRemindersBridge.java | 14 ++- ...ndRemindersDetailPreferenceController.java | 75 ++++++++++++++ .../appinfo/AlarmsAndRemindersDetails.java | 25 ++--- .../appinfo/AppInfoDashboardFragment.java | 8 +- ...mindersDetailPreferenceControllerTest.java | 98 +++++++++++++++++++ 6 files changed, 204 insertions(+), 22 deletions(-) create mode 100644 src/com/android/settings/applications/appinfo/AlarmsAndRemindersDetailPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/applications/appinfo/AlarmsAndRemindersDetailPreferenceControllerTest.java diff --git a/res/xml/app_info_settings.xml b/res/xml/app_info_settings.xml index fef52432d24..2afaedeaa10 100644 --- a/res/xml/app_info_settings.xml +++ b/res/xml/app_info_settings.xml @@ -175,6 +175,12 @@ android:summary="@string/summary_placeholder" settings:controller="com.android.settings.applications.specialaccess.interactacrossprofiles.InteractAcrossProfilesDetailsPreferenceController" /> + + diff --git a/src/com/android/settings/applications/AppStateAlarmsAndRemindersBridge.java b/src/com/android/settings/applications/AppStateAlarmsAndRemindersBridge.java index 4e9a96efb5f..cf938a5e3d4 100644 --- a/src/com/android/settings/applications/AppStateAlarmsAndRemindersBridge.java +++ b/src/com/android/settings/applications/AppStateAlarmsAndRemindersBridge.java @@ -19,6 +19,7 @@ package com.android.settings.applications; import android.Manifest; import android.app.AlarmManager; import android.app.AppGlobals; +import android.app.compat.CompatChanges; import android.content.Context; import android.content.pm.IPackageManager; import android.os.RemoteException; @@ -63,14 +64,21 @@ public class AppStateAlarmsAndRemindersBridge extends AppStateBaseBridge { } } + private boolean isChangeEnabled(String packageName, int userId) { + return CompatChanges.isChangeEnabled(AlarmManager.REQUIRE_EXACT_ALARM_PERMISSION, + packageName, UserHandle.of(userId)); + } + /** * Returns information regarding {@link Manifest.permission#SCHEDULE_EXACT_ALARM} for the given * package and uid. */ public AlarmsAndRemindersState createPermissionState(String packageName, int uid) { - final boolean permissionRequested = ArrayUtils.contains(mRequesterPackages, packageName); - final boolean permissionGranted = mAlarmManager.hasScheduleExactAlarm(packageName, - UserHandle.getUserId(uid)); + final int userId = UserHandle.getUserId(uid); + + final boolean permissionRequested = ArrayUtils.contains(mRequesterPackages, packageName) + && isChangeEnabled(packageName, userId); + final boolean permissionGranted = mAlarmManager.hasScheduleExactAlarm(packageName, userId); return new AlarmsAndRemindersState(permissionRequested, permissionGranted); } diff --git a/src/com/android/settings/applications/appinfo/AlarmsAndRemindersDetailPreferenceController.java b/src/com/android/settings/applications/appinfo/AlarmsAndRemindersDetailPreferenceController.java new file mode 100644 index 00000000000..cfd4bf1c265 --- /dev/null +++ b/src/com/android/settings/applications/appinfo/AlarmsAndRemindersDetailPreferenceController.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2021 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.applications.appinfo; + +import android.content.Context; +import android.content.pm.PackageInfo; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; + +import com.android.settings.SettingsPreferenceFragment; +import com.android.settings.applications.AppStateAlarmsAndRemindersBridge; + +/** + * Preference controller for + * {@link com.android.settings.applications.appinfo.AlarmsAndRemindersDetails} Settings fragment. + */ +public class AlarmsAndRemindersDetailPreferenceController extends AppInfoPreferenceControllerBase { + + private String mPackageName; + + public AlarmsAndRemindersDetailPreferenceController(Context context, String key) { + super(context, key); + } + + @Override + public int getAvailabilityStatus() { + return isCandidate() ? AVAILABLE : CONDITIONALLY_UNAVAILABLE; + } + + @Override + public void updateState(Preference preference) { + preference.setSummary(getPreferenceSummary()); + } + + @Override + protected Class getDetailFragmentClass() { + return AlarmsAndRemindersDetails.class; + } + + @VisibleForTesting + CharSequence getPreferenceSummary() { + return AlarmsAndRemindersDetails.getSummary(mContext, mParent.getAppEntry()); + } + + @VisibleForTesting + boolean isCandidate() { + final PackageInfo packageInfo = mParent.getPackageInfo(); + if (packageInfo == null) { + return false; + } + final AppStateAlarmsAndRemindersBridge.AlarmsAndRemindersState appState = + new AppStateAlarmsAndRemindersBridge(mContext, null, null).createPermissionState( + mPackageName, packageInfo.applicationInfo.uid); + return appState.shouldBeVisible(); + } + + void setPackageName(String packageName) { + mPackageName = packageName; + } +} diff --git a/src/com/android/settings/applications/appinfo/AlarmsAndRemindersDetails.java b/src/com/android/settings/applications/appinfo/AlarmsAndRemindersDetails.java index 3765dd9b68f..648696ba25d 100644 --- a/src/com/android/settings/applications/appinfo/AlarmsAndRemindersDetails.java +++ b/src/com/android/settings/applications/appinfo/AlarmsAndRemindersDetails.java @@ -18,7 +18,6 @@ package com.android.settings.applications.appinfo; import static android.app.Activity.RESULT_CANCELED; import static android.app.Activity.RESULT_OK; -import android.app.ActivityManager; import android.app.AppOpsManager; import android.app.settings.SettingsEnums; import android.content.Context; @@ -49,25 +48,20 @@ public class AlarmsAndRemindersDetails extends AppInfoWithHeader private AppOpsManager mAppOpsManager; private RestrictedSwitchPreference mSwitchPref; private AppStateAlarmsAndRemindersBridge.AlarmsAndRemindersState mPermissionState; - private ActivityManager mActivityManager; private volatile Boolean mUncommittedState; /** * Returns the string that states whether the app has access to * {@link android.Manifest.permission#SCHEDULE_EXACT_ALARM}. */ - public static int getSummary(Context context, AppEntry entry) { - final AppStateAlarmsAndRemindersBridge.AlarmsAndRemindersState state; - if (entry.extraInfo instanceof AppStateAlarmsAndRemindersBridge.AlarmsAndRemindersState) { - state = (AppStateAlarmsAndRemindersBridge.AlarmsAndRemindersState) entry.extraInfo; - } else { - state = new AppStateAlarmsAndRemindersBridge(context, /*appState=*/null, - /*callback=*/null).createPermissionState(entry.info.packageName, - entry.info.uid); - } + public static CharSequence getSummary(Context context, AppEntry entry) { + final AppStateAlarmsAndRemindersBridge.AlarmsAndRemindersState state = + new AppStateAlarmsAndRemindersBridge(context, /*appState=*/null, + /*callback=*/null).createPermissionState(entry.info.packageName, + entry.info.uid); - return state.isAllowed() ? R.string.app_permission_summary_allowed - : R.string.app_permission_summary_not_allowed; + return context.getString(state.isAllowed() ? R.string.app_permission_summary_allowed + : R.string.app_permission_summary_not_allowed); } @Override @@ -77,7 +71,6 @@ public class AlarmsAndRemindersDetails extends AppInfoWithHeader final Context context = getActivity(); mAppBridge = new AppStateAlarmsAndRemindersBridge(context, mState, /*callback=*/null); mAppOpsManager = context.getSystemService(AppOpsManager.class); - mActivityManager = context.getSystemService(ActivityManager.class); if (savedInstanceState != null) { mUncommittedState = (Boolean) savedInstanceState.get(UNCOMMITTED_STATE_KEY); @@ -115,10 +108,6 @@ public class AlarmsAndRemindersDetails extends AppInfoWithHeader final int uid = mPackageInfo.applicationInfo.uid; mAppOpsManager.setUidMode(AppOpsManager.OPSTR_SCHEDULE_EXACT_ALARM, uid, newState ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_ERRORED); - if (!newState) { - mActivityManager.killUid(uid, - AppOpsManager.OPSTR_SCHEDULE_EXACT_ALARM + " no longer allowed."); - } } private void logPermissionChange(boolean newState, String packageName) { diff --git a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java index cb0ed07b4ab..9cc3836263e 100755 --- a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java +++ b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java @@ -197,8 +197,14 @@ public class AppInfoDashboardFragment extends DashboardFragment acrossProfiles.setPackageName(packageName); acrossProfiles.setParentFragment(this); + final AlarmsAndRemindersDetailPreferenceController alarmsAndReminders = + use(AlarmsAndRemindersDetailPreferenceController.class); + alarmsAndReminders.setPackageName(packageName); + alarmsAndReminders.setParentFragment(this); + use(AdvancedAppInfoPreferenceCategoryController.class).setChildren(Arrays.asList( - writeSystemSettings, drawOverlay, pip, externalSource, acrossProfiles)); + writeSystemSettings, drawOverlay, pip, externalSource, acrossProfiles, + alarmsAndReminders)); } @Override diff --git a/tests/robotests/src/com/android/settings/applications/appinfo/AlarmsAndRemindersDetailPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/appinfo/AlarmsAndRemindersDetailPreferenceControllerTest.java new file mode 100644 index 00000000000..58b894e4a7d --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/appinfo/AlarmsAndRemindersDetailPreferenceControllerTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2021 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.applications.appinfo; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; + +import androidx.preference.Preference; + +import com.android.settings.core.BasePreferenceController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class AlarmsAndRemindersDetailPreferenceControllerTest { + + @Mock + private AppInfoDashboardFragment mFragment; + @Mock + private Preference mPreference; + + private Context mContext; + private AlarmsAndRemindersDetailPreferenceController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + mController = spy(new AlarmsAndRemindersDetailPreferenceController(mContext, "test_key")); + mController.setPackageName("Package1"); + mController.setParentFragment(mFragment); + final String key = mController.getPreferenceKey(); + when(mPreference.getKey()).thenReturn(key); + } + + @Test + public void getAvailabilityStatus_notCandidate_shouldReturnUnavailable() { + doReturn(false).when(mController).isCandidate(); + + assertThat(mController.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void getAvailabilityStatus_isCandidate_shouldReturnAvailable() { + doReturn(true).when(mController).isCandidate(); + + assertThat(mController.getAvailabilityStatus()) + .isEqualTo(BasePreferenceController.AVAILABLE); + } + + @Test + public void getDetailFragmentClass_shouldReturnAlarmsAndRemindersDetails() { + assertThat(mController.getDetailFragmentClass()).isEqualTo(AlarmsAndRemindersDetails.class); + } + + @Test + public void updateState_shouldSetSummary() { + final String summary = "test summary"; + doReturn(summary).when(mController).getPreferenceSummary(); + + mController.updateState(mPreference); + + verify(mPreference).setSummary(summary); + } + + @Test + public void isCandidate_nullPackageInfo_shouldNotCrash() { + mController.isCandidate(); + // no crash + } +}