1 /* 2 * Copyright (C) 2021 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.systemui.qrcodescanner.controller; 18 19 import static android.provider.Settings.Secure.LOCK_SCREEN_SHOW_QR_CODE_SCANNER; 20 21 import android.annotation.IntDef; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.PackageManager; 26 import android.database.ContentObserver; 27 import android.provider.DeviceConfig; 28 import android.provider.Settings; 29 import android.util.Log; 30 31 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; 32 import com.android.systemui.dagger.SysUISingleton; 33 import com.android.systemui.dagger.qualifiers.Background; 34 import com.android.systemui.settings.UserTracker; 35 import com.android.systemui.statusbar.policy.CallbackController; 36 import com.android.systemui.util.DeviceConfigProxy; 37 import com.android.systemui.util.settings.SecureSettings; 38 39 import org.jetbrains.annotations.NotNull; 40 41 import java.lang.annotation.Retention; 42 import java.lang.annotation.RetentionPolicy; 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.Objects; 46 import java.util.concurrent.Executor; 47 import java.util.concurrent.atomic.AtomicInteger; 48 49 import javax.inject.Inject; 50 51 /** 52 * Controller to handle communication between SystemUI and QR Code Scanner provider. 53 * Only listens to the {@link QRCodeScannerChangeEvent} if there is an active observer (i.e. 54 * registerQRCodeScannerChangeObservers 55 * for the required {@link QRCodeScannerChangeEvent} has been called). 56 */ 57 @SysUISingleton 58 public class QRCodeScannerController implements 59 CallbackController<QRCodeScannerController.Callback> { 60 /** 61 * Event for the change in availability and preference of the QR code scanner. 62 */ 63 public interface Callback { 64 /** 65 * Listener implementation for {@link QRCodeScannerChangeEvent} 66 * DEFAULT_QR_CODE_SCANNER_CHANGE 67 */ onQRCodeScannerActivityChanged()68 default void onQRCodeScannerActivityChanged() { 69 } 70 71 /** 72 * Listener implementation for {@link QRCodeScannerChangeEvent} 73 * QR_CODE_SCANNER_PREFERENCE_CHANGE 74 */ onQRCodeScannerPreferenceChanged()75 default void onQRCodeScannerPreferenceChanged() { 76 } 77 } 78 79 @Retention(RetentionPolicy.SOURCE) 80 @IntDef(value = {DEFAULT_QR_CODE_SCANNER_CHANGE, QR_CODE_SCANNER_PREFERENCE_CHANGE}) 81 public @interface QRCodeScannerChangeEvent { 82 } 83 84 public static final int DEFAULT_QR_CODE_SCANNER_CHANGE = 0; 85 public static final int QR_CODE_SCANNER_PREFERENCE_CHANGE = 1; 86 87 private static final String TAG = "QRCodeScannerController"; 88 89 private final Context mContext; 90 private final Executor mExecutor; 91 private final SecureSettings mSecureSettings; 92 private final DeviceConfigProxy mDeviceConfigProxy; 93 private final ArrayList<Callback> mCallbacks = new ArrayList<>(); 94 private final UserTracker mUserTracker; 95 private final boolean mConfigEnableLockScreenButton; 96 97 private HashMap<Integer, ContentObserver> mQRCodeScannerPreferenceObserver = new HashMap<>(); 98 private DeviceConfig.OnPropertiesChangedListener mOnDefaultQRCodeScannerChangedListener = null; 99 private UserTracker.Callback mUserChangedListener = null; 100 101 private boolean mQRCodeScannerEnabled; 102 private Intent mIntent = null; 103 private String mQRCodeScannerActivity = null; 104 private ComponentName mComponentName = null; 105 private AtomicInteger mQRCodeScannerPreferenceChangeEvents = new AtomicInteger(0); 106 private AtomicInteger mDefaultQRCodeScannerChangeEvents = new AtomicInteger(0); 107 private Boolean mIsCameraAvailable = null; 108 109 @Inject QRCodeScannerController( Context context, @Background Executor executor, SecureSettings secureSettings, DeviceConfigProxy proxy, UserTracker userTracker)110 public QRCodeScannerController( 111 Context context, 112 @Background Executor executor, 113 SecureSettings secureSettings, 114 DeviceConfigProxy proxy, 115 UserTracker userTracker) { 116 mContext = context; 117 mExecutor = executor; 118 mSecureSettings = secureSettings; 119 mDeviceConfigProxy = proxy; 120 mUserTracker = userTracker; 121 mConfigEnableLockScreenButton = mContext.getResources().getBoolean( 122 android.R.bool.config_enableQrCodeScannerOnLockScreen); 123 } 124 125 /** 126 * Add a callback for {@link QRCodeScannerChangeEvent} events 127 */ 128 @Override addCallback(@otNull Callback listener)129 public void addCallback(@NotNull Callback listener) { 130 if (!isCameraAvailable()) return; 131 132 synchronized (mCallbacks) { 133 mCallbacks.add(listener); 134 } 135 } 136 137 /** 138 * Remove callback for {@link QRCodeScannerChangeEvent} events 139 */ 140 @Override removeCallback(@otNull Callback listener)141 public void removeCallback(@NotNull Callback listener) { 142 if (!isCameraAvailable()) return; 143 144 synchronized (mCallbacks) { 145 mCallbacks.remove(listener); 146 } 147 } 148 149 /** 150 * Returns a verified intent to start the QR code scanner activity. 151 * Returns null if the intent is not available 152 */ getIntent()153 public Intent getIntent() { 154 return mIntent; 155 } 156 157 /** 158 * Returns true if lock screen entry point for QR Code Scanner is to be enabled. 159 */ isEnabledForLockScreenButton()160 public boolean isEnabledForLockScreenButton() { 161 return mQRCodeScannerEnabled && isAbleToOpenCameraApp() && isAvailableOnDevice(); 162 } 163 164 /** Returns whether the feature is available on the device. */ isAvailableOnDevice()165 public boolean isAvailableOnDevice() { 166 return mConfigEnableLockScreenButton; 167 } 168 169 /** 170 * Returns true if the feature can open a camera app on the device. 171 */ isAbleToOpenCameraApp()172 public boolean isAbleToOpenCameraApp() { 173 return mIntent != null && isActivityCallable(mIntent); 174 } 175 176 /** 177 * Register the change observers for {@link QRCodeScannerChangeEvent} 178 * 179 * @param events {@link QRCodeScannerChangeEvent} events that need to be handled. 180 */ registerQRCodeScannerChangeObservers( @RCodeScannerChangeEvent int... events)181 public void registerQRCodeScannerChangeObservers( 182 @QRCodeScannerChangeEvent int... events) { 183 if (!isCameraAvailable()) return; 184 185 for (int event : events) { 186 switch (event) { 187 case DEFAULT_QR_CODE_SCANNER_CHANGE: 188 mDefaultQRCodeScannerChangeEvents.incrementAndGet(); 189 registerDefaultQRCodeScannerObserver(); 190 break; 191 case QR_CODE_SCANNER_PREFERENCE_CHANGE: 192 mQRCodeScannerPreferenceChangeEvents.incrementAndGet(); 193 registerQRCodePreferenceObserver(); 194 registerUserChangeObservers(); 195 break; 196 default: 197 Log.e(TAG, "Unrecognised event: " + event); 198 } 199 } 200 } 201 202 /** 203 * Unregister the change observers for {@link QRCodeScannerChangeEvent}. Make sure only to call 204 * this after registerQRCodeScannerChangeObservers 205 * 206 * @param events {@link QRCodeScannerChangeEvent} events that need to be handled. 207 */ unregisterQRCodeScannerChangeObservers( @RCodeScannerChangeEvent int... events)208 public void unregisterQRCodeScannerChangeObservers( 209 @QRCodeScannerChangeEvent int... events) { 210 if (!isCameraAvailable()) return; 211 212 for (int event : events) { 213 switch (event) { 214 case DEFAULT_QR_CODE_SCANNER_CHANGE: 215 if (mOnDefaultQRCodeScannerChangedListener == null) continue; 216 217 if (mDefaultQRCodeScannerChangeEvents.decrementAndGet() == 0) { 218 unregisterDefaultQRCodeScannerObserver(); 219 } 220 break; 221 case QR_CODE_SCANNER_PREFERENCE_CHANGE: 222 if (mUserTracker == null) continue; 223 224 if (mQRCodeScannerPreferenceChangeEvents.decrementAndGet() == 0) { 225 unregisterQRCodePreferenceObserver(); 226 unregisterUserChangeObservers(); 227 } 228 break; 229 default: 230 Log.e(TAG, "Unrecognised event: " + event); 231 } 232 } 233 } 234 235 /** Returns true if camera is available on the device */ isCameraAvailable()236 public boolean isCameraAvailable() { 237 if (mIsCameraAvailable == null) { 238 mIsCameraAvailable = mContext.getPackageManager().hasSystemFeature( 239 PackageManager.FEATURE_CAMERA); 240 } 241 return mIsCameraAvailable; 242 } 243 updateQRCodeScannerPreferenceDetails(boolean updateSettings)244 private void updateQRCodeScannerPreferenceDetails(boolean updateSettings) { 245 if (!mConfigEnableLockScreenButton) { 246 // Settings only apply to lock screen entry point. 247 return; 248 } 249 250 boolean prevQRCodeScannerEnabled = mQRCodeScannerEnabled; 251 mQRCodeScannerEnabled = mSecureSettings.getIntForUser(LOCK_SCREEN_SHOW_QR_CODE_SCANNER, 0, 252 mUserTracker.getUserId()) != 0; 253 if (updateSettings) { 254 mSecureSettings.putStringForUser(Settings.Secure.SHOW_QR_CODE_SCANNER_SETTING, 255 mQRCodeScannerActivity, mUserTracker.getUserId()); 256 } 257 258 if (!Objects.equals(mQRCodeScannerEnabled, prevQRCodeScannerEnabled)) { 259 notifyQRCodeScannerPreferenceChanged(); 260 } 261 } 262 getDefaultScannerActivity()263 private String getDefaultScannerActivity() { 264 return mContext.getResources().getString( 265 com.android.internal.R.string.config_defaultQrCodeComponent); 266 } 267 updateQRCodeScannerActivityDetails()268 private void updateQRCodeScannerActivityDetails() { 269 String qrCodeScannerActivity = mDeviceConfigProxy.getString( 270 DeviceConfig.NAMESPACE_SYSTEMUI, 271 SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER, ""); 272 273 // "" means either the flags is not available or is set to "", and in both the cases we 274 // want to use R.string.config_defaultQrCodeComponent 275 if (Objects.equals(qrCodeScannerActivity, "")) { 276 qrCodeScannerActivity = getDefaultScannerActivity(); 277 } 278 279 String prevQrCodeScannerActivity = mQRCodeScannerActivity; 280 ComponentName componentName = null; 281 Intent intent = new Intent(); 282 if (qrCodeScannerActivity != null) { 283 componentName = ComponentName.unflattenFromString(qrCodeScannerActivity); 284 intent.setComponent(componentName); 285 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); 286 } 287 288 if (isActivityAvailable(intent)) { 289 mQRCodeScannerActivity = qrCodeScannerActivity; 290 mComponentName = componentName; 291 mIntent = intent; 292 } else { 293 mQRCodeScannerActivity = null; 294 mComponentName = null; 295 mIntent = null; 296 } 297 298 if (!Objects.equals(mQRCodeScannerActivity, prevQrCodeScannerActivity)) { 299 notifyQRCodeScannerActivityChanged(); 300 } 301 } 302 isActivityAvailable(Intent intent)303 private boolean isActivityAvailable(Intent intent) { 304 // Our intent should always be explicit and should have a component set 305 if (intent.getComponent() == null) return false; 306 307 int flags = PackageManager.MATCH_DIRECT_BOOT_AWARE 308 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 309 | PackageManager.MATCH_UNINSTALLED_PACKAGES 310 | PackageManager.MATCH_DISABLED_COMPONENTS 311 | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS 312 | PackageManager.MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS; 313 return !mContext.getPackageManager().queryIntentActivities(intent, 314 flags).isEmpty(); 315 } 316 isActivityCallable(Intent intent)317 private boolean isActivityCallable(Intent intent) { 318 // Our intent should always be explicit and should have a component set 319 if (intent.getComponent() == null) return false; 320 321 int flags = PackageManager.MATCH_DIRECT_BOOT_AWARE 322 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 323 | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; 324 return !mContext.getPackageManager().queryIntentActivities(intent, 325 flags).isEmpty(); 326 } 327 unregisterUserChangeObservers()328 private void unregisterUserChangeObservers() { 329 mUserTracker.removeCallback(mUserChangedListener); 330 331 // Reset cached values to default as we are no longer listening 332 mUserChangedListener = null; 333 mQRCodeScannerEnabled = false; 334 } 335 unregisterQRCodePreferenceObserver()336 private void unregisterQRCodePreferenceObserver() { 337 if (!mConfigEnableLockScreenButton) { 338 // Settings only apply to lock screen entry point. 339 return; 340 } 341 342 mQRCodeScannerPreferenceObserver.forEach((key, value) -> { 343 mSecureSettings.unregisterContentObserver(value); 344 }); 345 346 // Reset cached values to default as we are no longer listening 347 mQRCodeScannerPreferenceObserver = new HashMap<>(); 348 mSecureSettings.putStringForUser(Settings.Secure.SHOW_QR_CODE_SCANNER_SETTING, null, 349 mUserTracker.getUserId()); 350 } 351 unregisterDefaultQRCodeScannerObserver()352 private void unregisterDefaultQRCodeScannerObserver() { 353 mDeviceConfigProxy.removeOnPropertiesChangedListener( 354 mOnDefaultQRCodeScannerChangedListener); 355 356 // Reset cached values to default as we are no longer listening 357 mOnDefaultQRCodeScannerChangedListener = null; 358 mQRCodeScannerActivity = null; 359 mIntent = null; 360 mComponentName = null; 361 } 362 notifyQRCodeScannerActivityChanged()363 private void notifyQRCodeScannerActivityChanged() { 364 // Clone and iterate so that we don't block other threads trying to add to mCallbacks 365 ArrayList<Callback> callbacksCopy; 366 synchronized (mCallbacks) { 367 callbacksCopy = (ArrayList) mCallbacks.clone(); 368 } 369 370 callbacksCopy.forEach(c -> c.onQRCodeScannerActivityChanged()); 371 } 372 notifyQRCodeScannerPreferenceChanged()373 private void notifyQRCodeScannerPreferenceChanged() { 374 // Clone and iterate so that we don't block other threads trying to add to mCallbacks 375 ArrayList<Callback> callbacksCopy; 376 synchronized (mCallbacks) { 377 callbacksCopy = (ArrayList) mCallbacks.clone(); 378 } 379 380 callbacksCopy.forEach(c -> c.onQRCodeScannerPreferenceChanged()); 381 } 382 registerDefaultQRCodeScannerObserver()383 private void registerDefaultQRCodeScannerObserver() { 384 if (mOnDefaultQRCodeScannerChangedListener != null) return; 385 386 // While registering the observers for the first time update the default values in the 387 // background 388 mExecutor.execute(() -> updateQRCodeScannerActivityDetails()); 389 mOnDefaultQRCodeScannerChangedListener = 390 properties -> { 391 if (DeviceConfig.NAMESPACE_SYSTEMUI.equals(properties.getNamespace()) 392 && (properties.getKeyset().contains( 393 SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER))) { 394 updateQRCodeScannerActivityDetails(); 395 updateQRCodeScannerPreferenceDetails(/* updateSettings = */true); 396 } 397 }; 398 mDeviceConfigProxy.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, 399 mExecutor, mOnDefaultQRCodeScannerChangedListener); 400 } 401 registerQRCodePreferenceObserver()402 private void registerQRCodePreferenceObserver() { 403 if (!mConfigEnableLockScreenButton) { 404 // Settings only apply to lock screen entry point. 405 return; 406 } 407 408 int userId = mUserTracker.getUserId(); 409 if (mQRCodeScannerPreferenceObserver.getOrDefault(userId, null) != null) return; 410 411 // While registering the observers for the first time update the default values in the 412 // background 413 mExecutor.execute( 414 () -> updateQRCodeScannerPreferenceDetails(/* updateSettings = */true)); 415 mQRCodeScannerPreferenceObserver.put(userId, new ContentObserver(null /* handler */) { 416 @Override 417 public void onChange(boolean selfChange) { 418 mExecutor.execute(() -> { 419 updateQRCodeScannerPreferenceDetails(/* updateSettings = */false); 420 }); 421 } 422 }); 423 mSecureSettings.registerContentObserverForUser( 424 mSecureSettings.getUriFor(LOCK_SCREEN_SHOW_QR_CODE_SCANNER), false, 425 mQRCodeScannerPreferenceObserver.get(userId), userId); 426 } 427 registerUserChangeObservers()428 private void registerUserChangeObservers() { 429 if (mUserChangedListener != null) return; 430 431 mUserChangedListener = new UserTracker.Callback() { 432 @Override 433 public void onUserChanged(int newUser, Context userContext) { 434 // For the new user, 435 // 1. Enable setting (if qr code scanner activity is available, and if not already 436 // done) 437 // 2. Update the lock screen entry point preference as per the user 438 registerQRCodePreferenceObserver(); 439 updateQRCodeScannerPreferenceDetails(/* updateSettings = */true); 440 } 441 }; 442 mUserTracker.addCallback(mUserChangedListener, mExecutor); 443 } 444 } 445