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