• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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