blob: dedd341a46bebba82026634531bb888434b521e0 [file] [log] [blame]
/*
* Copyright (C) 2019 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.controls.controller
import android.app.ActivityManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.database.ContentObserver
import android.net.Uri
import android.os.Environment
import android.os.UserHandle
import android.provider.Settings
import android.service.controls.Control
import android.service.controls.actions.ControlAction
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.ControlsServiceInfo
import com.android.systemui.controls.management.ControlsListingController
import com.android.systemui.controls.ui.ControlsUiController
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dump.DumpManager
import com.android.systemui.util.concurrency.DelayableExecutor
import java.io.FileDescriptor
import java.io.PrintWriter
import java.util.Optional
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ControlsControllerImpl @Inject constructor (
private val context: Context,
@Background private val executor: DelayableExecutor,
private val uiController: ControlsUiController,
private val bindingController: ControlsBindingController,
private val listingController: ControlsListingController,
private val broadcastDispatcher: BroadcastDispatcher,
optionalWrapper: Optional<ControlsFavoritePersistenceWrapper>,
dumpManager: DumpManager
) : Dumpable, ControlsController {
companion object {
private const val TAG = "ControlsControllerImpl"
internal const val CONTROLS_AVAILABLE = "systemui.controls_available"
internal val URI = Settings.Secure.getUriFor(CONTROLS_AVAILABLE)
private const val USER_CHANGE_RETRY_DELAY = 500L // ms
private const val DEFAULT_ENABLED = 1
}
private var userChanging: Boolean = true
private var currentUser = UserHandle.of(ActivityManager.getCurrentUser())
override val currentUserId
get() = currentUser.identifier
private val contentResolver: ContentResolver
get() = context.contentResolver
override var available = Settings.Secure.getIntForUser(
contentResolver, CONTROLS_AVAILABLE, DEFAULT_ENABLED, currentUserId) != 0
private set
private val persistenceWrapper = optionalWrapper.orElseGet {
ControlsFavoritePersistenceWrapper(
Environment.buildPath(
context.filesDir,
ControlsFavoritePersistenceWrapper.FILE_NAME
),
executor
)
}
private fun setValuesForUser(newUser: UserHandle) {
Log.d(TAG, "Changing to user: $newUser")
currentUser = newUser
val userContext = context.createContextAsUser(currentUser, 0)
val fileName = Environment.buildPath(
userContext.filesDir, ControlsFavoritePersistenceWrapper.FILE_NAME)
persistenceWrapper.changeFile(fileName)
available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
DEFAULT_ENABLED, newUser.identifier) != 0
resetFavorites(available)
bindingController.changeUser(newUser)
listingController.changeUser(newUser)
userChanging = false
}
private val userSwitchReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_USER_SWITCHED) {
userChanging = true
listingController.removeCallback(listingCallback)
val newUser =
UserHandle.of(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, sendingUserId))
if (currentUser == newUser) {
userChanging = false
return
}
setValuesForUser(newUser)
}
}
}
@VisibleForTesting
internal val settingObserver = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean, uri: Uri, userId: Int) {
// Do not listen to changes in the middle of user change, those will be read by the
// user-switch receiver.
if (userChanging || userId != currentUserId) {
return
}
available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
DEFAULT_ENABLED, currentUserId) != 0
resetFavorites(available)
}
}
// Handling of removed components
/**
* Check if any component has been removed and if so, remove all its favorites.
*
* If some component has been removed, the new set of favorites will also be saved.
*/
private val listingCallback = object : ControlsListingController.ControlsListingCallback {
override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
executor.execute {
val serviceInfoSet = serviceInfos.map(ControlsServiceInfo::componentName).toSet()
val favoriteComponentSet = Favorites.getAllStructures().map {
it.componentName
}.toSet()
var changed = false
favoriteComponentSet.subtract(serviceInfoSet).forEach {
changed = true
Favorites.removeStructures(it)
bindingController.onComponentRemoved(it)
}
// Check if something has been removed, if so, store the new list
if (changed) {
persistenceWrapper.storeFavorites(Favorites.getAllStructures())
}
}
}
}
init {
dumpManager.registerDumpable(javaClass.name, this)
resetFavorites(available)
userChanging = false
broadcastDispatcher.registerReceiver(
userSwitchReceiver,
IntentFilter(Intent.ACTION_USER_SWITCHED),
executor,
UserHandle.ALL
)
contentResolver.registerContentObserver(URI, false, settingObserver, UserHandle.USER_ALL)
}
private fun resetFavorites(shouldLoad: Boolean) {
Favorites.clear()
if (shouldLoad) {
Favorites.load(persistenceWrapper.readFavorites())
listingController.addCallback(listingCallback)
}
}
private fun confirmAvailability(): Boolean {
if (userChanging) {
Log.w(TAG, "Controls not available while user is changing")
return false
}
if (!available) {
Log.d(TAG, "Controls not available")
return false
}
return true
}
override fun loadForComponent(
componentName: ComponentName,
dataCallback: Consumer<ControlsController.LoadData>
) {
if (!confirmAvailability()) {
if (userChanging) {
// Try again later, userChanging should not last forever. If so, we have bigger
// problems
executor.executeDelayed(
{ loadForComponent(componentName, dataCallback) },
USER_CHANGE_RETRY_DELAY,
TimeUnit.MILLISECONDS
)
} else {
dataCallback.accept(createLoadDataObject(emptyList(), emptyList(), true))
}
return
}
bindingController.bindAndLoad(
componentName,
object : ControlsBindingController.LoadCallback {
override fun accept(controls: List<Control>) {
executor.execute {
val favoritesForComponentKeys = Favorites
.getControlsForComponent(componentName).map { it.controlId }
val changed = Favorites.updateControls(componentName, controls)
if (changed) {
persistenceWrapper.storeFavorites(Favorites.getAllStructures())
}
val removed = findRemoved(favoritesForComponentKeys.toSet(), controls)
val controlsWithFavorite = controls.map {
ControlStatus(it, it.controlId in favoritesForComponentKeys)
}
val loadData = createLoadDataObject(
Favorites.getControlsForComponent(componentName)
.filter { it.controlId in removed }
.map { createRemovedStatus(componentName, it) } +
controlsWithFavorite,
favoritesForComponentKeys
)
dataCallback.accept(loadData)
}
}
override fun error(message: String) {
val loadData = Favorites.getControlsForComponent(componentName).let {
controls ->
val keys = controls.map { it.controlId }
createLoadDataObject(
controls.map { createRemovedStatus(componentName, it, false) },
keys,
true
)
}
dataCallback.accept(loadData)
}
}
)
}
private fun createRemovedStatus(
componentName: ComponentName,
controlInfo: ControlInfo,
setRemoved: Boolean = true
): ControlStatus {
val intent = Intent(Intent.ACTION_MAIN).apply {
addCategory(Intent.CATEGORY_LAUNCHER)
this.`package` = componentName.packageName
}
val pendingIntent = PendingIntent.getActivity(context,
componentName.hashCode(),
intent,
0)
val control = Control.StatelessBuilder(controlInfo.controlId, pendingIntent)
.setTitle(controlInfo.controlTitle)
.setDeviceType(controlInfo.deviceType)
.build()
return ControlStatus(control, true, setRemoved)
}
private fun findRemoved(favoriteKeys: Set<String>, list: List<Control>): Set<String> {
val controlsKeys = list.map { it.controlId }
return favoriteKeys.minus(controlsKeys)
}
override fun subscribeToFavorites(structureInfo: StructureInfo) {
if (!confirmAvailability()) return
bindingController.subscribe(structureInfo)
}
override fun unsubscribe() {
if (!confirmAvailability()) return
bindingController.unsubscribe()
}
override fun replaceFavoritesForStructure(structureInfo: StructureInfo) {
if (!confirmAvailability()) return
executor.execute {
Favorites.replaceControls(structureInfo)
persistenceWrapper.storeFavorites(Favorites.getAllStructures())
}
}
override fun refreshStatus(componentName: ComponentName, control: Control) {
if (!confirmAvailability()) {
Log.d(TAG, "Controls not available")
return
}
executor.execute {
val changed = Favorites.updateControls(
componentName,
listOf(control)
)
if (changed) {
persistenceWrapper.storeFavorites(Favorites.getAllStructures())
}
}
uiController.onRefreshState(componentName, listOf(control))
}
override fun onActionResponse(componentName: ComponentName, controlId: String, response: Int) {
if (!confirmAvailability()) return
uiController.onActionResponse(componentName, controlId, response)
}
override fun action(
componentName: ComponentName,
controlInfo: ControlInfo,
action: ControlAction
) {
if (!confirmAvailability()) return
bindingController.action(componentName, controlInfo, action)
}
override fun getFavorites(): List<StructureInfo> = Favorites.getAllStructures()
override fun countFavoritesForComponent(componentName: ComponentName): Int =
Favorites.getControlsForComponent(componentName).size
override fun getFavoritesForComponent(componentName: ComponentName): List<StructureInfo> =
Favorites.getStructuresForComponent(componentName)
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
pw.println("ControlsController state:")
pw.println(" Available: $available")
pw.println(" Changing users: $userChanging")
pw.println(" Current user: ${currentUser.identifier}")
pw.println(" Favorites:")
Favorites.getAllStructures().forEach { s ->
pw.println(" ${ s }")
s.controls.forEach { c ->
pw.println(" ${ c }")
}
}
pw.println(bindingController.toString())
}
}
/**
* Relies on immutable data for thread safety. When necessary to update favMap, use reassignment to
* replace it, which will not disrupt any ongoing map traversal.
*
* Update/replace calls should use thread isolation to avoid race conditions.
*/
private object Favorites {
private var favMap = mapOf<ComponentName, List<StructureInfo>>()
fun getAllStructures(): List<StructureInfo> = favMap.flatMap { it.value }
fun getStructuresForComponent(componentName: ComponentName): List<StructureInfo> =
favMap.get(componentName) ?: emptyList()
fun getControlsForStructure(structure: StructureInfo): List<ControlInfo> =
getStructuresForComponent(structure.componentName)
.firstOrNull { it.structure == structure.structure }
?.controls ?: emptyList()
fun getControlsForComponent(componentName: ComponentName): List<ControlInfo> =
getStructuresForComponent(componentName).flatMap { it.controls }
fun load(structures: List<StructureInfo>) {
favMap = structures.groupBy { it.componentName }
}
fun updateControls(componentName: ComponentName, controls: List<Control>): Boolean {
val controlsById = controls.associateBy { it.controlId }
// utilize a new map to allow for changes to structure names
val structureToControls = mutableMapOf<CharSequence, MutableList<ControlInfo>>()
// Must retain the current control order within each structure
var changed = false
getStructuresForComponent(componentName).forEach { s ->
s.controls.forEach { c ->
val (sName, ci) = controlsById.get(c.controlId)?.let { updatedControl ->
val controlInfo = if (updatedControl.title != c.controlTitle ||
updatedControl.deviceType != c.deviceType) {
changed = true
c.copy(
controlTitle = updatedControl.title,
deviceType = updatedControl.deviceType
)
} else { c }
val updatedStructure = updatedControl.structure ?: ""
if (s.structure != updatedStructure) {
changed = true
}
Pair(updatedStructure, controlInfo)
} ?: Pair(s.structure, c)
structureToControls.getOrPut(sName, { mutableListOf() }).add(ci)
}
}
if (!changed) return false
val structures = structureToControls.map { (s, cs) -> StructureInfo(componentName, s, cs) }
val newFavMap = favMap.toMutableMap()
newFavMap.put(componentName, structures)
favMap = newFavMap
return true
}
fun removeStructures(componentName: ComponentName) {
val newFavMap = favMap.toMutableMap()
newFavMap.remove(componentName)
favMap = newFavMap
}
fun replaceControls(updatedStructure: StructureInfo) {
val newFavMap = favMap.toMutableMap()
val structures = mutableListOf<StructureInfo>()
val componentName = updatedStructure.componentName
var replaced = false
getStructuresForComponent(componentName).forEach { s ->
val newStructure = if (s.structure == updatedStructure.structure) {
replaced = true
updatedStructure
} else { s }
structures.add(newStructure)
}
if (!replaced) {
structures.add(updatedStructure)
}
newFavMap.put(componentName, structures.toList())
favMap = newFavMap.toMap()
}
fun clear() {
favMap = mapOf<ComponentName, List<StructureInfo>>()
}
}