OSDN Git Service

Create a page to manage dnd permission for individual app
authorFan Zhang <zhfan@google.com>
Fri, 15 Mar 2019 23:41:24 +0000 (16:41 -0700)
committerFan Zhang <zhfan@google.com>
Mon, 18 Mar 2019 21:42:31 +0000 (14:42 -0700)
- 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

12 files changed:
res/values/strings.xml
res/xml/zen_access_permission_details.xml [new file with mode: 0644]
src/com/android/settings/applications/specialaccess/zenaccess/FriendlyWarningDialogFragment.java [new file with mode: 0644]
src/com/android/settings/applications/specialaccess/zenaccess/ScaryWarningDialogFragment.java [new file with mode: 0644]
src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessController.java
src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessDetails.java [new file with mode: 0644]
src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixin.java [new file with mode: 0644]
src/com/android/settings/notification/ZenAccessSettings.java
tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessControllerTest.java
tests/robotests/src/com/android/settings/applications/specialaccess/zenaccess/ZenAccessSettingObserverMixinTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/notification/ZenAccessSettingsTest.java [deleted file]
tests/robotests/src/com/android/settings/testutils/shadow/ShadowNotificationManager.java

index e4d0d11..faef8a0 100644 (file)
     <!-- Sound & notification > Advanced section: Title for managing Do Not Disturb access option. [CHAR LIMIT=40] -->
     <string name="manage_zen_access_title">Do Not Disturb access</string>
 
+    <!-- Button title that grants 'Do Not Disturb' permission to an app [CHAR_LIMIT=60]-->
+    <string name="zen_access_detail_switch">Allow Do Not Disturb</string>
+
     <!-- Sound & notification > Do Not Disturb access > Text to display when the list is empty. [CHAR LIMIT=NONE] -->
     <string name="zen_access_empty_text">No installed apps have requested Do Not Disturb access</string>
 
diff --git a/res/xml/zen_access_permission_details.xml b/res/xml/zen_access_permission_details.xml
new file mode 100644 (file)
index 0000000..afa8d80
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:key="zen_access_permission_detail_settings"
+    android:title="@string/manage_zen_access_title">
+
+    <SwitchPreference
+        android:key="zen_access_switch"
+        android:title="@string/zen_access_detail_switch"/>
+
+</PreferenceScreen>
\ 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 (file)
index 0000000..fc85f7d
--- /dev/null
@@ -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 (file)
index 0000000..69318f8
--- /dev/null
@@ -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();
+    }
+}
index 88d444d..946599b 100644 (file)
 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<String> getPackagesRequestingNotificationPolicyAccess() {
+        final ArraySet<String> 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<PackageInfo> 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<String> getAutoApprovedPackages(Context context) {
+        final Set<String> 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 (file)
index 0000000..a18e7d6
--- /dev/null
@@ -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<String> 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<String> 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 (file)
index 0000000..30507ef
--- /dev/null
@@ -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 */);
+    }
+}
index d057c75..fca8255 100644 (file)
@@ -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<ApplicationInfo> apps = new ArrayList<>();
-        final ArraySet<String> requesting = getPackagesRequestingNotificationPolicyAccess();
+        final Set<String> requesting =
+                ZenAccessController.getPackagesRequestingNotificationPolicyAccess();
         if (!requesting.isEmpty()) {
             final List<ApplicationInfo> 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<String> getPackagesRequestingNotificationPolicyAccess() {
-        ArraySet<String> 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<PackageInfo> 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.
-     */
-    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();
+            screen.addPreference(pref);
         }
     }
 
     /**
-     * Warning dialog when revoking zen access warning that zen rule instances will be deleted.
+     * @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 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 =
index bcb4bb3..6041e9d 100644 (file)
@@ -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 (file)
index 0000000..cba1a51
--- /dev/null
@@ -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 (file)
index c2a6f4f..0000000
+++ /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"));
-    }
-}
index 8325777..78fb23f 100644 (file)
@@ -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<String> mNotificationPolicyGrantedPackages = new ArraySet<>();
 
     @Implementation
     protected void setZenMode(int mode, Uri conditionId, String reason) {
@@ -40,6 +44,11 @@ public class ShadowNotificationManager {
     }
 
     @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);
+    }
 }