1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.systemui.statusbar.phone;
16 
17 import static com.android.systemui.qs.dagger.QSFlagsModule.RBC_AVAILABLE;
18 
19 import android.annotation.Nullable;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.database.ContentObserver;
24 import android.hardware.display.ColorDisplayManager;
25 import android.hardware.display.NightDisplayListener;
26 import android.os.Handler;
27 import android.os.UserHandle;
28 import android.util.Log;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.systemui.R;
32 import com.android.systemui.dagger.NightDisplayListenerModule;
33 import com.android.systemui.dagger.qualifiers.Background;
34 import com.android.systemui.plugins.qs.QSTile;
35 import com.android.systemui.qs.AutoAddTracker;
36 import com.android.systemui.qs.QSHost;
37 import com.android.systemui.qs.ReduceBrightColorsController;
38 import com.android.systemui.qs.SettingObserver;
39 import com.android.systemui.qs.external.CustomTile;
40 import com.android.systemui.statusbar.policy.CastController;
41 import com.android.systemui.statusbar.policy.CastController.CastDevice;
42 import com.android.systemui.statusbar.policy.DataSaverController;
43 import com.android.systemui.statusbar.policy.DataSaverController.Listener;
44 import com.android.systemui.statusbar.policy.DeviceControlsController;
45 import com.android.systemui.statusbar.policy.HotspotController;
46 import com.android.systemui.statusbar.policy.HotspotController.Callback;
47 import com.android.systemui.statusbar.policy.SafetyController;
48 import com.android.systemui.statusbar.policy.WalletController;
49 import com.android.systemui.util.UserAwareController;
50 import com.android.systemui.util.settings.SecureSettings;
51 
52 import java.util.ArrayList;
53 import java.util.Collection;
54 import java.util.Objects;
55 
56 import javax.inject.Named;
57 
58 /**
59  * Manages which tiles should be automatically added to QS.
60  */
61 public class AutoTileManager implements UserAwareController {
62     private static final String TAG = "AutoTileManager";
63 
64     public static final String HOTSPOT = "hotspot";
65     public static final String SAVER = "saver";
66     public static final String INVERSION = "inversion";
67     public static final String WORK = "work";
68     public static final String NIGHT = "night";
69     public static final String CAST = "cast";
70     public static final String DEVICE_CONTROLS = "controls";
71     public static final String WALLET = "wallet";
72     public static final String BRIGHTNESS = "reduce_brightness";
73     static final String SETTING_SEPARATOR = ":";
74 
75     private UserHandle mCurrentUser;
76     private boolean mInitialized;
77     private final String mSafetySpec;
78 
79     protected final Context mContext;
80     protected final QSHost mHost;
81     protected final Handler mHandler;
82     protected final SecureSettings mSecureSettings;
83     protected final AutoAddTracker mAutoTracker;
84     private final HotspotController mHotspotController;
85     private final DataSaverController mDataSaverController;
86     private final ManagedProfileController mManagedProfileController;
87     private final NightDisplayListenerModule.Builder mNightDisplayListenerBuilder;
88     private NightDisplayListener mNightDisplayListener;
89     private final CastController mCastController;
90     private final DeviceControlsController mDeviceControlsController;
91     private final WalletController mWalletController;
92     private final ReduceBrightColorsController mReduceBrightColorsController;
93     private final SafetyController mSafetyController;
94     private final boolean mIsReduceBrightColorsAvailable;
95     private final ArrayList<AutoAddSetting> mAutoAddSettingList = new ArrayList<>();
96 
AutoTileManager(Context context, AutoAddTracker.Builder autoAddTrackerBuilder, QSHost host, @Background Handler handler, SecureSettings secureSettings, HotspotController hotspotController, DataSaverController dataSaverController, ManagedProfileController managedProfileController, NightDisplayListenerModule.Builder nightDisplayListenerBuilder, CastController castController, ReduceBrightColorsController reduceBrightColorsController, DeviceControlsController deviceControlsController, WalletController walletController, SafetyController safetyController, @Named(RBC_AVAILABLE) boolean isReduceBrightColorsAvailable)97     public AutoTileManager(Context context, AutoAddTracker.Builder autoAddTrackerBuilder,
98             QSHost host,
99             @Background Handler handler,
100             SecureSettings secureSettings,
101             HotspotController hotspotController,
102             DataSaverController dataSaverController,
103             ManagedProfileController managedProfileController,
104             NightDisplayListenerModule.Builder nightDisplayListenerBuilder,
105             CastController castController,
106             ReduceBrightColorsController reduceBrightColorsController,
107             DeviceControlsController deviceControlsController,
108             WalletController walletController,
109             SafetyController safetyController,
110             @Named(RBC_AVAILABLE) boolean isReduceBrightColorsAvailable) {
111         mContext = context;
112         mHost = host;
113         mSecureSettings = secureSettings;
114         mCurrentUser = mHost.getUserContext().getUser();
115         mAutoTracker = autoAddTrackerBuilder.setUserId(mCurrentUser.getIdentifier()).build();
116         mHandler = handler;
117         mHotspotController = hotspotController;
118         mDataSaverController = dataSaverController;
119         mManagedProfileController = managedProfileController;
120         mNightDisplayListenerBuilder = nightDisplayListenerBuilder;
121         mCastController = castController;
122         mReduceBrightColorsController = reduceBrightColorsController;
123         mIsReduceBrightColorsAvailable = isReduceBrightColorsAvailable;
124         mDeviceControlsController = deviceControlsController;
125         mWalletController = walletController;
126         mSafetyController = safetyController;
127         String safetySpecClass;
128         try {
129             safetySpecClass =
130                     context.getResources().getString(R.string.safety_quick_settings_tile_class);
131             if (safetySpecClass.length() == 0) {
132                 safetySpecClass = null;
133             }
134         } catch (Resources.NotFoundException | NullPointerException e) {
135             safetySpecClass = null;
136         }
137         mSafetySpec = safetySpecClass != null ? CustomTile.toSpec(new ComponentName(mContext
138                 .getPackageManager().getPermissionControllerPackageName(), safetySpecClass)) : null;
139     }
140 
141     /**
142      * Init method must be called after construction to start listening
143      */
init()144     public void init() {
145         if (mInitialized) {
146             Log.w(TAG, "Trying to re-initialize");
147             return;
148         }
149         mAutoTracker.initialize();
150         populateSettingsList();
151         startControllersAndSettingsListeners();
152         mInitialized = true;
153     }
154 
startControllersAndSettingsListeners()155     protected void startControllersAndSettingsListeners() {
156         if (!mAutoTracker.isAdded(HOTSPOT)) {
157             mHotspotController.addCallback(mHotspotCallback);
158         }
159         if (!mAutoTracker.isAdded(SAVER)) {
160             mDataSaverController.addCallback(mDataSaverListener);
161         }
162         mManagedProfileController.addCallback(mProfileCallback);
163 
164         mNightDisplayListener = mNightDisplayListenerBuilder
165                 .setUser(mCurrentUser.getIdentifier())
166                 .build();
167         if (!mAutoTracker.isAdded(NIGHT)
168                 && ColorDisplayManager.isNightDisplayAvailable(mContext)) {
169             mNightDisplayListener.setCallback(mNightDisplayCallback);
170         }
171         if (!mAutoTracker.isAdded(CAST)) {
172             mCastController.addCallback(mCastCallback);
173         }
174         if (!mAutoTracker.isAdded(BRIGHTNESS) && mIsReduceBrightColorsAvailable) {
175             mReduceBrightColorsController.addCallback(mReduceBrightColorsCallback);
176         }
177         // We always want this callback, because if the feature stops being supported,
178         // we want to remove the tile from AutoAddTracker. That way it will be re-added when the
179         // feature is reenabled (similar to work tile).
180         mDeviceControlsController.setCallback(mDeviceControlsCallback);
181         if (!mAutoTracker.isAdded(WALLET)) {
182             initWalletController();
183         }
184         if (mSafetySpec != null) {
185             if (!mAutoTracker.isAdded(mSafetySpec)) {
186                 initSafetyTile();
187             }
188             mSafetyController.addCallback(mSafetyCallback);
189         }
190 
191         int settingsN = mAutoAddSettingList.size();
192         for (int i = 0; i < settingsN; i++) {
193             if (!mAutoTracker.isAdded(mAutoAddSettingList.get(i).mSpec)) {
194                 mAutoAddSettingList.get(i).setListening(true);
195             }
196         }
197     }
198 
stopListening()199     protected void stopListening() {
200         mHotspotController.removeCallback(mHotspotCallback);
201         mDataSaverController.removeCallback(mDataSaverListener);
202         mManagedProfileController.removeCallback(mProfileCallback);
203         if (ColorDisplayManager.isNightDisplayAvailable(mContext)
204                 && mNightDisplayListener != null) {
205             mNightDisplayListener.setCallback(null);
206         }
207         if (mIsReduceBrightColorsAvailable) {
208             mReduceBrightColorsController.removeCallback(mReduceBrightColorsCallback);
209         }
210         mCastController.removeCallback(mCastCallback);
211         mDeviceControlsController.removeCallback();
212         if (mSafetySpec != null) {
213             mSafetyController.removeCallback(mSafetyCallback);
214         }
215         int settingsN = mAutoAddSettingList.size();
216         for (int i = 0; i < settingsN; i++) {
217             mAutoAddSettingList.get(i).setListening(false);
218         }
219     }
220 
destroy()221     public void destroy() {
222         stopListening();
223         mAutoTracker.destroy();
224     }
225 
226     /**
227      * Populates a list with the pairs setting:spec in the config resource.
228      * <p>
229      * This will only create {@link AutoAddSetting} objects for those tiles that have not been
230      * auto-added before, and set the corresponding {@link ContentObserver} to listening.
231      */
populateSettingsList()232     private void populateSettingsList() {
233         String [] autoAddList;
234         try {
235             autoAddList = mContext.getResources().getStringArray(
236                     R.array.config_quickSettingsAutoAdd);
237         } catch (Resources.NotFoundException e) {
238             Log.w(TAG, "Missing config resource");
239             return;
240         }
241         // getStringArray returns @NotNull, so if we got here, autoAddList is not null
242         for (String tile : autoAddList) {
243             String[] split = tile.split(SETTING_SEPARATOR);
244             if (split.length == 2) {
245                 String setting = split[0];
246                 String spec = split[1];
247                 // Populate all the settings. As they may not have been added in other users
248                 AutoAddSetting s = new AutoAddSetting(
249                         mSecureSettings, mHandler, setting, mCurrentUser.getIdentifier(), spec);
250                 mAutoAddSettingList.add(s);
251             } else {
252                 Log.w(TAG, "Malformed item in array: " + tile);
253             }
254         }
255     }
256 
257     /*
258      * This will be sent off the main thread if needed
259      */
260     @Override
changeUser(UserHandle newUser)261     public void changeUser(UserHandle newUser) {
262         if (!mInitialized) {
263             throw new IllegalStateException("AutoTileManager not initialized");
264         }
265         if (!Thread.currentThread().equals(mHandler.getLooper().getThread())) {
266             mHandler.post(() -> changeUser(newUser));
267             return;
268         }
269         if (newUser.getIdentifier() == mCurrentUser.getIdentifier()) {
270             return;
271         }
272         stopListening();
273         mCurrentUser = newUser;
274         int settingsN = mAutoAddSettingList.size();
275         for (int i = 0; i < settingsN; i++) {
276             mAutoAddSettingList.get(i).setUserId(newUser.getIdentifier());
277         }
278         mAutoTracker.changeUser(newUser);
279         startControllersAndSettingsListeners();
280     }
281 
282     @Override
getCurrentUserId()283     public int getCurrentUserId() {
284         return mCurrentUser.getIdentifier();
285     }
286 
287     private final ManagedProfileController.Callback mProfileCallback =
288             new ManagedProfileController.Callback() {
289                 @Override
290                 public void onManagedProfileChanged() {
291                     if (mManagedProfileController.hasActiveProfile()) {
292                         if (mAutoTracker.isAdded(WORK)) return;
293                         final int position = mAutoTracker.getRestoredTilePosition(WORK);
294                         mHost.addTile(WORK, position);
295                         mAutoTracker.setTileAdded(WORK);
296                     } else {
297                         if (!mAutoTracker.isAdded(WORK)) return;
298                         mHost.removeTile(WORK);
299                         mAutoTracker.setTileRemoved(WORK);
300                     }
301                 }
302 
303                 @Override
304                 public void onManagedProfileRemoved() {
305                 }
306             };
307 
308     private final DataSaverController.Listener mDataSaverListener = new Listener() {
309         @Override
310         public void onDataSaverChanged(boolean isDataSaving) {
311             if (mAutoTracker.isAdded(SAVER)) return;
312             if (isDataSaving) {
313                 mHost.addTile(SAVER);
314                 mAutoTracker.setTileAdded(SAVER);
315                 mHandler.post(() -> mDataSaverController.removeCallback(mDataSaverListener));
316             }
317         }
318     };
319 
320     private final HotspotController.Callback mHotspotCallback = new Callback() {
321         @Override
322         public void onHotspotChanged(boolean enabled, int numDevices) {
323             if (mAutoTracker.isAdded(HOTSPOT)) return;
324             if (enabled) {
325                 mHost.addTile(HOTSPOT);
326                 mAutoTracker.setTileAdded(HOTSPOT);
327                 mHandler.post(() -> mHotspotController.removeCallback(mHotspotCallback));
328             }
329         }
330     };
331 
332     private final DeviceControlsController.Callback mDeviceControlsCallback =
333             new DeviceControlsController.Callback() {
334         @Override
335         public void onControlsUpdate(@Nullable Integer position) {
336             if (mAutoTracker.isAdded(DEVICE_CONTROLS)) return;
337             if (position != null && !hasTile(DEVICE_CONTROLS)) {
338                 mHost.addTile(DEVICE_CONTROLS, position);
339                 mAutoTracker.setTileAdded(DEVICE_CONTROLS);
340             }
341             mHandler.post(() -> mDeviceControlsController.removeCallback());
342         }
343 
344         @Override
345         public void removeControlsAutoTracker() {
346             mAutoTracker.setTileRemoved(DEVICE_CONTROLS);
347         }
348     };
349 
hasTile(String tileSpec)350     private boolean hasTile(String tileSpec) {
351         if (tileSpec == null) return false;
352         Collection<QSTile> tiles = mHost.getTiles();
353         for (QSTile tile : tiles) {
354             if (tileSpec.equals(tile.getTileSpec())) {
355                 return true;
356             }
357         }
358         return false;
359     }
360 
initWalletController()361     private void initWalletController() {
362         if (mAutoTracker.isAdded(WALLET)) return;
363         Integer position = mWalletController.getWalletPosition();
364 
365         if (position != null) {
366             mHost.addTile(WALLET, position);
367             mAutoTracker.setTileAdded(WALLET);
368         }
369     }
370 
initSafetyTile()371     private void initSafetyTile() {
372         if (mSafetySpec == null || mAutoTracker.isAdded(mSafetySpec)) {
373             return;
374         }
375         mHost.addTile(CustomTile.getComponentFromSpec(mSafetySpec), true);
376         mAutoTracker.setTileAdded(mSafetySpec);
377     }
378 
379     @VisibleForTesting
380     final NightDisplayListener.Callback mNightDisplayCallback =
381             new NightDisplayListener.Callback() {
382         @Override
383         public void onActivated(boolean activated) {
384             if (activated) {
385                 addNightTile();
386             }
387         }
388 
389         @Override
390         public void onAutoModeChanged(int autoMode) {
391             if (autoMode == ColorDisplayManager.AUTO_MODE_CUSTOM_TIME
392                     || autoMode == ColorDisplayManager.AUTO_MODE_TWILIGHT) {
393                 addNightTile();
394             }
395         }
396 
397         private void addNightTile() {
398             if (mAutoTracker.isAdded(NIGHT)) return;
399             mHost.addTile(NIGHT);
400             mAutoTracker.setTileAdded(NIGHT);
401             mHandler.post(() -> mNightDisplayListener.setCallback(null));
402         }
403     };
404 
405     @VisibleForTesting
406     final ReduceBrightColorsController.Listener mReduceBrightColorsCallback =
407             new ReduceBrightColorsController.Listener() {
408                 @Override
409                 public void onActivated(boolean activated) {
410                     if (activated) {
411                         addReduceBrightColorsTile();
412                     }
413                 }
414 
415                 private void addReduceBrightColorsTile() {
416                     if (mAutoTracker.isAdded(BRIGHTNESS)) return;
417                     mHost.addTile(BRIGHTNESS);
418                     mAutoTracker.setTileAdded(BRIGHTNESS);
419                     mHandler.post(() -> mReduceBrightColorsController.removeCallback(this));
420                 }
421             };
422 
423     @VisibleForTesting
424     final CastController.Callback mCastCallback = new CastController.Callback() {
425         @Override
426         public void onCastDevicesChanged() {
427             if (mAutoTracker.isAdded(CAST)) return;
428 
429             boolean isCasting = false;
430             for (CastDevice device : mCastController.getCastDevices()) {
431                 if (device.state == CastDevice.STATE_CONNECTED
432                         || device.state == CastDevice.STATE_CONNECTING) {
433                     isCasting = true;
434                     break;
435                 }
436             }
437 
438             if (isCasting) {
439                 mHost.addTile(CAST);
440                 mAutoTracker.setTileAdded(CAST);
441                 mHandler.post(() -> mCastController.removeCallback(mCastCallback));
442             }
443         }
444     };
445 
446     @VisibleForTesting
447     final SafetyController.Listener mSafetyCallback = new SafetyController.Listener() {
448         @Override
449         public void onSafetyCenterEnableChanged(boolean isSafetyCenterEnabled) {
450             if (mSafetySpec == null) {
451                 return;
452             }
453 
454             if (isSafetyCenterEnabled && !mAutoTracker.isAdded(mSafetySpec)) {
455                 initSafetyTile();
456             } else if (!isSafetyCenterEnabled && mAutoTracker.isAdded(mSafetySpec)) {
457                 mHost.removeTile(mSafetySpec);
458                 mAutoTracker.setTileRemoved(mSafetySpec);
459             }
460         }
461     };
462 
463     @VisibleForTesting
getSecureSettingForKey(String key)464     protected SettingObserver getSecureSettingForKey(String key) {
465         for (SettingObserver s : mAutoAddSettingList) {
466             if (Objects.equals(key, s.getKey())) {
467                 return s;
468             }
469         }
470         return null;
471     }
472 
473     /**
474      * Tracks tiles that should be auto added when a setting changes.
475      * <p>
476      * When the setting changes to a value different from 0, if the tile has not been auto added
477      * before, it will be added and the listener will be stopped.
478      */
479     private class AutoAddSetting extends SettingObserver {
480         private final String mSpec;
481 
AutoAddSetting( SecureSettings secureSettings, Handler handler, String setting, int userId, String tileSpec )482         AutoAddSetting(
483                 SecureSettings secureSettings,
484                 Handler handler,
485                 String setting,
486                 int userId,
487                 String tileSpec
488         ) {
489             super(secureSettings, handler, setting, userId);
490             mSpec = tileSpec;
491         }
492 
493         @Override
handleValueChanged(int value, boolean observedChange)494         protected void handleValueChanged(int value, boolean observedChange) {
495             if (mAutoTracker.isAdded(mSpec)) {
496                 // This should not be listening anymore
497                 mHandler.post(() -> setListening(false));
498                 return;
499             }
500             if (value != 0) {
501                 if (mSpec.startsWith(CustomTile.PREFIX)) {
502                     mHost.addTile(CustomTile.getComponentFromSpec(mSpec), /* end */ true);
503                 } else {
504                     mHost.addTile(mSpec);
505                 }
506                 mAutoTracker.setTileAdded(mSpec);
507                 mHandler.post(() -> setListening(false));
508             }
509         }
510     }
511 }
512