1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.notification;
18 
19 import android.annotation.Nullable;
20 import android.app.AlertDialog;
21 import android.app.Dialog;
22 import android.app.DialogFragment;
23 import android.app.NotificationManager;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.DialogInterface;
27 import android.content.pm.ApplicationInfo;
28 import android.content.pm.PackageItemInfo;
29 import android.content.pm.PackageManager;
30 import android.database.ContentObserver;
31 import android.net.Uri;
32 import android.os.AsyncTask;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.Looper;
36 import android.provider.Settings;
37 import android.provider.Settings.Secure;
38 import android.support.v14.preference.SwitchPreference;
39 import android.support.v7.preference.Preference;
40 import android.support.v7.preference.Preference.OnPreferenceChangeListener;
41 import android.support.v7.preference.PreferenceScreen;
42 import android.text.TextUtils;
43 import android.util.ArraySet;
44 import android.view.View;
45 import android.widget.Toast;
46 
47 import com.android.internal.annotations.VisibleForTesting;
48 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
49 import com.android.settings.R;
50 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
51 import com.android.settings.overlay.FeatureFactory;
52 
53 import java.util.ArrayList;
54 import java.util.Collections;
55 import java.util.List;
56 
57 public class ZenAccessSettings extends EmptyTextSettings {
58 
59     private final SettingObserver mObserver = new SettingObserver();
60     private static final String ENABLED_SERVICES_SEPARATOR = ":";
61 
62     private Context mContext;
63     private PackageManager mPkgMan;
64     private NotificationManager mNoMan;
65 
66     @Override
getMetricsCategory()67     public int getMetricsCategory() {
68         return MetricsEvent.NOTIFICATION_ZEN_MODE_ACCESS;
69     }
70 
71     @Override
onCreate(Bundle icicle)72     public void onCreate(Bundle icicle) {
73         super.onCreate(icicle);
74 
75         mContext = getActivity();
76         mPkgMan = mContext.getPackageManager();
77         mNoMan = mContext.getSystemService(NotificationManager.class);
78         setPreferenceScreen(getPreferenceManager().createPreferenceScreen(mContext));
79     }
80 
81     @Override
onViewCreated(View view, @Nullable Bundle savedInstanceState)82     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
83         super.onViewCreated(view, savedInstanceState);
84         setEmptyText(R.string.zen_access_empty_text);
85     }
86 
87     @Override
onResume()88     public void onResume() {
89         super.onResume();
90         reloadList();
91         getContentResolver().registerContentObserver(
92                 Secure.getUriFor(Secure.ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES), false,
93                 mObserver);
94         getContentResolver().registerContentObserver(
95                 Secure.getUriFor(Secure.ENABLED_NOTIFICATION_LISTENERS), false,
96                 mObserver);
97     }
98 
99     @Override
onPause()100     public void onPause() {
101         super.onPause();
102         getContentResolver().unregisterContentObserver(mObserver);
103     }
104 
reloadList()105     private void reloadList() {
106         final PreferenceScreen screen = getPreferenceScreen();
107         screen.removeAll();
108         final ArrayList<ApplicationInfo> apps = new ArrayList<>();
109         final ArraySet<String> requesting = mNoMan.getPackagesRequestingNotificationPolicyAccess();
110         if (!requesting.isEmpty()) {
111             final List<ApplicationInfo> installed = mPkgMan.getInstalledApplications(0);
112             if (installed != null) {
113                 for (ApplicationInfo app : installed) {
114                     if (requesting.contains(app.packageName)) {
115                         apps.add(app);
116                     }
117                 }
118             }
119         }
120         ArraySet<String> autoApproved = getEnabledNotificationListeners();
121         requesting.addAll(autoApproved);
122         Collections.sort(apps, new PackageItemInfo.DisplayNameComparator(mPkgMan));
123         for (ApplicationInfo app : apps) {
124             final String pkg = app.packageName;
125             final CharSequence label = app.loadLabel(mPkgMan);
126             final SwitchPreference pref = new SwitchPreference(getPrefContext());
127             pref.setPersistent(false);
128             pref.setIcon(app.loadIcon(mPkgMan));
129             pref.setTitle(label);
130             pref.setChecked(hasAccess(pkg));
131             if (autoApproved.contains(pkg)) {
132                 pref.setEnabled(false);
133                 pref.setSummary(getString(R.string.zen_access_disabled_package_warning));
134             }
135             pref.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
136                 @Override
137                 public boolean onPreferenceChange(Preference preference, Object newValue) {
138                     final boolean access = (Boolean) newValue;
139                     if (access) {
140                         new ScaryWarningDialogFragment()
141                                 .setPkgInfo(pkg, label)
142                                 .show(getFragmentManager(), "dialog");
143                     } else {
144                         new FriendlyWarningDialogFragment()
145                                 .setPkgInfo(pkg, label)
146                                 .show(getFragmentManager(), "dialog");
147                     }
148                     return false;
149                 }
150             });
151             screen.addPreference(pref);
152         }
153     }
154 
getEnabledNotificationListeners()155     private ArraySet<String> getEnabledNotificationListeners() {
156         ArraySet<String> packages = new ArraySet<>();
157         String settingValue = Settings.Secure.getString(getContext().getContentResolver(),
158                 Settings.Secure.ENABLED_NOTIFICATION_LISTENERS);
159         if (!TextUtils.isEmpty(settingValue)) {
160             String[] restored = settingValue.split(ENABLED_SERVICES_SEPARATOR);
161             for (int i = 0; i < restored.length; i++) {
162                 ComponentName value = ComponentName.unflattenFromString(restored[i]);
163                 if (null != value) {
164                     packages.add(value.getPackageName());
165                 }
166             }
167         }
168         return packages;
169     }
170 
hasAccess(String pkg)171     private boolean hasAccess(String pkg) {
172         return mNoMan.isNotificationPolicyAccessGrantedForPackage(pkg);
173     }
174 
setAccess(final Context context, final String pkg, final boolean access)175     private static void setAccess(final Context context, final String pkg, final boolean access) {
176         logSpecialPermissionChange(access, pkg, context);
177         AsyncTask.execute(new Runnable() {
178             @Override
179             public void run() {
180                 final NotificationManager mgr = context.getSystemService(NotificationManager.class);
181                 mgr.setNotificationPolicyAccessGranted(pkg, access);
182             }
183         });
184     }
185 
186     @VisibleForTesting
logSpecialPermissionChange(boolean enable, String packageName, Context context)187     static void logSpecialPermissionChange(boolean enable, String packageName, Context context) {
188         int logCategory = enable ? MetricsEvent.APP_SPECIAL_PERMISSION_DND_ALLOW
189                 : MetricsEvent.APP_SPECIAL_PERMISSION_DND_DENY;
190         FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context,
191                 logCategory, packageName);
192     }
193 
194 
deleteRules(final Context context, final String pkg)195     private static void deleteRules(final Context context, final String pkg) {
196         AsyncTask.execute(new Runnable() {
197             @Override
198             public void run() {
199                 final NotificationManager mgr = context.getSystemService(NotificationManager.class);
200                 mgr.removeAutomaticZenRules(pkg);
201             }
202         });
203     }
204 
205     private final class SettingObserver extends ContentObserver {
SettingObserver()206         public SettingObserver() {
207             super(new Handler(Looper.getMainLooper()));
208         }
209 
210         @Override
onChange(boolean selfChange, Uri uri)211         public void onChange(boolean selfChange, Uri uri) {
212             reloadList();
213         }
214     }
215 
216     /**
217      * Warning dialog when allowing zen access warning about the privileges being granted.
218      */
219     public static class ScaryWarningDialogFragment extends InstrumentedDialogFragment {
220         static final String KEY_PKG = "p";
221         static final String KEY_LABEL = "l";
222 
223         @Override
getMetricsCategory()224         public int getMetricsCategory() {
225             return MetricsEvent.DIALOG_ZEN_ACCESS_GRANT;
226         }
227 
setPkgInfo(String pkg, CharSequence label)228         public ScaryWarningDialogFragment setPkgInfo(String pkg, CharSequence label) {
229             Bundle args = new Bundle();
230             args.putString(KEY_PKG, pkg);
231             args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString());
232             setArguments(args);
233             return this;
234         }
235 
236         @Override
onCreateDialog(Bundle savedInstanceState)237         public Dialog onCreateDialog(Bundle savedInstanceState) {
238             super.onCreate(savedInstanceState);
239             final Bundle args = getArguments();
240             final String pkg = args.getString(KEY_PKG);
241             final String label = args.getString(KEY_LABEL);
242 
243             final String title = getResources().getString(R.string.zen_access_warning_dialog_title,
244                     label);
245             final String summary = getResources()
246                     .getString(R.string.zen_access_warning_dialog_summary);
247             return new AlertDialog.Builder(getContext())
248                     .setMessage(summary)
249                     .setTitle(title)
250                     .setCancelable(true)
251                     .setPositiveButton(R.string.allow,
252                             new DialogInterface.OnClickListener() {
253                                 public void onClick(DialogInterface dialog, int id) {
254                                     setAccess(getContext(), pkg, true);
255                                 }
256                             })
257                     .setNegativeButton(R.string.deny,
258                             new DialogInterface.OnClickListener() {
259                                 public void onClick(DialogInterface dialog, int id) {
260                                     // pass
261                                 }
262                             })
263                     .create();
264         }
265     }
266 
267     /**
268      * Warning dialog when revoking zen access warning that zen rule instances will be deleted.
269      */
270     public static class FriendlyWarningDialogFragment extends InstrumentedDialogFragment {
271         static final String KEY_PKG = "p";
272         static final String KEY_LABEL = "l";
273 
274 
275         @Override
276         public int getMetricsCategory() {
277             return MetricsEvent.DIALOG_ZEN_ACCESS_REVOKE;
278         }
279 
280         public FriendlyWarningDialogFragment setPkgInfo(String pkg, CharSequence label) {
281             Bundle args = new Bundle();
282             args.putString(KEY_PKG, pkg);
283             args.putString(KEY_LABEL, TextUtils.isEmpty(label) ? pkg : label.toString());
284             setArguments(args);
285             return this;
286         }
287 
288         @Override
289         public Dialog onCreateDialog(Bundle savedInstanceState) {
290             super.onCreate(savedInstanceState);
291             final Bundle args = getArguments();
292             final String pkg = args.getString(KEY_PKG);
293             final String label = args.getString(KEY_LABEL);
294 
295             final String title = getResources().getString(
296                     R.string.zen_access_revoke_warning_dialog_title, label);
297             final String summary = getResources()
298                     .getString(R.string.zen_access_revoke_warning_dialog_summary);
299             return new AlertDialog.Builder(getContext())
300                     .setMessage(summary)
301                     .setTitle(title)
302                     .setCancelable(true)
303                     .setPositiveButton(R.string.okay,
304                             new DialogInterface.OnClickListener() {
305                                 public void onClick(DialogInterface dialog, int id) {
306                                     deleteRules(getContext(), pkg);
307                                     setAccess(getContext(), pkg, false);
308                                 }
309                             })
310                     .setNegativeButton(R.string.cancel,
311                             new DialogInterface.OnClickListener() {
312                                 public void onClick(DialogInterface dialog, int id) {
313                                     // pass
314                                 }
315                             })
316                     .create();
317         }
318     }
319 }
320