1 /*
2  * Copyright (C) 2010 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.app.Dialog;
20 import android.app.NotificationManager;
21 import android.app.settings.SettingsEnums;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.os.AsyncTask;
25 import android.os.Bundle;
26 import android.os.UserManager;
27 import android.provider.SearchIndexableResource;
28 import android.provider.Settings;
29 import android.service.notification.NotificationListenerService;
30 import android.widget.Toast;
31 
32 import androidx.annotation.VisibleForTesting;
33 import androidx.appcompat.app.AlertDialog;
34 import androidx.fragment.app.Fragment;
35 
36 import com.android.settings.R;
37 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
38 import com.android.settings.overlay.FeatureFactory;
39 import com.android.settings.search.BaseSearchIndexProvider;
40 import com.android.settings.search.Indexable;
41 import com.android.settings.utils.ManagedServiceSettings;
42 import com.android.settingslib.search.SearchIndexable;
43 
44 import java.util.ArrayList;
45 import java.util.List;
46 
47 /**
48  * Settings screen for managing notification listener permissions
49  */
50 @SearchIndexable
51 public class NotificationAccessSettings extends ManagedServiceSettings {
52     private static final String TAG = "NotificationAccessSettings";
53     private static final Config CONFIG = new Config.Builder()
54             .setTag(TAG)
55             .setSetting(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS)
56             .setIntentAction(NotificationListenerService.SERVICE_INTERFACE)
57             .setPermission(android.Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE)
58             .setNoun("notification listener")
59             .setWarningDialogTitle(R.string.notification_listener_security_warning_title)
60             .setWarningDialogSummary(R.string.notification_listener_security_warning_summary)
61             .setEmptyText(R.string.no_notification_listeners)
62             .build();
63 
64     private NotificationManager mNm;
65 
66     @Override
onCreate(Bundle icicle)67     public void onCreate(Bundle icicle) {
68         super.onCreate(icicle);
69         final Context ctx = getContext();
70         if (UserManager.get(ctx).isManagedProfile()) {
71             // Apps in the work profile do not support notification listeners.
72             Toast.makeText(ctx, R.string.notification_settings_work_profile, Toast.LENGTH_SHORT)
73                 .show();
74             finish();
75         }
76     }
77 
78     @Override
getMetricsCategory()79     public int getMetricsCategory() {
80         return SettingsEnums.NOTIFICATION_ACCESS;
81     }
82 
83     @Override
onAttach(Context context)84     public void onAttach(Context context) {
85         super.onAttach(context);
86         mNm = context.getSystemService(NotificationManager.class);
87     }
88 
89     @Override
getConfig()90     protected Config getConfig() {
91         return CONFIG;
92     }
93 
94     @Override
setEnabled(ComponentName service, String title, boolean enable)95     protected boolean setEnabled(ComponentName service, String title, boolean enable) {
96         logSpecialPermissionChange(enable, service.getPackageName());
97         if (!enable) {
98             if (!isServiceEnabled(service)) {
99                 return true; // already disabled
100             }
101             // show a friendly dialog
102             new FriendlyWarningDialogFragment()
103                     .setServiceInfo(service, title, this)
104                     .show(getFragmentManager(), "friendlydialog");
105             return false;
106         } else {
107             if (isServiceEnabled(service)) {
108                 return true; // already enabled
109             }
110             // show a scary dialog
111             new ScaryWarningDialogFragment()
112                     .setServiceInfo(service, title, this)
113                     .show(getFragmentManager(), "dialog");
114             return false;
115         }
116     }
117 
118     @Override
isServiceEnabled(ComponentName cn)119     protected boolean isServiceEnabled(ComponentName cn) {
120         return mNm.isNotificationListenerAccessGranted(cn);
121     }
122 
123     @Override
enable(ComponentName service)124     protected void enable(ComponentName service) {
125         mNm.setNotificationListenerAccessGranted(service, true);
126     }
127 
128     @Override
getPreferenceScreenResId()129     protected int getPreferenceScreenResId() {
130         return R.xml.notification_access_settings;
131     }
132 
133     @VisibleForTesting
logSpecialPermissionChange(boolean enable, String packageName)134     void logSpecialPermissionChange(boolean enable, String packageName) {
135         int logCategory = enable ? SettingsEnums.APP_SPECIAL_PERMISSION_NOTIVIEW_ALLOW
136                 : SettingsEnums.APP_SPECIAL_PERMISSION_NOTIVIEW_DENY;
137         FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider().action(getContext(),
138                 logCategory, packageName);
139     }
140 
disable(final NotificationAccessSettings parent, final ComponentName cn)141     private static void disable(final NotificationAccessSettings parent, final ComponentName cn) {
142         parent.mNm.setNotificationListenerAccessGranted(cn, false);
143         AsyncTask.execute(() -> {
144             if (!parent.mNm.isNotificationPolicyAccessGrantedForPackage(
145                     cn.getPackageName())) {
146                 parent.mNm.removeAutomaticZenRules(cn.getPackageName());
147             }
148         });
149     }
150 
151     public static class FriendlyWarningDialogFragment extends InstrumentedDialogFragment {
152         static final String KEY_COMPONENT = "c";
153         static final String KEY_LABEL = "l";
154 
setServiceInfo(ComponentName cn, String label, Fragment target)155         public FriendlyWarningDialogFragment setServiceInfo(ComponentName cn, String label,
156                 Fragment target) {
157             Bundle args = new Bundle();
158             args.putString(KEY_COMPONENT, cn.flattenToString());
159             args.putString(KEY_LABEL, label);
160             setArguments(args);
161             setTargetFragment(target, 0);
162             return this;
163         }
164 
165         @Override
getMetricsCategory()166         public int getMetricsCategory() {
167             return SettingsEnums.DIALOG_DISABLE_NOTIFICATION_ACCESS;
168         }
169 
170         @Override
onCreateDialog(Bundle savedInstanceState)171         public Dialog onCreateDialog(Bundle savedInstanceState) {
172             final Bundle args = getArguments();
173             final String label = args.getString(KEY_LABEL);
174             final ComponentName cn = ComponentName.unflattenFromString(args
175                     .getString(KEY_COMPONENT));
176             NotificationAccessSettings parent = (NotificationAccessSettings) getTargetFragment();
177 
178             final String summary = getResources().getString(
179                     R.string.notification_listener_disable_warning_summary, label);
180             return new AlertDialog.Builder(getContext())
181                     .setMessage(summary)
182                     .setCancelable(true)
183                     .setPositiveButton(R.string.notification_listener_disable_warning_confirm,
184                             (dialog, id) -> disable(parent, cn))
185                     .setNegativeButton(R.string.notification_listener_disable_warning_cancel,
186                             (dialog, id) -> {
187                                 // pass
188                             })
189                     .create();
190         }
191     }
192 
193     public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
194             new BaseSearchIndexProvider() {
195                 @Override
196                 public List<SearchIndexableResource> getXmlResourcesToIndex(Context context,
197                         boolean enabled) {
198                     final List<SearchIndexableResource> result = new ArrayList<>();
199 
200                     final SearchIndexableResource sir = new SearchIndexableResource(context);
201                     sir.xmlResId = R.xml.notification_access_settings;
202                     result.add(sir);
203                     return result;
204                 }
205             };
206 }
207