• 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.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 
71     private final ConfirmationDialogFragment.ConfirmListener mGrantConfirmListener = arguments -> {
72         ComponentName service = arguments.getParcelable(KEY_SERVICE);
73         grantNotificationAccess(service);
74     };
75     private final ConfirmationDialogFragment.ConfirmListener mRevokeConfirmListener =
76             arguments -> {
77                 ComponentName service = arguments.getParcelable(KEY_SERVICE);
78                 revokeNotificationAccess(service);
79             };
80 
NotificationAccessPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)81     public NotificationAccessPreferenceController(Context context, String preferenceKey,
82             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
83         super(context, preferenceKey, fragmentController, uxRestrictions);
84         mNm = context.getSystemService(NotificationManager.class);
85         mServiceListing = new ServiceListing.Builder(context)
86                 .setPermission(Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE)
87                 .setIntentAction(NotificationListenerService.SERVICE_INTERFACE)
88                 .setSetting(Settings.Secure.ENABLED_NOTIFICATION_LISTENERS)
89                 .setTag(NotificationAccessPreferenceController.class.getSimpleName())
90                 .setNoun("notification listener") // For logging.
91                 .build();
92         mIconDrawableFactory = IconDrawableFactory.newInstance(context);
93     }
94 
95     @Override
getPreferenceType()96     protected Class<PreferenceGroup> getPreferenceType() {
97         return PreferenceGroup.class;
98     }
99 
100     @Override
onCreateInternal()101     protected void onCreateInternal() {
102         ConfirmationDialogFragment grantConfirmDialogFragment =
103                 (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
104                         GRANT_CONFIRM_DIALOG_TAG);
105         ConfirmationDialogFragment.resetListeners(
106                 grantConfirmDialogFragment,
107                 mGrantConfirmListener,
108                 /* rejectListener= */ null,
109                 /* neutralListener= */ null);
110 
111         ConfirmationDialogFragment revokeConfirmDialogFragment =
112                 (ConfirmationDialogFragment) getFragmentController().findDialogByTag(
113                         REVOKE_CONFIRM_DIALOG_TAG);
114         ConfirmationDialogFragment.resetListeners(
115                 revokeConfirmDialogFragment,
116                 mRevokeConfirmListener,
117                 /* rejectListener= */ null,
118                 /* neutralListener= */ null);
119 
120         mServiceListing.addCallback(mCallback);
121     }
122 
123     @Override
onStartInternal()124     protected void onStartInternal() {
125         mServiceListing.reload();
126         mServiceListing.setListening(true);
127     }
128 
129     @Override
onStopInternal()130     protected void onStopInternal() {
131         mServiceListing.setListening(false);
132     }
133 
134     @Override
onDestroyInternal()135     protected void onDestroyInternal() {
136         mServiceListing.removeCallback(mCallback);
137     }
138 
onServicesReloaded(List<ServiceInfo> services)139     private void onServicesReloaded(List<ServiceInfo> services) {
140         PackageManager packageManager = getContext().getPackageManager();
141         services.sort(new PackageItemInfo.DisplayNameComparator(packageManager));
142         getPreference().removeAll();
143         for (ServiceInfo service : services) {
144             ComponentName cn = new ComponentName(service.packageName, service.name);
145             CharSequence title = null;
146             try {
147                 title = packageManager.getApplicationInfoAsUser(service.packageName, /* flags= */ 0,
148                         UserHandle.myUserId()).loadLabel(packageManager);
149             } catch (PackageManager.NameNotFoundException e) {
150                 LOG.e("can't find package name", e);
151             }
152             String summary = service.loadLabel(packageManager).toString();
153             SwitchPreference pref = new CarUiSwitchPreference(getContext());
154             pref.setPersistent(false);
155             pref.setIcon(mIconDrawableFactory.getBadgedIcon(service, service.applicationInfo,
156                     UserHandle.getUserId(service.applicationInfo.uid)));
157             if (title != null && !title.equals(summary)) {
158                 pref.setTitle(title);
159                 pref.setSummary(summary);
160             } else {
161                 pref.setTitle(summary);
162             }
163             pref.setKey(cn.flattenToString());
164             pref.setChecked(isAccessGranted(cn));
165             pref.setOnPreferenceChangeListener((preference, newValue) -> {
166                 boolean enable = (boolean) newValue;
167                 return promptUserToConfirmChange(cn, summary, enable);
168             });
169             getPreference().addPreference(pref);
170         }
171     }
172 
isAccessGranted(ComponentName service)173     private boolean isAccessGranted(ComponentName service) {
174         return mNm.isNotificationListenerAccessGranted(service);
175     }
176 
grantNotificationAccess(ComponentName service)177     private void grantNotificationAccess(ComponentName service) {
178         mNm.setNotificationListenerAccessGranted(service, /* granted= */ true);
179     }
180 
revokeNotificationAccess(ComponentName service)181     private void revokeNotificationAccess(ComponentName service) {
182         mNm.setNotificationListenerAccessGranted(service, /* granted= */ false);
183         AsyncTask.execute(() -> {
184             if (!mNm.isNotificationPolicyAccessGrantedForPackage(service.getPackageName())) {
185                 mNm.removeAutomaticZenRules(service.getPackageName());
186             }
187         });
188     }
189 
promptUserToConfirmChange(ComponentName service, String label, boolean grantAccess)190     private boolean promptUserToConfirmChange(ComponentName service, String label,
191             boolean grantAccess) {
192         if (isAccessGranted(service) == grantAccess) {
193             return true;
194         }
195         ConfirmationDialogFragment.Builder dialogFragment =
196                 grantAccess ? createConfirmGrantDialogFragment(label)
197                         : createConfirmRevokeDialogFragment(label);
198         dialogFragment.addArgumentParcelable(KEY_SERVICE, service);
199         getFragmentController().showDialog(dialogFragment.build(),
200                 grantAccess ? GRANT_CONFIRM_DIALOG_TAG : REVOKE_CONFIRM_DIALOG_TAG);
201         return false;
202     }
203 
createConfirmGrantDialogFragment(String label)204     private ConfirmationDialogFragment.Builder createConfirmGrantDialogFragment(String label) {
205         String title = getContext().getResources().getString(
206                 R.string.notification_listener_security_warning_title, label);
207         String summary = getContext().getResources().getString(
208                 R.string.notification_listener_security_warning_summary, label);
209         return new ConfirmationDialogFragment.Builder(getContext())
210                 .setTitle(title)
211                 .setMessage(summary)
212                 .setPositiveButton(R.string.allow, mGrantConfirmListener)
213                 .setNegativeButton(R.string.deny, /* rejectionListener= */ null);
214     }
215 
createConfirmRevokeDialogFragment(String label)216     private ConfirmationDialogFragment.Builder createConfirmRevokeDialogFragment(String label) {
217         String summary = getContext().getResources().getString(
218                 R.string.notification_listener_revoke_warning_summary, label);
219         return new ConfirmationDialogFragment.Builder(getContext())
220                 .setMessage(summary)
221                 .setPositiveButton(R.string.notification_listener_revoke_warning_confirm,
222                         mRevokeConfirmListener)
223                 .setNegativeButton(R.string.notification_listener_revoke_warning_cancel,
224                         /* rejectionListener= */ null);
225     }
226 }
227