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