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 + } +}