1 /*
2  * Copyright 2019 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.car.settings.applications.specialaccess;
18 
19 import android.Manifest;
20 import android.app.NotificationManager;
21 import android.car.drivingstate.CarUxRestrictions;
22 import android.content.ComponentName;
23 import android.content.Context;
24 import android.content.pm.PackageItemInfo;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ServiceInfo;
27 import android.os.AsyncTask;
28 import android.os.UserHandle;
29 import android.provider.Settings;
30 import android.service.notification.NotificationListenerService;
31 import android.util.IconDrawableFactory;
32 
33 import androidx.annotation.VisibleForTesting;
34 import androidx.preference.PreferenceGroup;
35 import androidx.preference.SwitchPreference;
36 
37 import com.android.car.settings.R;
38 import com.android.car.settings.common.ConfirmationDialogFragment;
39 import com.android.car.settings.common.FragmentController;
40 import com.android.car.settings.common.Logger;
41 import com.android.car.settings.common.PreferenceController;
42 import com.android.car.ui.preference.CarUiSwitchPreference;
43 import com.android.settingslib.applications.ServiceListing;
44 
45 import java.util.List;
46 
47 /**
48  * Displays a list of notification listener services and provides toggles to allow the user to
49  * grant/revoke permission for listening to notifications. Before changing the value of a
50  * permission, the user is shown a confirmation dialog with information about the risks and
51  * potential effects.
52  */
53 public class NotificationAccessPreferenceController extends PreferenceController<PreferenceGroup> {
54 
55     private static final Logger LOG = new Logger(NotificationAccessPreferenceController.class);
56 
57     @VisibleForTesting
58     static final String GRANT_CONFIRM_DIALOG_TAG =
59             "com.android.car.settings.applications.specialaccess.GrantNotificationAccessDialog";
60     @VisibleForTesting
61     static final String REVOKE_CONFIRM_DIALOG_TAG =
62             "com.android.car.settings.applications.specialaccess.RevokeNotificationAccessDialog";
63     private static final String KEY_SERVICE = "service";
64 
65     private final NotificationManager mNm;
66     private final ServiceListing mServiceListing;
67     private final IconDrawableFactory mIconDrawableFactory;
68 
69     private final ServiceListing.Callback mCallback = this::onServicesReloaded;
70     @VisibleForTesting
71     AsyncTask<Void, Void, Void> mAsyncTask;
72 
73     private final ConfirmationDialogFragment.ConfirmListener mGrantConfirmListener = arguments -> {
74         ComponentName service = arguments.getParcelable(KEY_SERVICE);
75         grantNotificationAccess(service);
76     };
77     private final ConfirmationDialogFragment.ConfirmListener mRevokeConfirmListener =
78             arguments -> {
79                 ComponentName service = arguments.getParcelable(KEY_SERVICE);
80                 revokeNotificationAccess(service);
81             };
82 
NotificationAccessPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)83     public NotificationAccessPreferenceController(Context context, String preferenceKey,
84             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
85         this(context, preferenceKey, fragmentController, uxRestrictions,
86                 context.getSystemService(NotificationManager.class));
87     }
88 
89     @VisibleForTesting
NotificationAccessPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions, NotificationManager notificationManager)90     NotificationAccessPreferenceController(Context context, String preferenceKey,
91             FragmentController fragmentController, CarUxRestrictions uxRestrictions,
92             NotificationManager notificationManager) {
93         super(context, preferenceKey, fragmentController, uxRestrictions);
94         mNm = notificationManager;
95         mServiceListing = new ServiceListing.Builder(context)
96                 .setPermission(Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE)
97                 .setIntentAction(NotificationListenerService.SERVICE_INTERFACE)
98                 .setSetting(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS)
99                 .setTag(NotificationAccessPreferenceController.class.getSimpleName())
100                 .setNoun("notification listener") // For logging.
101                 .build();
102         mIconDrawableFactory = IconDrawableFactory.newInstance(context);
103     }
104 
105     @Override
getPreferenceType()106     protected Class<PreferenceGroup> getPreferenceType() {
107         return PreferenceGroup.class;
108     }
109 
110     @Override
onCreateInternal()111     protected void onCreateInternal() {
112         ConfirmationDialogFragment grantConfirmDialogFragment =
113                 (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
114                         GRANT_CONFIRM_DIALOG_TAG);
115         ConfirmationDialogFragment.resetListeners(
116                 grantConfirmDialogFragment,
117                 mGrantConfirmListener,
118                 /* rejectListener= */ null,
119                 /* neutralListener= */ null);
120 
121         ConfirmationDialogFragment revokeConfirmDialogFragment =
122                 (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
123                         REVOKE_CONFIRM_DIALOG_TAG);
124         ConfirmationDialogFragment.resetListeners(
125                 revokeConfirmDialogFragment,
126                 mRevokeConfirmListener,
127                 /* rejectListener= */ null,
128                 /* neutralListener= */ null);
129 
130         mServiceListing.addCallback(mCallback);
131     }
132 
133     @Override
onStartInternal()134     protected void onStartInternal() {
135         mServiceListing.reload();
136         mServiceListing.setListening(true);
137     }
138 
139     @Override
onStopInternal()140     protected void onStopInternal() {
141         mServiceListing.setListening(false);
142     }
143 
144     @Override
onDestroyInternal()145     protected void onDestroyInternal() {
146         mServiceListing.removeCallback(mCallback);
147     }
148 
149     @VisibleForTesting
onServicesReloaded(List<ServiceInfo> services)150     void onServicesReloaded(List<ServiceInfo> services) {
151         PackageManager packageManager = getContext().getPackageManager();
152         services.sort(new PackageItemInfo.DisplayNameComparator(packageManager));
153         getPreference().removeAll();
154         for (ServiceInfo service : services) {
155             ComponentName cn = new ComponentName(service.packageName, service.name);
156             CharSequence title = null;
157             try {
158                 title = packageManager.getApplicationInfoAsUser(service.packageName, /* flags= */ 0,
159                         UserHandle.myUserId()).loadLabel(packageManager);
160             } catch (PackageManager.NameNotFoundException e) {
161                 LOG.e("can't find package name", e);
162             }
163             String summary = service.loadLabel(packageManager).toString();
164             SwitchPreference pref = new CarUiSwitchPreference(getContext());
165             pref.setPersistent(false);
166             pref.setIcon(mIconDrawableFactory.getBadgedIcon(service, service.applicationInfo,
167                     UserHandle.getUserId(service.applicationInfo.uid)));
168             if (title != null && !title.equals(summary)) {
169                 pref.setTitle(title);
170                 pref.setSummary(summary);
171             } else {
172                 pref.setTitle(summary);
173             }
174             pref.setKey(cn.flattenToString());
175             pref.setChecked(isAccessGranted(cn));
176             pref.setOnPreferenceChangeListener((preference, newValue) -> {
177                 boolean enable = (boolean) newValue;
178                 return promptUserToConfirmChange(cn, summary, enable);
179             });
180             getPreference().addPreference(pref);
181         }
182     }
183 
isAccessGranted(ComponentName service)184     private boolean isAccessGranted(ComponentName service) {
185         return mNm.isNotificationListenerAccessGranted(service);
186     }
187 
grantNotificationAccess(ComponentName service)188     private void grantNotificationAccess(ComponentName service) {
189         mNm.setNotificationListenerAccessGranted(service, /* granted= */ true);
190     }
191 
revokeNotificationAccess(ComponentName service)192     private void revokeNotificationAccess(ComponentName service) {
193         mNm.setNotificationListenerAccessGranted(service, /* granted= */ false);
194         mAsyncTask = new AsyncTask<Void, Void, Void>() {
195             @Override
196             protected Void doInBackground(Void... unused) {
197                 if (!mNm.isNotificationPolicyAccessGrantedForPackage(service.getPackageName())) {
198                     mNm.removeAutomaticZenRules(service.getPackageName());
199                 }
200                 return null;
201             }
202         };
203         mAsyncTask.execute();
204     }
205 
promptUserToConfirmChange(ComponentName service, String label, boolean grantAccess)206     private boolean promptUserToConfirmChange(ComponentName service, String label,
207             boolean grantAccess) {
208         if (isAccessGranted(service) == grantAccess) {
209             return true;
210         }
211         ConfirmationDialogFragment.Builder dialogFragment =
212                 grantAccess ? createConfirmGrantDialogFragment(label)
213                         : createConfirmRevokeDialogFragment(label);
214         dialogFragment.addArgumentParcelable(KEY_SERVICE, service);
215         getFragmentController().showDialog(dialogFragment.build(),
216                 grantAccess ? GRANT_CONFIRM_DIALOG_TAG : REVOKE_CONFIRM_DIALOG_TAG);
217         return false;
218     }
219 
createConfirmGrantDialogFragment(String label)220     private ConfirmationDialogFragment.Builder createConfirmGrantDialogFragment(String label) {
221         String title = getContext().getResources().getString(
222                 R.string.notification_listener_security_warning_title, label);
223         String summary = getContext().getResources().getString(
224                 R.string.notification_listener_security_warning_summary, label);
225         return new ConfirmationDialogFragment.Builder(getContext())
226                 .setTitle(title)
227                 .setMessage(summary)
228                 .setPositiveButton(R.string.allow, mGrantConfirmListener)
229                 .setNegativeButton(R.string.deny, /* rejectionListener= */ null);
230     }
231 
createConfirmRevokeDialogFragment(String label)232     private ConfirmationDialogFragment.Builder createConfirmRevokeDialogFragment(String label) {
233         String summary = getContext().getResources().getString(
234                 R.string.notification_listener_revoke_warning_summary, label);
235         return new ConfirmationDialogFragment.Builder(getContext())
236                 .setMessage(summary)
237                 .setPositiveButton(R.string.notification_listener_revoke_warning_confirm,
238                         mRevokeConfirmListener)
239                 .setNegativeButton(R.string.notification_listener_revoke_warning_cancel,
240                         /* rejectionListener= */ null);
241     }
242 }
243