• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.statusbar.policy
18 
19 import android.content.ComponentName
20 import android.content.Context
21 import android.content.SharedPreferences
22 import android.provider.Settings
23 import android.util.Log
24 import com.android.systemui.controls.ControlsServiceInfo
25 import com.android.systemui.controls.dagger.ControlsComponent
26 import com.android.systemui.controls.management.ControlsListingController
27 import com.android.systemui.dagger.SysUISingleton
28 import com.android.systemui.res.R
29 import com.android.systemui.settings.UserContextProvider
30 import com.android.systemui.statusbar.policy.DeviceControlsController.Callback
31 import com.android.systemui.util.settings.SecureSettings
32 import javax.inject.Inject
33 
34 /**
35  * Watches for Device Controls QS Tile activation, which can happen in two ways:
36  * <ol>
37  * <li>Migration from Power Menu - For existing Android 11 users, create a tile in a high priority
38  *   position.
39  * <li>Device controls service becomes available - For non-migrated users, create a tile and place
40  *   at the end of active tiles, and initiate seeding where possible.
41  * </ol>
42  */
43 @SysUISingleton
44 public class DeviceControlsControllerImpl
45 @Inject
46 constructor(
47     private val context: Context,
48     private val controlsComponent: ControlsComponent,
49     private val userContextProvider: UserContextProvider,
50     private val secureSettings: SecureSettings
51 ) : DeviceControlsController {
52 
53     private var callback: Callback? = null
54     internal var position: Int? = null
55 
56     private val listingCallback =
57         object : ControlsListingController.ControlsListingCallback {
58             override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
59                 if (!serviceInfos.isEmpty()) {
60                     seedFavorites(serviceInfos)
61                 }
62             }
63         }
64 
65     companion object {
66         private const val TAG = "DeviceControlsControllerImpl"
67         internal const val QS_PRIORITY_POSITION = 3
68         internal const val QS_DEFAULT_POSITION = 7
69 
70         internal const val PREFS_CONTROLS_SEEDING_COMPLETED = "SeedingCompleted"
71         const val PREFS_CONTROLS_FILE = "controls_prefs"
72         private const val SEEDING_MAX = 2
73     }
74 
75     private fun checkMigrationToQs() {
76         controlsComponent.getControlsController().ifPresent {
77             if (!it.getFavorites().isEmpty()) {
78                 position = QS_PRIORITY_POSITION
79                 fireControlsUpdate()
80             }
81         }
82     }
83 
84     /**
85      * This migration logic assumes that something like [AutoAddTracker] is tracking state
86      * externally, and won't call this method after receiving a response via
87      * [Callback#onControlsUpdate], once per user. Otherwise the calculated position may be
88      * incorrect.
89      */
90     override fun setCallback(callback: Callback) {
91         if (!controlsComponent.isEnabled()) {
92             callback.removeControlsAutoTracker()
93             return
94         }
95         // Treat any additional call as a reset before recalculating
96         removeCallback()
97         this.callback = callback
98 
99         if (secureSettings.getInt(Settings.Secure.CONTROLS_ENABLED, 1) == 0) {
100             fireControlsUpdate()
101         } else {
102             checkMigrationToQs()
103             controlsComponent.getControlsListingController().ifPresent {
104                 it.addCallback(listingCallback)
105             }
106         }
107     }
108 
109     override fun removeCallback() {
110         position = null
111         callback = null
112         controlsComponent.getControlsListingController().ifPresent {
113             it.removeCallback(listingCallback)
114         }
115     }
116 
117     private fun fireControlsUpdate() {
118         Log.i(TAG, "Setting DeviceControlsTile position: $position")
119         callback?.onControlsUpdate(position)
120     }
121 
122     /**
123      * See if any available control service providers match one of the preferred components. If they
124      * do, and there are no current favorites for that component, query the preferred component for
125      * a limited number of suggested controls.
126      */
127     private fun seedFavorites(serviceInfos: List<ControlsServiceInfo>) {
128         val preferredControlsPackages =
129             context.getResources().getStringArray(R.array.config_controlsPreferredPackages)
130 
131         val prefs =
132             userContextProvider.userContext.getSharedPreferences(
133                 PREFS_CONTROLS_FILE,
134                 Context.MODE_PRIVATE
135             )
136         val seededPackages =
137             prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet()) ?: emptySet()
138 
139         val controlsController = controlsComponent.getControlsController().get()
140         val componentsToSeed = mutableListOf<ComponentName>()
141         var i = 0
142         while (i < Math.min(SEEDING_MAX, preferredControlsPackages.size)) {
143             val pkg = preferredControlsPackages[i]
144             serviceInfos.forEach {
145                 if (pkg.equals(it.componentName.packageName) && !seededPackages.contains(pkg)) {
146                     if (controlsController.countFavoritesForComponent(it.componentName) > 0) {
147                         // When there are existing controls but no saved preference, assume it
148                         // is out of sync, perhaps through a device restore, and update the
149                         // preference
150                         addPackageToSeededSet(prefs, pkg)
151                     } else if (it.panelActivity != null) {
152                         // Do not seed for packages with panels
153                         addPackageToSeededSet(prefs, pkg)
154                     } else {
155                         componentsToSeed.add(it.componentName)
156                     }
157                 }
158             }
159             i++
160         }
161 
162         if (componentsToSeed.isEmpty()) return
163 
164         controlsController.seedFavoritesForComponents(
165             componentsToSeed,
166             { response ->
167                 Log.d(TAG, "Controls seeded: $response")
168                 if (response.accepted) {
169                     addPackageToSeededSet(prefs, response.packageName)
170                     if (position == null) {
171                         position = QS_DEFAULT_POSITION
172                     }
173                     fireControlsUpdate()
174 
175                     controlsComponent.getControlsListingController().ifPresent {
176                         it.removeCallback(listingCallback)
177                     }
178                 }
179             }
180         )
181     }
182 
183     private fun addPackageToSeededSet(prefs: SharedPreferences, pkg: String) {
184         val seededPackages =
185             prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet()) ?: emptySet()
186         val updatedPkgs = seededPackages.toMutableSet()
187         updatedPkgs.add(pkg)
188         prefs.edit().putStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, updatedPkgs).apply()
189     }
190 }
191