diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 9986e34d7c2..2ff4ee4ed76 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3117,6 +3117,24 @@ android:value="@string/menu_key_apps"/> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/xml/turn_screen_on_settings.xml b/res/xml/turn_screen_on_settings.xml new file mode 100644 index 00000000000..6aefa72f8a8 --- /dev/null +++ b/res/xml/turn_screen_on_settings.xml @@ -0,0 +1,23 @@ + + + + diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index 57d7d105188..8da809ca016 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -271,6 +271,7 @@ public class Settings extends SettingsActivity { public static class VrListenersSettingsActivity extends SettingsActivity { /* empty */ } public static class PremiumSmsAccessActivity extends SettingsActivity { /* empty */ } public static class PictureInPictureSettingsActivity extends SettingsActivity { /* empty */ } + public static class TurnScreenOnSettingsActivity extends SettingsActivity { /* empty */ } public static class AppPictureInPictureSettingsActivity extends SettingsActivity { /* empty */ } public static class ZenAccessSettingsActivity extends SettingsActivity { /* empty */ } public static class ZenAccessDetailSettingsActivity extends SettingsActivity {} diff --git a/src/com/android/settings/applications/specialaccess/turnscreenon/TurnScreenOnDetails.java b/src/com/android/settings/applications/specialaccess/turnscreenon/TurnScreenOnDetails.java new file mode 100644 index 00000000000..4540ab9863a --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/turnscreenon/TurnScreenOnDetails.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2022 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.specialaccess.turnscreenon; + +import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.AppOpsManager.MODE_ERRORED; +import static android.app.AppOpsManager.OP_TURN_SCREEN_ON; + +import android.app.AppOpsManager; +import android.app.settings.SettingsEnums; +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoWithHeader; + +/** + * Detail page for turn screen on special app access. + */ +public class TurnScreenOnDetails extends AppInfoWithHeader + implements OnPreferenceChangeListener { + + private static final String KEY_APP_OPS_SETTINGS_SWITCH = "app_ops_settings_switch"; + + private SwitchPreference mSwitchPref; + private AppOpsManager mAppOpsManager; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mAppOpsManager = this.getSystemService(AppOpsManager.class); + + // find preferences + addPreferencesFromResource(R.xml.turn_screen_on_permissions_details); + mSwitchPref = (SwitchPreference) findPreference(KEY_APP_OPS_SETTINGS_SWITCH); + + // set title/summary for all of them + mSwitchPref.setTitle(R.string.allow_turn_screen_on); + + // install event listeners + mSwitchPref.setOnPreferenceChangeListener(this); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + if (preference == mSwitchPref) { + setTurnScreenOnAppOp(mPackageInfo.applicationInfo.uid, mPackageName, + (Boolean) newValue); + return true; + } + return false; + } + + @Override + protected boolean refreshUi() { + boolean isAllowed = isTurnScreenOnAllowed(mAppOpsManager, + mPackageInfo.applicationInfo.uid, mPackageName); + mSwitchPref.setChecked(isAllowed); + return true; + } + + @Override + protected AlertDialog createDialog(int id, int errorCode) { + return null; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_MANAGE_TURN_SCREEN_ON; + } + + /** + * Sets whether the app associated with the given {@code packageName} is allowed to turn the + * screen on. + */ + void setTurnScreenOnAppOp(int uid, String packageName, boolean value) { + final int newMode = value ? MODE_ALLOWED : MODE_ERRORED; + mAppOpsManager.setMode(OP_TURN_SCREEN_ON, uid, packageName, newMode); + } + + /** + * @return whether the app associated with the given {@code packageName} is allowed to turn the + * screen on. + */ + static boolean isTurnScreenOnAllowed(AppOpsManager appOpsManager, int uid, String packageName) { + return appOpsManager.checkOpNoThrow(OP_TURN_SCREEN_ON, uid, packageName) == MODE_ALLOWED; + } + + /** + * @return the summary for the current state of whether the app associated with the given + * packageName is allowed to turn the screen on. + */ + public static int getPreferenceSummary(AppOpsManager appOpsManager, int uid, + String packageName) { + final boolean enabled = TurnScreenOnDetails.isTurnScreenOnAllowed(appOpsManager, uid, + packageName); + return enabled ? R.string.app_permission_summary_allowed + : R.string.app_permission_summary_not_allowed; + } +} diff --git a/src/com/android/settings/applications/specialaccess/turnscreenon/TurnScreenOnSettings.java b/src/com/android/settings/applications/specialaccess/turnscreenon/TurnScreenOnSettings.java new file mode 100644 index 00000000000..302d6b595ae --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/turnscreenon/TurnScreenOnSettings.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2022 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.specialaccess.turnscreenon; + +import android.Manifest; +import android.annotation.Nullable; +import android.app.AppOpsManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.IconDrawableFactory; +import android.util.Pair; +import android.view.View; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceClickListener; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.widget.EmptyTextSettings; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.AppPreference; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * Settings page for providing special app access to turn the screen of the device on. + */ +@SearchIndexable +public class TurnScreenOnSettings extends EmptyTextSettings { + + @VisibleForTesting + static final List IGNORE_PACKAGE_LIST = new ArrayList<>(); + + static { + IGNORE_PACKAGE_LIST.add("com.android.systemui"); + } + + /** + * Comparator by name, then user id. + * {@see PackageItemInfo#DisplayNameComparator} + */ + static class AppComparator implements Comparator> { + + private final Collator mCollator = Collator.getInstance(); + private final PackageManager mPm; + + AppComparator(PackageManager pm) { + mPm = pm; + } + + public final int compare(Pair a, + Pair b) { + CharSequence sa = a.first.loadLabel(mPm); + if (sa == null) sa = a.first.name; + CharSequence sb = b.first.loadLabel(mPm); + if (sb == null) sb = b.first.name; + int nameCmp = mCollator.compare(sa.toString(), sb.toString()); + if (nameCmp != 0) { + return nameCmp; + } else { + return a.second - b.second; + } + } + } + + private AppOpsManager mAppOpsManager; + private Context mContext; + private PackageManager mPackageManager; + private UserManager mUserManager; + private IconDrawableFactory mIconDrawableFactory; + + public TurnScreenOnSettings() { + // Do nothing + } + + public TurnScreenOnSettings(PackageManager pm, UserManager um) { + mPackageManager = pm; + mUserManager = um; + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mContext = getActivity(); + mPackageManager = mContext.getPackageManager(); + mUserManager = mContext.getSystemService(UserManager.class); + mAppOpsManager = mContext.getSystemService(AppOpsManager.class); + mIconDrawableFactory = IconDrawableFactory.newInstance(mContext); + } + + @Override + public void onResume() { + super.onResume(); + + // Clear the prefs + final PreferenceScreen screen = getPreferenceScreen(); + screen.removeAll(); + + // Fetch the set of applications for each profile which have the permission required to turn + // the screen on with a wake lock. + final ArrayList> apps = collectTurnScreenOnApps( + UserHandle.myUserId()); + apps.sort(new AppComparator(mPackageManager)); + + // Rebuild the list of prefs + final Context prefContext = getPrefContext(); + for (final Pair appData : apps) { + final ApplicationInfo appInfo = appData.first; + final int userId = appData.second; + final UserHandle user = UserHandle.of(userId); + final String packageName = appInfo.packageName; + final CharSequence label = appInfo.loadLabel(mPackageManager); + + final Preference pref = new AppPreference(prefContext); + pref.setIcon(mIconDrawableFactory.getBadgedIcon(appInfo, userId)); + pref.setTitle(mPackageManager.getUserBadgedLabel(label, user)); + pref.setSummary(TurnScreenOnDetails.getPreferenceSummary(mAppOpsManager, + appInfo.uid, packageName)); + pref.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + AppInfoBase.startAppInfoFragment(TurnScreenOnDetails.class, + getString(R.string.turn_screen_on_title), + packageName, appInfo.uid, + TurnScreenOnSettings.this, -1, getMetricsCategory()); + return true; + } + }); + screen.addPreference(pref); + } + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setEmptyText(R.string.no_applications); + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.turn_screen_on_settings; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.SETTINGS_MANAGE_TURN_SCREEN_ON; + } + + /** + * @return the list of applications for the given user and all their profiles that can turn on + * the screen with wake locks. + */ + @VisibleForTesting + ArrayList> collectTurnScreenOnApps(int userId) { + final ArrayList> apps = new ArrayList<>(); + final ArrayList userIds = new ArrayList<>(); + for (UserInfo user : mUserManager.getProfiles(userId)) { + userIds.add(user.id); + } + + for (int id : userIds) { + final List installedPackages = mPackageManager.getInstalledPackagesAsUser( + /* flags= */ 0, id); + for (PackageInfo packageInfo : installedPackages) { + if (hasTurnScreenOnPermission(mPackageManager, packageInfo.packageName)) { + apps.add(new Pair<>(packageInfo.applicationInfo, id)); + } + } + } + return apps; + } + + /** + * @return true if the package has the permission to turn the screen on. + */ + @VisibleForTesting + static boolean hasTurnScreenOnPermission(PackageManager packageManager, String packageName) { + if (IGNORE_PACKAGE_LIST.contains(packageName)) { + return false; + } + return packageManager.checkPermission(Manifest.permission.WAKE_LOCK, packageName) + == PackageManager.PERMISSION_GRANTED; + } + + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.turn_screen_on_settings); +} diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index ce3cab99572..b11ce0175ef 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -60,6 +60,8 @@ import com.android.settings.applications.specialaccess.notificationaccess.Notifi import com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureDetails; import com.android.settings.applications.specialaccess.pictureinpicture.PictureInPictureSettings; import com.android.settings.applications.specialaccess.premiumsms.PremiumSmsAccess; +import com.android.settings.applications.specialaccess.turnscreenon.TurnScreenOnDetails; +import com.android.settings.applications.specialaccess.turnscreenon.TurnScreenOnSettings; import com.android.settings.applications.specialaccess.vrlistener.VrListenerSettings; import com.android.settings.applications.specialaccess.zenaccess.ZenAccessDetails; import com.android.settings.backup.PrivacySettings; @@ -335,7 +337,9 @@ public class SettingsGateway { AutoBrightnessSettings.class.getName(), OneHandedSettings.class.getName(), MobileNetworkSettings.class.getName(), - AppLocaleDetails.class.getName() + AppLocaleDetails.class.getName(), + TurnScreenOnSettings.class.getName(), + TurnScreenOnDetails.class.getName() }; public static final String[] SETTINGS_FOR_RESTRICTED = { diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/turnscreenon/TurnScreenOnDetailsTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/turnscreenon/TurnScreenOnDetailsTest.java new file mode 100644 index 00000000000..c27ad2b92bf --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/specialaccess/turnscreenon/TurnScreenOnDetailsTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 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.specialaccess.turnscreenon; + +import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.AppOpsManager.MODE_ERRORED; +import static android.app.AppOpsManager.OP_TURN_SCREEN_ON; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.app.AppOpsManager; + +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; + +@RunWith(RobolectricTestRunner.class) +public class TurnScreenOnDetailsTest { + + private static final int UID = 0; + private static final String PACKAGE_NAME = "com.android.fake.package"; + + @Mock + private AppOpsManager mAppOpsManager; + + + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void isTurnScreenOnAllowed_appOpErrored_shouldReturnFalse() { + when(mAppOpsManager.checkOpNoThrow(eq(OP_TURN_SCREEN_ON), eq(UID), + eq(PACKAGE_NAME))).thenReturn(MODE_ERRORED); + + boolean isAllowed = TurnScreenOnDetails.isTurnScreenOnAllowed(mAppOpsManager, UID, + PACKAGE_NAME); + + assertThat(isAllowed).isFalse(); + } + + @Test + public void isTurnScreenOnAllowed_appOpAllowed_shouldReturnTrue() { + when(mAppOpsManager.checkOpNoThrow(eq(OP_TURN_SCREEN_ON), eq(UID), + eq(PACKAGE_NAME))).thenReturn(MODE_ALLOWED); + + boolean isAllowed = TurnScreenOnDetails.isTurnScreenOnAllowed(mAppOpsManager, UID, + PACKAGE_NAME); + + assertThat(isAllowed).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/turnscreenon/TurnScreenOnSettingsTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/turnscreenon/TurnScreenOnSettingsTest.java new file mode 100644 index 00000000000..6325d9d8779 --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/specialaccess/turnscreenon/TurnScreenOnSettingsTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2022 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.specialaccess.turnscreenon; + + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.UserInfo; +import android.os.UserManager; +import android.util.Pair; + +import com.android.settings.testutils.FakeFeatureFactory; + +import com.google.common.collect.ImmutableList; + +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 java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class TurnScreenOnSettingsTest { + + private static final int PRIMARY_USER_ID = 0; + private static final int PROFILE_USER_ID = 10; + + private TurnScreenOnSettings mFragment; + @Mock + private PackageManager mPackageManager; + @Mock + private UserManager mUserManager; + private ArrayList mPrimaryUserPackages; + private ArrayList mProfileUserPackages; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + FakeFeatureFactory.setupForTest(); + mFragment = new TurnScreenOnSettings(mPackageManager, mUserManager); + mPrimaryUserPackages = new ArrayList<>(); + mProfileUserPackages = new ArrayList<>(); + when(mPackageManager.getInstalledPackagesAsUser(anyInt(), eq(PRIMARY_USER_ID))) + .thenReturn(mPrimaryUserPackages); + when(mPackageManager.getInstalledPackagesAsUser(anyInt(), eq(PROFILE_USER_ID))) + .thenReturn(mProfileUserPackages); + + UserInfo primaryUserInfo = new UserInfo(); + primaryUserInfo.id = PRIMARY_USER_ID; + UserInfo profileUserInfo = new UserInfo(); + profileUserInfo.id = PROFILE_USER_ID; + + when(mUserManager.getProfiles(PRIMARY_USER_ID)) + .thenReturn(ImmutableList.of(primaryUserInfo, profileUserInfo)); + } + + @Test + public void testCollectTurnScreenOnApps_variousPackages_shouldReturnOnlyPackagesWithTurnScreenOnPermission() { + PackageInfo primaryP1 = createPackage("Calculator", true); + PackageInfo primaryP2 = createPackage("Clock", false); + PackageInfo profileP1 = createPackage("Browser", false); + PackageInfo profileP2 = createPackage("Files", true); + mPrimaryUserPackages.add(primaryP1); + mPrimaryUserPackages.add(primaryP2); + mProfileUserPackages.add(profileP1); + mProfileUserPackages.add(profileP2); + + List> apps = mFragment.collectTurnScreenOnApps( + PRIMARY_USER_ID); + + assertThat(containsPackages(apps, primaryP1, profileP2)).isTrue(); + assertThat(containsPackages(apps, primaryP2, profileP1)).isFalse(); + } + + @Test + public void collectTurnScreenOnApps_noTurnScreenOnPackages_shouldReturnEmptyList() { + PackageInfo primaryP1 = createPackage("Calculator", false); + PackageInfo profileP1 = createPackage("Browser", false); + mPrimaryUserPackages.add(primaryP1); + mProfileUserPackages.add(profileP1); + + List> apps = mFragment.collectTurnScreenOnApps( + PRIMARY_USER_ID); + + assertThat(apps).isEmpty(); + } + + @Test + public void sort_multiplePackages_appsShouldBeOrderedByAppName() { + PackageInfo primaryP1 = createPackage("Android", true); + PackageInfo primaryP2 = createPackage("Boop", true); + PackageInfo primaryP3 = createPackage("Deck", true); + PackageInfo profileP1 = createPackage("Android", true); + PackageInfo profileP2 = createPackage("Cool", true); + PackageInfo profileP3 = createPackage("Fast", false); + mPrimaryUserPackages.add(primaryP1); + mPrimaryUserPackages.add(primaryP2); + mPrimaryUserPackages.add(primaryP3); + mProfileUserPackages.add(profileP1); + mProfileUserPackages.add(profileP2); + mProfileUserPackages.add(profileP3); + List> apps = mFragment.collectTurnScreenOnApps( + PRIMARY_USER_ID); + + apps.sort(new TurnScreenOnSettings.AppComparator(null)); + + assertThat(isOrdered(apps, primaryP1, profileP1, primaryP2, profileP2, primaryP3)).isTrue(); + } + + @Test + public void hasTurnScreenOnPermission_ignoredPackages_shouldReturnFalse() { + boolean res = false; + + for (String ignoredPackage : TurnScreenOnSettings.IGNORE_PACKAGE_LIST) { + res |= TurnScreenOnSettings.hasTurnScreenOnPermission(mPackageManager, ignoredPackage); + } + + assertThat(res).isFalse(); + } + + private boolean containsPackages(List> apps, + PackageInfo... packages) { + for (PackageInfo aPackage : packages) { + boolean found = false; + for (Pair app : apps) { + if (app.first == aPackage.applicationInfo) { + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + private boolean isOrdered(List> apps, PackageInfo... packages) { + if (apps.size() != packages.length) { + return false; + } + + for (int i = 0; i < packages.length; i++) { + if (packages[i].applicationInfo != apps.get(i).first) { + return false; + } + } + return true; + } + + private PackageInfo createPackage(String packageName, boolean hasTurnScreenOnPermission) { + PackageInfo pi = new PackageInfo(); + when(mPackageManager.checkPermission(Manifest.permission.WAKE_LOCK, + packageName)).thenReturn( + hasTurnScreenOnPermission ? PackageManager.PERMISSION_GRANTED + : PackageManager.PERMISSION_DENIED); + pi.packageName = packageName; + pi.applicationInfo = new ApplicationInfo(); + pi.applicationInfo.name = packageName; + return pi; + } +}