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