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