From 5bb2a87b7f6bd6d87e1d01720005e1f90fbf40db Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Fri, 15 Mar 2019 16:41:24 -0700 Subject: [PATCH] Create a page to manage dnd permission for individual app - Change the original ZenAccessPage to - Remove the inline switch - Make the preference click target go into the new detail page - Some formatting/style change. - Create a new detail page for zen access. - Exit if app didn't declare this permission - Preset the switch toggle to their current permission grant state - Move the warning dialog logic from ZenAccessSettings to here. - Move some common functionality into ZenAccessController, a static helper class Bug: 128547723 Test: robotest Change-Id: I1ebb32396869d07ff4283b300bd716506298c9b5 --- res/values/strings.xml | 3 + res/xml/zen_access_permission_details.xml | 27 ++ .../FriendlyWarningDialogFragment.java | 76 ++++++ .../zenaccess/ScaryWarningDialogFragment.java | 73 +++++ .../zenaccess/ZenAccessController.java | 79 +++++- .../zenaccess/ZenAccessDetails.java | 100 +++++++ .../ZenAccessSettingObserverMixin.java | 77 ++++++ .../notification/ZenAccessSettings.java | 256 +++--------------- .../zenaccess/ZenAccessControllerTest.java | 43 +++ .../ZenAccessSettingObserverMixinTest.java | 109 ++++++++ .../notification/ZenAccessSettingsTest.java | 63 ----- .../shadow/ShadowNotificationManager.java | 13 + 12 files changed, 635 insertions(+), 284 deletions(-) create mode 100644 res/xml/zen_access_permission_details.xml create mode 100644 src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java create mode 100644 src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java create mode 100644 src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java create mode 100644 src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java create mode 100644 tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixinTest.java delete mode 100644 tests/robotests/src/com/android/settings/notification/ZenAccessSettingsTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index e4d0d115f0d..faef8a0528c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7939,6 +7939,9 @@ Do Not Disturb access + + Allow Do Not Disturb + No installed apps have requested Do Not Disturb access diff --git a/res/xml/zen_access_permission_details.xml b/res/xml/zen_access_permission_details.xml new file mode 100644 index 00000000000..afa8d80a17c --- /dev/null +++ b/res/xml/zen_access_permission_details.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java b/src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java new file mode 100644 index 00000000000..fc85f7dffd0 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2019 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.zenaccess; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * Warning dialog when revoking zen access warning that zen rule instances will be deleted. + */ +public class FriendlyWarningDialogFragment extends InstrumentedDialogFragment { + static final String KEY_PKG = "p"; + static final String KEY_LABEL = "l"; + + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_ZEN_ACCESS_REVOKE; + } + + public FriendlyWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { + Bundle args = new Bundle(); + args.putString(KEY_PKG, pkg); + args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); + setArguments(args); + return this; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Bundle args = getArguments(); + final String pkg = args.getString(KEY_PKG); + final String label = args.getString(KEY_LABEL); + + final String title = getResources().getString( + R.string.zen_access_revoke_warning_dialog_title, label); + final String summary = getResources() + .getString(R.string.zen_access_revoke_warning_dialog_summary); + return new AlertDialog.Builder(getContext()) + .setMessage(summary) + .setTitle(title) + .setCancelable(true) + .setPositiveButton(R.string.okay, + (dialog, id) -> { + ZenAccessController.deleteRules(getContext(), pkg); + ZenAccessController.setAccess(getContext(), pkg, false); + }) + .setNegativeButton(R.string.cancel, + (dialog, id) -> { + // pass + }) + .create(); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java b/src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java new file mode 100644 index 00000000000..69318f8d6af --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2019 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.zenaccess; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.notification.ZenAccessSettings; + +/** + * Warning dialog when allowing zen access warning about the privileges being granted. + */ +public class ScaryWarningDialogFragment extends InstrumentedDialogFragment { + static final String KEY_PKG = "p"; + static final String KEY_LABEL = "l"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_ZEN_ACCESS_GRANT; + } + + public ScaryWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { + Bundle args = new Bundle(); + args.putString(KEY_PKG, pkg); + args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); + setArguments(args); + return this; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Bundle args = getArguments(); + final String pkg = args.getString(KEY_PKG); + final String label = args.getString(KEY_LABEL); + + final String title = getResources().getString(R.string.zen_access_warning_dialog_title, + label); + final String summary = getResources() + .getString(R.string.zen_access_warning_dialog_summary); + return new AlertDialog.Builder(getContext()) + .setMessage(summary) + .setTitle(title) + .setCancelable(true) + .setPositiveButton(R.string.allow, + (dialog, id) -> ZenAccessController.setAccess(getContext(), pkg, true)) + .setNegativeButton(R.string.deny, + (dialog, id) -> { + // pass + }) + .create(); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java index 88d444d485c..946599b4aa8 100644 --- a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java @@ -17,12 +17,29 @@ package com.android.settings.applications.specialaccess.zenaccess; import android.app.ActivityManager; +import android.app.AppGlobals; +import android.app.NotificationManager; +import android.app.settings.SettingsEnums; import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.ParceledListSlice; +import android.os.AsyncTask; +import android.os.RemoteException; +import android.util.ArraySet; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; import com.android.settings.core.BasePreferenceController; +import com.android.settings.overlay.FeatureFactory; + +import java.util.List; +import java.util.Set; public class ZenAccessController extends BasePreferenceController { + private static final String TAG = "ZenAccessController"; + private final ActivityManager mActivityManager; public ZenAccessController(Context context, String preferenceKey) { @@ -32,8 +49,68 @@ public class ZenAccessController extends BasePreferenceController { @Override public int getAvailabilityStatus() { - return !mActivityManager.isLowRamDevice() + return isSupported(mActivityManager) ? AVAILABLE_UNSEARCHABLE : UNSUPPORTED_ON_DEVICE; } + + public static boolean isSupported(ActivityManager activityManager) { + return !activityManager.isLowRamDevice(); + } + + public static Set getPackagesRequestingNotificationPolicyAccess() { + final ArraySet requestingPackages = new ArraySet<>(); + try { + final String[] PERM = { + android.Manifest.permission.ACCESS_NOTIFICATION_POLICY + }; + final ParceledListSlice list = AppGlobals.getPackageManager() + .getPackagesHoldingPermissions(PERM, 0 /*flags*/, + ActivityManager.getCurrentUser()); + final List pkgs = list.getList(); + if (pkgs != null) { + for (PackageInfo info : pkgs) { + requestingPackages.add(info.packageName); + } + } + } catch (RemoteException e) { + Log.e(TAG, "Cannot reach packagemanager", e); + } + return requestingPackages; + } + + public static Set getAutoApprovedPackages(Context context) { + final Set autoApproved = new ArraySet<>(); + autoApproved.addAll(context.getSystemService(NotificationManager.class) + .getEnabledNotificationListenerPackages()); + return autoApproved; + } + + public static boolean hasAccess(Context context, String pkg) { + return context.getSystemService( + NotificationManager.class).isNotificationPolicyAccessGrantedForPackage(pkg); + } + + public static void setAccess(final Context context, final String pkg, final boolean access) { + logSpecialPermissionChange(access, pkg, context); + AsyncTask.execute(() -> { + final NotificationManager mgr = context.getSystemService(NotificationManager.class); + mgr.setNotificationPolicyAccessGranted(pkg, access); + }); + } + + public static void deleteRules(final Context context, final String pkg) { + AsyncTask.execute(() -> { + final NotificationManager mgr = context.getSystemService(NotificationManager.class); + mgr.removeAutomaticZenRules(pkg); + }); + } + + @VisibleForTesting + static void logSpecialPermissionChange(boolean enable, String packageName, Context context) { + int logCategory = enable ? SettingsEnums.APP_SPECIAL_PERMISSION_DND_ALLOW + : SettingsEnums.APP_SPECIAL_PERMISSION_DND_DENY; + FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, + logCategory, packageName); + } } diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java new file mode 100644 index 00000000000..a18e7d63cad --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2019 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.zenaccess; + +import android.app.ActivityManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; +import androidx.preference.SwitchPreference; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoWithHeader; + +import java.util.Set; + +public class ZenAccessDetails extends AppInfoWithHeader implements + ZenAccessSettingObserverMixin.Listener { + + private static final String SWITCH_PREF_KEY = "zen_access_switch"; + + @Override + public int getMetricsCategory() { + return SettingsEnums.ZEN_ACCESS_DETAIL; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.zen_access_permission_details); + getSettingsLifecycle().addObserver( + new ZenAccessSettingObserverMixin(getContext(), this /* listener */)); + } + + @Override + protected boolean refreshUi() { + final Context context = getContext(); + if (!ZenAccessController.isSupported(context.getSystemService(ActivityManager.class))) { + return false; + } + // If this app didn't declare this permission in their manifest, don't bother showing UI. + final Set needAccessApps = + ZenAccessController.getPackagesRequestingNotificationPolicyAccess(); + if (!needAccessApps.contains(mPackageName)) { + return false; + } + updatePreference(context, findPreference(SWITCH_PREF_KEY)); + return true; + } + + @Override + protected AlertDialog createDialog(int id, int errorCode) { + return null; + } + + public void updatePreference(Context context, SwitchPreference preference) { + final CharSequence label = mPackageInfo.applicationInfo.loadLabel(mPm); + final Set autoApproved = ZenAccessController.getAutoApprovedPackages(context); + if (autoApproved.contains(mPackageName)) { + //Auto approved, user cannot do anything. Hard code summary and disable preference. + preference.setEnabled(false); + preference.setSummary(getString(R.string.zen_access_disabled_package_warning)); + return; + } + preference.setChecked(ZenAccessController.hasAccess(context, mPackageName)); + preference.setOnPreferenceChangeListener((p, newValue) -> { + final boolean access = (Boolean) newValue; + if (access) { + new ScaryWarningDialogFragment() + .setPkgInfo(mPackageName, label) + .show(getFragmentManager(), "dialog"); + } else { + new FriendlyWarningDialogFragment() + .setPkgInfo(mPackageName, label) + .show(getFragmentManager(), "dialog"); + } + return false; + }); + } + + @Override + public void onZenAccessPolicyChanged() { + refreshUi(); + } +} diff --git a/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java new file mode 100644 index 00000000000..30507efffa9 --- /dev/null +++ b/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 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.zenaccess; + +import android.app.ActivityManager; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; + +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnStart; +import com.android.settingslib.core.lifecycle.events.OnStop; + +public class ZenAccessSettingObserverMixin extends ContentObserver implements LifecycleObserver, + OnStart, OnStop { + + public interface Listener { + void onZenAccessPolicyChanged(); + } + + private final Context mContext; + private final Listener mListener; + + public ZenAccessSettingObserverMixin(Context context, Listener listener) { + super(new Handler(Looper.getMainLooper())); + mContext = context; + mListener = listener; + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (mListener != null) { + mListener.onZenAccessPolicyChanged(); + } + } + + @Override + public void onStart() { + if (!ZenAccessController.isSupported(mContext.getSystemService(ActivityManager.class))) { + return; + } + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), + false /* notifyForDescendants */, + this /* observer */); + mContext.getContentResolver().registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS), + false /* notifyForDescendants */, + this /* observer */); + } + + @Override + public void onStop() { + if (!ZenAccessController.isSupported(mContext.getSystemService(ActivityManager.class))) { + return; + } + mContext.getContentResolver().unregisterContentObserver(this /* observer */); + } +} diff --git a/src/com/android/settings/notification/ZenAccessSettings.java b/src/com/android/settings/notification/ZenAccessSettings.java index d057c755754..fca82552244 100644 --- a/src/com/android/settings/notification/ZenAccessSettings.java +++ b/src/com/android/settings/notification/ZenAccessSettings.java @@ -18,56 +18,40 @@ package com.android.settings.notification; import android.annotation.Nullable; import android.app.ActivityManager; -import android.app.AppGlobals; -import android.app.Dialog; import android.app.NotificationManager; import android.app.settings.SettingsEnums; import android.content.Context; -import android.content.DialogInterface; import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; -import android.content.pm.ParceledListSlice; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.RemoteException; import android.provider.SearchIndexableResource; -import android.provider.Settings.Secure; -import android.text.TextUtils; import android.util.ArraySet; -import android.util.Log; import android.view.View; -import androidx.annotation.VisibleForTesting; -import androidx.appcompat.app.AlertDialog; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreference; import com.android.settings.R; -import com.android.settings.core.instrumentation.InstrumentedDialogFragment; -import com.android.settings.overlay.FeatureFactory; +import com.android.settings.applications.AppInfoBase; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessController; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessDetails; +import com.android.settings.applications.specialaccess.zenaccess.ZenAccessSettingObserverMixin; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; -import com.android.settings.widget.AppSwitchPreference; import com.android.settings.widget.EmptyTextSettings; import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.apppreference.AppPreference; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; @SearchIndexable -public class ZenAccessSettings extends EmptyTextSettings { +public class ZenAccessSettings extends EmptyTextSettings implements + ZenAccessSettingObserverMixin.Listener { private final String TAG = "ZenAccessSettings"; - private final SettingObserver mObserver = new SettingObserver(); private Context mContext; private PackageManager mPkgMan; private NotificationManager mNoMan; @@ -84,6 +68,8 @@ public class ZenAccessSettings extends EmptyTextSettings { mContext = getActivity(); mPkgMan = mContext.getPackageManager(); mNoMan = mContext.getSystemService(NotificationManager.class); + getSettingsLifecycle().addObserver( + new ZenAccessSettingObserverMixin(getContext(), this /* listener */)); } @Override @@ -102,30 +88,22 @@ public class ZenAccessSettings extends EmptyTextSettings { super.onResume(); if (!ActivityManager.isLowRamDeviceStatic()) { reloadList(); - getContentResolver().registerContentObserver( - Secure.getUriFor(Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), false, - mObserver); - getContentResolver().registerContentObserver( - Secure.getUriFor(Secure.ENABLED_NOTIFICATION_LISTENERS), false, - mObserver); } else { setEmptyText(R.string.disabled_low_ram_device); } } @Override - public void onPause() { - super.onPause(); - if (!ActivityManager.isLowRamDeviceStatic()) { - getContentResolver().unregisterContentObserver(mObserver); - } + public void onZenAccessPolicyChanged() { + reloadList(); } private void reloadList() { final PreferenceScreen screen = getPreferenceScreen(); screen.removeAll(); final ArrayList apps = new ArrayList<>(); - final ArraySet requesting = getPackagesRequestingNotificationPolicyAccess(); + final Set requesting = + ZenAccessController.getPackagesRequestingNotificationPolicyAccess(); if (!requesting.isEmpty()) { final List installed = mPkgMan.getInstalledApplications(0); if (installed != null) { @@ -143,204 +121,42 @@ public class ZenAccessSettings extends EmptyTextSettings { for (ApplicationInfo app : apps) { final String pkg = app.packageName; final CharSequence label = app.loadLabel(mPkgMan); - final SwitchPreference pref = new AppSwitchPreference(getPrefContext()); + final AppPreference pref = new AppPreference(getPrefContext()); pref.setKey(pkg); - pref.setPersistent(false); pref.setIcon(app.loadIcon(mPkgMan)); pref.setTitle(label); - pref.setChecked(hasAccess(pkg)); if (autoApproved.contains(pkg)) { + //Auto approved, user cannot do anything. Hard code summary and disable preference. pref.setEnabled(false); pref.setSummary(getString(R.string.zen_access_disabled_package_warning)); + } else { + // Not auto approved, update summary according to notification backend. + pref.setSummary(getPreferenceSummary(pkg)); } - pref.setOnPreferenceChangeListener(new OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - final boolean access = (Boolean) newValue; - if (access) { - new ScaryWarningDialogFragment() - .setPkgInfo(pkg, label) - .show(getFragmentManager(), "dialog"); - } else { - new FriendlyWarningDialogFragment() - .setPkgInfo(pkg, label) - .show(getFragmentManager(), "dialog"); - } - return false; - } + pref.setOnPreferenceClickListener(preference -> { + AppInfoBase.startAppInfoFragment( + ZenAccessDetails.class /* fragment */, + R.string.manage_zen_access_title /* titleRes */, + pkg, + app.uid, + this /* source */, + -1 /* requestCode */, + getMetricsCategory() /* sourceMetricsCategory */); + return true; }); + screen.addPreference(pref); } } - private ArraySet getPackagesRequestingNotificationPolicyAccess() { - ArraySet requestingPackages = new ArraySet<>(); - try { - final String[] PERM = { - android.Manifest.permission.ACCESS_NOTIFICATION_POLICY - }; - final ParceledListSlice list = AppGlobals.getPackageManager() - .getPackagesHoldingPermissions(PERM, 0 /*flags*/, - ActivityManager.getCurrentUser()); - final List pkgs = list.getList(); - if (pkgs != null) { - for (PackageInfo info : pkgs) { - requestingPackages.add(info.packageName); - } - } - } catch(RemoteException e) { - Log.e(TAG, "Cannot reach packagemanager", e); - } - return requestingPackages; - } - - private boolean hasAccess(String pkg) { - return mNoMan.isNotificationPolicyAccessGrantedForPackage(pkg); - } - - private static void setAccess(final Context context, final String pkg, final boolean access) { - logSpecialPermissionChange(access, pkg, context); - AsyncTask.execute(new Runnable() { - @Override - public void run() { - final NotificationManager mgr = context.getSystemService(NotificationManager.class); - mgr.setNotificationPolicyAccessGranted(pkg, access); - } - }); - } - - @VisibleForTesting - static void logSpecialPermissionChange(boolean enable, String packageName, Context context) { - int logCategory = enable ? SettingsEnums.APP_SPECIAL_PERMISSION_DND_ALLOW - : SettingsEnums.APP_SPECIAL_PERMISSION_DND_DENY; - FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, - logCategory, packageName); - } - - - private static void deleteRules(final Context context, final String pkg) { - AsyncTask.execute(new Runnable() { - @Override - public void run() { - final NotificationManager mgr = context.getSystemService(NotificationManager.class); - mgr.removeAutomaticZenRules(pkg); - } - }); - } - - private final class SettingObserver extends ContentObserver { - public SettingObserver() { - super(new Handler(Looper.getMainLooper())); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - reloadList(); - } - } - /** - * Warning dialog when allowing zen access warning about the privileges being granted. + * @return the summary for the current state of whether the app associated with the given + * {@param packageName} is allowed to enter picture-in-picture. */ - public static class ScaryWarningDialogFragment extends InstrumentedDialogFragment { - static final String KEY_PKG = "p"; - static final String KEY_LABEL = "l"; - - @Override - public int getMetricsCategory() { - return SettingsEnums.DIALOG_ZEN_ACCESS_GRANT; - } - - public ScaryWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { - Bundle args = new Bundle(); - args.putString(KEY_PKG, pkg); - args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); - setArguments(args); - return this; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Bundle args = getArguments(); - final String pkg = args.getString(KEY_PKG); - final String label = args.getString(KEY_LABEL); - - final String title = getResources().getString(R.string.zen_access_warning_dialog_title, - label); - final String summary = getResources() - .getString(R.string.zen_access_warning_dialog_summary); - return new AlertDialog.Builder(getContext()) - .setMessage(summary) - .setTitle(title) - .setCancelable(true) - .setPositiveButton(R.string.allow, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - setAccess(getContext(), pkg, true); - } - }) - .setNegativeButton(R.string.deny, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - // pass - } - }) - .create(); - } - } - - /** - * Warning dialog when revoking zen access warning that zen rule instances will be deleted. - */ - public static class FriendlyWarningDialogFragment extends InstrumentedDialogFragment { - static final String KEY_PKG = "p"; - static final String KEY_LABEL = "l"; - - - @Override - public int getMetricsCategory() { - return SettingsEnums.DIALOG_ZEN_ACCESS_REVOKE; - } - - public FriendlyWarningDialogFragment setPkgInfo(String pkg, CharSequence label) { - Bundle args = new Bundle(); - args.putString(KEY_PKG, pkg); - args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString()); - setArguments(args); - return this; - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Bundle args = getArguments(); - final String pkg = args.getString(KEY_PKG); - final String label = args.getString(KEY_LABEL); - - final String title = getResources().getString( - R.string.zen_access_revoke_warning_dialog_title, label); - final String summary = getResources() - .getString(R.string.zen_access_revoke_warning_dialog_summary); - return new AlertDialog.Builder(getContext()) - .setMessage(summary) - .setTitle(title) - .setCancelable(true) - .setPositiveButton(R.string.okay, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - deleteRules(getContext(), pkg); - setAccess(getContext(), pkg, false); - } - }) - .setNegativeButton(R.string.cancel, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - // pass - } - }) - .create(); - } + private int getPreferenceSummary(String packageName) { + final boolean enabled = ZenAccessController.hasAccess(getContext(), packageName); + return enabled ? R.string.app_permission_summary_allowed + : R.string.app_permission_summary_not_allowed; } public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java index bcb4bb3bf4a..6041e9dead1 100644 --- a/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java +++ b/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java @@ -18,26 +18,41 @@ package com.android.settings.applications.specialaccess.zenaccess; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.app.NotificationManager; import android.content.Context; +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowNotificationManager; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowActivityManager; @RunWith(RobolectricTestRunner.class) public class ZenAccessControllerTest { + private static final String TEST_PKG = "com.test.package"; + + private FakeFeatureFactory mFeatureFactory; private Context mContext; private ZenAccessController mController; private ShadowActivityManager mActivityManager; + @Before public void setUp() { mContext = RuntimeEnvironment.application; + mFeatureFactory = FakeFeatureFactory.setupForTest(); mController = new ZenAccessController(mContext, "key"); mActivityManager = Shadow.extract(mContext.getSystemService(Context.ACTIVITY_SERVICE)); } @@ -52,4 +67,32 @@ public class ZenAccessControllerTest { mActivityManager.setIsLowRamDevice(true); assertThat(mController.isAvailable()).isFalse(); } + + @Test + public void logSpecialPermissionChange() { + ZenAccessController.logSpecialPermissionChange(true, "app", mContext); + verify(mFeatureFactory.metricsFeatureProvider).action(any(Context.class), + eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_DND_ALLOW), + eq("app")); + + ZenAccessController.logSpecialPermissionChange(false, "app", mContext); + verify(mFeatureFactory.metricsFeatureProvider).action(any(Context.class), + eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_DND_DENY), + eq("app")); + } + + @Test + @Config(shadows = ShadowNotificationManager.class) + public void hasAccess_granted_yes() { + final ShadowNotificationManager snm = Shadow.extract(mContext.getSystemService( + NotificationManager.class)); + snm.setNotificationPolicyAccessGrantedForPackage(TEST_PKG); + assertThat(ZenAccessController.hasAccess(mContext, TEST_PKG)).isTrue(); + } + + @Test + @Config(shadows = ShadowNotificationManager.class) + public void hasAccess_notGranted_no() { + assertThat(ZenAccessController.hasAccess(mContext, TEST_PKG)).isFalse(); + } } diff --git a/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixinTest.java b/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixinTest.java new file mode 100644 index 00000000000..cba1a5199ed --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixinTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2019 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.zenaccess; + +import static androidx.lifecycle.Lifecycle.Event.ON_START; +import static androidx.lifecycle.Lifecycle.Event.ON_STOP; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManager; +import android.content.Context; +import android.provider.Settings; + +import androidx.lifecycle.LifecycleOwner; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +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; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowActivityManager; + +@RunWith(RobolectricTestRunner.class) +public class ZenAccessSettingObserverMixinTest { + + @Mock + private ZenAccessSettingObserverMixin.Listener mListener; + + private Context mContext; + private LifecycleOwner mLifecycleOwner; + private Lifecycle mLifecycle; + private ZenAccessSettingObserverMixin mMixin; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + + mMixin = new ZenAccessSettingObserverMixin(mContext, mListener); + + mLifecycle.addObserver(mMixin); + } + + @Test + public void onStart_lowMemory_shouldNotRegisterListener() { + final ShadowActivityManager sam = Shadow.extract( + mContext.getSystemService(ActivityManager.class)); + sam.setIsLowRamDevice(true); + + mLifecycle.handleLifecycleEvent(ON_START); + + mContext.getContentResolver().notifyChange(Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), null); + + verify(mListener, never()).onZenAccessPolicyChanged(); + } + + @Test + public void onStart_highMemory_shouldRegisterListener() { + final ShadowActivityManager sam = Shadow.extract( + mContext.getSystemService(ActivityManager.class)); + sam.setIsLowRamDevice(false); + + mLifecycle.handleLifecycleEvent(ON_START); + + mContext.getContentResolver().notifyChange(Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), null); + + verify(mListener).onZenAccessPolicyChanged(); + } + + @Test + public void onStop_shouldUnregisterListener() { + final ShadowActivityManager sam = Shadow.extract( + mContext.getSystemService(ActivityManager.class)); + sam.setIsLowRamDevice(false); + + mLifecycle.handleLifecycleEvent(ON_START); + mLifecycle.handleLifecycleEvent(ON_STOP); + + mContext.getContentResolver().notifyChange(Settings.Secure.getUriFor( + Settings.Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), null); + + verify(mListener, never()).onZenAccessPolicyChanged(); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/ZenAccessSettingsTest.java b/tests/robotests/src/com/android/settings/notification/ZenAccessSettingsTest.java deleted file mode 100644 index c2a6f4f892c..00000000000 --- a/tests/robotests/src/com/android/settings/notification/ZenAccessSettingsTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2017 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; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; - -import android.content.Context; - -import com.android.internal.logging.nano.MetricsProto; -import com.android.settings.testutils.FakeFeatureFactory; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Answers; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; - -@RunWith(RobolectricTestRunner.class) -public class ZenAccessSettingsTest { - - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Context mContext; - - private FakeFeatureFactory mFeatureFactory; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - mFeatureFactory = FakeFeatureFactory.setupForTest(); - } - - @Test - public void logSpecialPermissionChange() { - ZenAccessSettings.logSpecialPermissionChange(true, "app", mContext); - verify(mFeatureFactory.metricsFeatureProvider).action(any(Context.class), - eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_DND_ALLOW), - eq("app")); - - ZenAccessSettings.logSpecialPermissionChange(false, "app", mContext); - verify(mFeatureFactory.metricsFeatureProvider).action(any(Context.class), - eq(MetricsProto.MetricsEvent.APP_SPECIAL_PERMISSION_DND_DENY), - eq("app")); - } -} diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java index 83257776289..78fb23f3133 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java @@ -19,15 +19,19 @@ package com.android.settings.testutils.shadow; import android.app.NotificationManager; import android.net.Uri; import android.service.notification.ZenModeConfig; +import android.util.ArraySet; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import java.util.Set; + @Implements(NotificationManager.class) public class ShadowNotificationManager { private int mZenMode; private ZenModeConfig mZenModeConfig; + private Set mNotificationPolicyGrantedPackages = new ArraySet<>(); @Implementation protected void setZenMode(int mode, Uri conditionId, String reason) { @@ -39,6 +43,11 @@ public class ShadowNotificationManager { return mZenMode; } + @Implementation + protected boolean isNotificationPolicyAccessGrantedForPackage(String pkg) { + return mNotificationPolicyGrantedPackages.contains(pkg); + } + @Implementation public ZenModeConfig getZenModeConfig() { return mZenModeConfig; @@ -47,4 +56,8 @@ public class ShadowNotificationManager { public void setZenModeConfig(ZenModeConfig config) { mZenModeConfig = config; } + + public void setNotificationPolicyAccessGrantedForPackage(String pkg) { + mNotificationPolicyGrantedPackages.add(pkg); + } }