/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.statusbar.policy import android.content.ComponentName import android.content.Context import android.content.SharedPreferences import android.provider.Settings import android.util.Log import com.android.systemui.R import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.phone.AutoTileManager import com.android.systemui.statusbar.policy.DeviceControlsController.Callback import com.android.systemui.util.settings.SecureSettings import javax.inject.Inject /** * Watches for Device Controls QS Tile activation, which can happen in two ways: *
    *
  1. Migration from Power Menu - For existing Android 11 users, create a tile in a high * priority position. *
  2. Device controls service becomes available - For non-migrated users, create a tile and * place at the end of active tiles, and initiate seeding where possible. *
*/ @SysUISingleton public class DeviceControlsControllerImpl @Inject constructor( private val context: Context, private val controlsComponent: ControlsComponent, private val userContextProvider: UserContextProvider, private val secureSettings: SecureSettings ) : DeviceControlsController { private var callback: Callback? = null internal var position: Int? = null private val listingCallback = object : ControlsListingController.ControlsListingCallback { override fun onServicesUpdated(serviceInfos: List) { if (!serviceInfos.isEmpty()) { seedFavorites(serviceInfos) } } } companion object { private const val TAG = "DeviceControlsControllerImpl" internal const val QS_PRIORITY_POSITION = 3 internal const val QS_DEFAULT_POSITION = 7 internal const val PREFS_CONTROLS_SEEDING_COMPLETED = "SeedingCompleted" const val PREFS_CONTROLS_FILE = "controls_prefs" private const val SEEDING_MAX = 2 } private fun checkMigrationToQs() { controlsComponent.getControlsController().ifPresent { if (!it.getFavorites().isEmpty()) { position = QS_PRIORITY_POSITION fireControlsUpdate() } } } /** * This migration logic assumes that something like [AutoTileManager] is tracking state * externally, and won't call this method after receiving a response via * [Callback#onControlsUpdate], once per user. Otherwise the calculated position may be * incorrect. */ override fun setCallback(callback: Callback) { if (!controlsComponent.isEnabled()) { callback.removeControlsAutoTracker() return } // Treat any additional call as a reset before recalculating removeCallback() this.callback = callback if (secureSettings.getInt(Settings.Secure.CONTROLS_ENABLED, 1) == 0) { fireControlsUpdate() } else { checkMigrationToQs() controlsComponent.getControlsListingController().ifPresent { it.addCallback(listingCallback) } } } override fun removeCallback() { position = null callback = null controlsComponent.getControlsListingController().ifPresent { it.removeCallback(listingCallback) } } private fun fireControlsUpdate() { Log.i(TAG, "Setting DeviceControlsTile position: $position") callback?.onControlsUpdate(position) } /** * See if any available control service providers match one of the preferred components. If * they do, and there are no current favorites for that component, query the preferred * component for a limited number of suggested controls. */ private fun seedFavorites(serviceInfos: List) { val preferredControlsPackages = context.getResources().getStringArray( R.array.config_controlsPreferredPackages) val prefs = userContextProvider.userContext.getSharedPreferences( PREFS_CONTROLS_FILE, Context.MODE_PRIVATE) val seededPackages = prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet()) ?: emptySet() val controlsController = controlsComponent.getControlsController().get() val componentsToSeed = mutableListOf() var i = 0 while (i < Math.min(SEEDING_MAX, preferredControlsPackages.size)) { val pkg = preferredControlsPackages[i] serviceInfos.forEach { if (pkg.equals(it.componentName.packageName) && !seededPackages.contains(pkg)) { if (controlsController.countFavoritesForComponent(it.componentName) > 0) { // When there are existing controls but no saved preference, assume it // is out of sync, perhaps through a device restore, and update the // preference addPackageToSeededSet(prefs, pkg) } else if (it.panelActivity != null) { // Do not seed for packages with panels addPackageToSeededSet(prefs, pkg) } else { componentsToSeed.add(it.componentName) } } } i++ } if (componentsToSeed.isEmpty()) return controlsController.seedFavoritesForComponents( componentsToSeed, { response -> Log.d(TAG, "Controls seeded: $response") if (response.accepted) { addPackageToSeededSet(prefs, response.packageName) if (position == null) { position = QS_DEFAULT_POSITION } fireControlsUpdate() controlsComponent.getControlsListingController().ifPresent { it.removeCallback(listingCallback) } } }) } private fun addPackageToSeededSet(prefs: SharedPreferences, pkg: String) { val seededPackages = prefs.getStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, emptySet()) ?: emptySet() val updatedPkgs = seededPackages.toMutableSet() updatedPkgs.add(pkg) prefs.edit().putStringSet(PREFS_CONTROLS_SEEDING_COMPLETED, updatedPkgs).apply() } }