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 mExecutor.execute(this::updateQRCodeScannerActivityDetails); 124 } 125 126 /** 127 * Add a callback for {@link QRCodeScannerChangeEvent} events 128 */ 129 @Override addCallback(@otNull Callback listener)130 public void addCallback(@NotNull Callback listener) { 131 if (!isCameraAvailable()) return; 132 133 synchronized (mCallbacks) { 134 mCallbacks.add(listener); 135 } 136 } 137 138 /** 139 * Remove callback for {@link QRCodeScannerChangeEvent} events 140 */ 141 @Override removeCallback(@otNull Callback listener)142 public void removeCallback(@NotNull Callback listener) { 143 if (!isCameraAvailable()) return; 144 145 synchronized (mCallbacks) { 146 mCallbacks.remove(listener); 147 } 148 } 149 150 /** 151 * Returns a verified intent to start the QR code scanner activity. 152 * Returns null if the intent is not available 153 */ getIntent()154 public Intent getIntent() { 155 return mIntent; 156 } 157 158 /** 159 * Returns true if lock screen entry point for QR Code Scanner is to be enabled. 160 */ isEnabledForLockScreenButton()161 public boolean isEnabledForLockScreenButton() { 162 return mQRCodeScannerEnabled && isAbleToLaunchScannerActivity() && isAllowedOnLockScreen(); 163 } 164 165 /** Returns whether the QR scanner button is allowed on lockscreen. */ isAllowedOnLockScreen()166 public boolean isAllowedOnLockScreen() { 167 return mConfigEnableLockScreenButton; 168 } 169 170 /** 171 * Returns true if the feature can open the configured QR scanner activity. 172 */ isAbleToLaunchScannerActivity()173 public boolean isAbleToLaunchScannerActivity() { 174 return mIntent != null && isActivityCallable(mIntent); 175 } 176 177 /** 178 * Register the change observers for {@link QRCodeScannerChangeEvent} 179 * 180 * @param events {@link QRCodeScannerChangeEvent} events that need to be handled. 181 */ registerQRCodeScannerChangeObservers( @RCodeScannerChangeEvent int... events)182 public void registerQRCodeScannerChangeObservers( 183 @QRCodeScannerChangeEvent int... events) { 184 if (!isCameraAvailable()) return; 185 186 for (int event : events) { 187 switch (event) { 188 case DEFAULT_QR_CODE_SCANNER_CHANGE: 189 mDefaultQRCodeScannerChangeEvents.incrementAndGet(); 190 registerDefaultQRCodeScannerObserver(); 191 break; 192 case QR_CODE_SCANNER_PREFERENCE_CHANGE: 193 mQRCodeScannerPreferenceChangeEvents.incrementAndGet(); 194 registerQRCodePreferenceObserver(); 195 registerUserChangeObservers(); 196 break; 197 default: 198 Log.e(TAG, "Unrecognised event: " + event); 199 } 200 } 201 } 202 203 /** 204 * Unregister the change observers for {@link QRCodeScannerChangeEvent}. Make sure only to call 205 * this after registerQRCodeScannerChangeObservers 206 * 207 * @param events {@link QRCodeScannerChangeEvent} events that need to be handled. 208 */ unregisterQRCodeScannerChangeObservers( @RCodeScannerChangeEvent int... events)209 public void unregisterQRCodeScannerChangeObservers( 210 @QRCodeScannerChangeEvent int... events) { 211 if (!isCameraAvailable()) return; 212 213 for (int event : events) { 214 switch (event) { 215 case DEFAULT_QR_CODE_SCANNER_CHANGE: 216 if (mOnDefaultQRCodeScannerChangedListener == null) continue; 217 218 if (mDefaultQRCodeScannerChangeEvents.decrementAndGet() == 0) { 219 unregisterDefaultQRCodeScannerObserver(); 220 } 221 break; 222 case QR_CODE_SCANNER_PREFERENCE_CHANGE: 223 if (mUserTracker == null) continue; 224 225 if (mQRCodeScannerPreferenceChangeEvents.decrementAndGet() == 0) { 226 unregisterQRCodePreferenceObserver(); 227 unregisterUserChangeObservers(); 228 } 229 break; 230 default: 231 Log.e(TAG, "Unrecognised event: " + event); 232 } 233 } 234 } 235 236 /** Returns true if camera is available on the device */ isCameraAvailable()237 public boolean isCameraAvailable() { 238 if (mIsCameraAvailable == null) { 239 mIsCameraAvailable = mContext.getPackageManager().hasSystemFeature( 240 PackageManager.FEATURE_CAMERA); 241 } 242 return mIsCameraAvailable; 243 } 244 updateQRCodeScannerPreferenceDetails(boolean updateSettings)245 private void updateQRCodeScannerPreferenceDetails(boolean updateSettings) { 246 if (!mConfigEnableLockScreenButton) { 247 // Settings only apply to lock screen entry point. 248 return; 249 } 250 251 boolean prevQRCodeScannerEnabled = mQRCodeScannerEnabled; 252 mQRCodeScannerEnabled = mSecureSettings.getIntForUser(LOCK_SCREEN_SHOW_QR_CODE_SCANNER, 0, 253 mUserTracker.getUserId()) != 0; 254 if (updateSettings) { 255 mSecureSettings.putStringForUser(Settings.Secure.SHOW_QR_CODE_SCANNER_SETTING, 256 mQRCodeScannerActivity, mUserTracker.getUserId()); 257 } 258 259 if (!Objects.equals(mQRCodeScannerEnabled, prevQRCodeScannerEnabled)) { 260 notifyQRCodeScannerPreferenceChanged(); 261 } 262 } 263 getDefaultScannerActivity()264 private String getDefaultScannerActivity() { 265 return mContext.getResources().getString( 266 com.android.internal.R.string.config_defaultQrCodeComponent); 267 } 268 updateQRCodeScannerActivityDetails()269 private void updateQRCodeScannerActivityDetails() { 270 String qrCodeScannerActivity = mDeviceConfigProxy.getString( 271 DeviceConfig.NAMESPACE_SYSTEMUI, 272 SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER, ""); 273 274 // "" means either the flags is not available or is set to "", and in both the cases we 275 // want to use R.string.config_defaultQrCodeComponent 276 if (Objects.equals(qrCodeScannerActivity, "")) { 277 qrCodeScannerActivity = getDefaultScannerActivity(); 278 } 279 280 String prevQrCodeScannerActivity = mQRCodeScannerActivity; 281 ComponentName componentName = null; 282 Intent intent = new Intent(); 283 if (qrCodeScannerActivity != null) { 284 componentName = ComponentName.unflattenFromString(qrCodeScannerActivity); 285 intent.setComponent(componentName); 286 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); 287 } 288 289 if (isActivityAvailable(intent)) { 290 mQRCodeScannerActivity = qrCodeScannerActivity; 291 mComponentName = componentName; 292 mIntent = intent; 293 } else { 294 mQRCodeScannerActivity = null; 295 mComponentName = null; 296 mIntent = null; 297 } 298 299 if (!Objects.equals(mQRCodeScannerActivity, prevQrCodeScannerActivity)) { 300 notifyQRCodeScannerActivityChanged(); 301 } 302 } 303 isActivityAvailable(Intent intent)304 private boolean isActivityAvailable(Intent intent) { 305 // Our intent should always be explicit and should have a component set 306 if (intent.getComponent() == null) return false; 307 308 int flags = PackageManager.MATCH_DIRECT_BOOT_AWARE 309 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 310 | PackageManager.MATCH_UNINSTALLED_PACKAGES 311 | PackageManager.MATCH_DISABLED_COMPONENTS 312 | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS 313 | PackageManager.MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS; 314 return !mContext.getPackageManager().queryIntentActivities(intent, 315 flags).isEmpty(); 316 } 317 isActivityCallable(Intent intent)318 private boolean isActivityCallable(Intent intent) { 319 // Our intent should always be explicit and should have a component set 320 if (intent.getComponent() == null) return false; 321 322 int flags = PackageManager.MATCH_DIRECT_BOOT_AWARE 323 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 324 | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS; 325 return !mContext.getPackageManager().queryIntentActivities(intent, 326 flags).isEmpty(); 327 } 328 unregisterUserChangeObservers()329 private void unregisterUserChangeObservers() { 330 mUserTracker.removeCallback(mUserChangedListener); 331 332 // Reset cached values to default as we are no longer listening 333 mUserChangedListener = null; 334 mQRCodeScannerEnabled = false; 335 } 336 unregisterQRCodePreferenceObserver()337 private void unregisterQRCodePreferenceObserver() { 338 if (!mConfigEnableLockScreenButton) { 339 // Settings only apply to lock screen entry point. 340 return; 341 } 342 343 mQRCodeScannerPreferenceObserver.forEach((key, value) -> { 344 mSecureSettings.unregisterContentObserverSync(value); 345 }); 346 347 // Reset cached values to default as we are no longer listening 348 mQRCodeScannerPreferenceObserver = new HashMap<>(); 349 mSecureSettings.putStringForUser(Settings.Secure.SHOW_QR_CODE_SCANNER_SETTING, null, 350 mUserTracker.getUserId()); 351 } 352 unregisterDefaultQRCodeScannerObserver()353 private void unregisterDefaultQRCodeScannerObserver() { 354 mDeviceConfigProxy.removeOnPropertiesChangedListener( 355 mOnDefaultQRCodeScannerChangedListener); 356 357 // Reset cached values to default as we are no longer listening 358 mOnDefaultQRCodeScannerChangedListener = null; 359 } 360 notifyQRCodeScannerActivityChanged()361 private void notifyQRCodeScannerActivityChanged() { 362 // Clone and iterate so that we don't block other threads trying to add to mCallbacks 363 ArrayList<Callback> callbacksCopy; 364 synchronized (mCallbacks) { 365 callbacksCopy = (ArrayList) mCallbacks.clone(); 366 } 367 368 callbacksCopy.forEach(c -> c.onQRCodeScannerActivityChanged()); 369 } 370 notifyQRCodeScannerPreferenceChanged()371 private void notifyQRCodeScannerPreferenceChanged() { 372 // Clone and iterate so that we don't block other threads trying to add to mCallbacks 373 ArrayList<Callback> callbacksCopy; 374 synchronized (mCallbacks) { 375 callbacksCopy = (ArrayList) mCallbacks.clone(); 376 } 377 378 callbacksCopy.forEach(c -> c.onQRCodeScannerPreferenceChanged()); 379 } 380 registerDefaultQRCodeScannerObserver()381 private void registerDefaultQRCodeScannerObserver() { 382 if (mOnDefaultQRCodeScannerChangedListener != null) return; 383 384 // While registering the observers for the first time update the default values in the 385 // background 386 mExecutor.execute(() -> updateQRCodeScannerActivityDetails()); 387 mOnDefaultQRCodeScannerChangedListener = 388 properties -> { 389 if (DeviceConfig.NAMESPACE_SYSTEMUI.equals(properties.getNamespace()) 390 && (properties.getKeyset().contains( 391 SystemUiDeviceConfigFlags.DEFAULT_QR_CODE_SCANNER))) { 392 updateQRCodeScannerActivityDetails(); 393 updateQRCodeScannerPreferenceDetails(/* updateSettings = */true); 394 } 395 }; 396 mDeviceConfigProxy.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, 397 mExecutor, mOnDefaultQRCodeScannerChangedListener); 398 } 399 registerQRCodePreferenceObserver()400 private void registerQRCodePreferenceObserver() { 401 if (!mConfigEnableLockScreenButton) { 402 // Settings only apply to lock screen entry point. 403 return; 404 } 405 406 int userId = mUserTracker.getUserId(); 407 if (mQRCodeScannerPreferenceObserver.getOrDefault(userId, null) != null) return; 408 409 // While registering the observers for the first time update the default values in the 410 // background 411 mExecutor.execute( 412 () -> updateQRCodeScannerPreferenceDetails(/* updateSettings = */true)); 413 mQRCodeScannerPreferenceObserver.put(userId, new ContentObserver(null /* handler */) { 414 @Override 415 public void onChange(boolean selfChange) { 416 mExecutor.execute(() -> { 417 updateQRCodeScannerPreferenceDetails(/* updateSettings = */false); 418 }); 419 } 420 }); 421 mSecureSettings.registerContentObserverForUserSync( 422 mSecureSettings.getUriFor(LOCK_SCREEN_SHOW_QR_CODE_SCANNER), false, 423 mQRCodeScannerPreferenceObserver.get(userId), userId); 424 } 425 registerUserChangeObservers()426 private void registerUserChangeObservers() { 427 if (mUserChangedListener != null) return; 428 429 mUserChangedListener = new UserTracker.Callback() { 430 @Override 431 public void onUserChanged(int newUser, Context userContext) { 432 // For the new user, 433 // 1. Enable setting (if qr code scanner activity is available, and if not already 434 // done) 435 // 2. Update the lock screen entry point preference as per the user 436 registerQRCodePreferenceObserver(); 437 updateQRCodeScannerPreferenceDetails(/* updateSettings = */true); 438 } 439 }; 440 mUserTracker.addCallback(mUserChangedListener, mExecutor); 441 } 442 } 443