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