Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 01c614b2 authored by Ale Nijamkin's avatar Ale Nijamkin Committed by Automerger Merge Worker
Browse files

Merge "Multi-user customizable quick affordances." into tm-qpr-dev am: eda61eee am: 551b4f4e

parents 44f15527 551b4f4e
Loading
Loading
Loading
Loading
+25 −17
Original line number Diff line number Diff line
@@ -17,12 +17,19 @@

package com.android.systemui.keyguard.data.quickaffordance

import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.multibindings.ElementsIntoSet

@Module
object KeyguardDataQuickAffordanceModule {
interface KeyguardDataQuickAffordanceModule {
    @Binds
    fun providerClientFactory(
        impl: KeyguardQuickAffordanceProviderClientFactoryImpl,
    ): KeyguardQuickAffordanceProviderClientFactory

    companion object {
        @Provides
        @ElementsIntoSet
        fun quickAffordanceConfigs(
@@ -41,3 +48,4 @@ object KeyguardDataQuickAffordanceModule {
            )
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -67,7 +67,7 @@ constructor(
    @Application private val scope: CoroutineScope,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val secureSettings: SecureSettings,
    private val selectionsManager: KeyguardQuickAffordanceSelectionManager,
    private val selectionsManager: KeyguardQuickAffordanceLocalUserSelectionManager,
) {
    companion object {
        private val BINDINGS =
+184 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.keyguard.data.quickaffordance

import android.content.Context
import android.content.IntentFilter
import android.content.SharedPreferences
import com.android.systemui.R
import com.android.systemui.backup.BackupHelper
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.onStart

/**
 * Manages and provides access to the current "selections" of keyguard quick affordances, answering
 * the question "which affordances should the keyguard show?" for the user associated with the
 * System UI process.
 */
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class KeyguardQuickAffordanceLocalUserSelectionManager
@Inject
constructor(
    @Application context: Context,
    private val userFileManager: UserFileManager,
    private val userTracker: UserTracker,
    broadcastDispatcher: BroadcastDispatcher,
) : KeyguardQuickAffordanceSelectionManager {

    private var sharedPrefs: SharedPreferences = instantiateSharedPrefs()

    private val userId: Flow<Int> = conflatedCallbackFlow {
        val callback =
            object : UserTracker.Callback {
                override fun onUserChanged(newUser: Int, userContext: Context) {
                    trySendWithFailureLogging(newUser, TAG)
                }
            }

        userTracker.addCallback(callback) { it.run() }
        trySendWithFailureLogging(userTracker.userId, TAG)

        awaitClose { userTracker.removeCallback(callback) }
    }

    private val defaults: Map<String, List<String>> by lazy {
        context.resources
            .getStringArray(R.array.config_keyguardQuickAffordanceDefaults)
            .associate { item ->
                val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER)
                check(splitUp.size == 2)
                val slotId = splitUp[0]
                val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER)
                slotId to affordanceIds
            }
    }

    /**
     * Emits an event each time a Backup & Restore restoration job is completed. Does not emit an
     * initial value.
     */
    private val backupRestorationEvents: Flow<Unit> =
        broadcastDispatcher.broadcastFlow(
            filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
            flags = Context.RECEIVER_NOT_EXPORTED,
            permission = BackupHelper.PERMISSION_SELF,
        )

    override val selections: Flow<Map<String, List<String>>> =
        combine(
                userId,
                backupRestorationEvents.onStart {
                    // We emit an initial event to make sure that the combine emits at least once,
                    // even if we never get a Backup & Restore restoration event (which is the most
                    // common case anyway as restoration really only happens on initial device
                    // setup).
                    emit(Unit)
                }
            ) { _, _ -> }
            .flatMapLatest {
                conflatedCallbackFlow {
                    // We want to instantiate a new SharedPreferences instance each time either the
                    // user ID changes or we have a backup & restore restoration event. The reason
                    // is that our sharedPrefs instance needs to be replaced with a new one as it
                    // depends on the user ID and when the B&R job completes, the backing file is
                    // replaced but the existing instance still has a stale in-memory cache.
                    sharedPrefs = instantiateSharedPrefs()

                    val listener =
                        SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
                            trySend(getSelections())
                        }

                    sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
                    send(getSelections())

                    awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) }
                }
            }

    override fun getSelections(): Map<String, List<String>> {
        val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) }
        val result =
            slotKeys
                .associate { key ->
                    val slotId = key.substring(KEY_PREFIX_SLOT.length)
                    val value = sharedPrefs.getString(key, null)
                    val affordanceIds =
                        if (!value.isNullOrEmpty()) {
                            value.split(AFFORDANCE_DELIMITER)
                        } else {
                            emptyList()
                        }
                    slotId to affordanceIds
                }
                .toMutableMap()

        // If the result map is missing keys, it means that the system has never set anything for
        // those slots. This is where we need examine our defaults and see if there should be a
        // default value for the affordances in the slot IDs that are missing from the result.
        //
        // Once the user makes any selection for a slot, even when they select "None", this class
        // will persist a key for that slot ID. In the case of "None", it will have a value of the
        // empty string. This is why this system works.
        defaults.forEach { (slotId, affordanceIds) ->
            if (!result.containsKey(slotId)) {
                result[slotId] = affordanceIds
            }
        }

        return result
    }

    override fun setSelections(
        slotId: String,
        affordanceIds: List<String>,
    ) {
        val key = "$KEY_PREFIX_SLOT$slotId"
        val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER)
        sharedPrefs.edit().putString(key, value).apply()
    }

    private fun instantiateSharedPrefs(): SharedPreferences {
        return userFileManager.getSharedPreferences(
            FILE_NAME,
            Context.MODE_PRIVATE,
            userTracker.userId,
        )
    }

    companion object {
        private const val TAG = "KeyguardQuickAffordancePrimaryUserSelectionManager"
        const val FILE_NAME = "quick_affordance_selections"
        private const val KEY_PREFIX_SLOT = "slot_"
        private const val SLOT_AFFORDANCES_DELIMITER = ":"
        private const val AFFORDANCE_DELIMITER = ","
    }
}
+43 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.keyguard.data.quickaffordance

import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient
import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClientImpl
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher

interface KeyguardQuickAffordanceProviderClientFactory {
    fun create(): KeyguardQuickAffordanceProviderClient
}

class KeyguardQuickAffordanceProviderClientFactoryImpl
@Inject
constructor(
    private val userTracker: UserTracker,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
) : KeyguardQuickAffordanceProviderClientFactory {
    override fun create(): KeyguardQuickAffordanceProviderClient {
        return KeyguardQuickAffordanceProviderClientImpl(
            context = userTracker.userContext,
            backgroundDispatcher = backgroundDispatcher,
        )
    }
}
+129 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.keyguard.data.quickaffordance

import android.content.Context
import android.os.UserHandle
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.settings.UserTracker
import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

/**
 * Manages and provides access to the current "selections" of keyguard quick affordances, answering
 * the question "which affordances should the keyguard show?" for users associated with other System
 * UI processes.
 */
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class KeyguardQuickAffordanceRemoteUserSelectionManager
@Inject
constructor(
    @Application private val scope: CoroutineScope,
    private val userTracker: UserTracker,
    private val clientFactory: KeyguardQuickAffordanceProviderClientFactory,
    private val userHandle: UserHandle,
) : KeyguardQuickAffordanceSelectionManager {

    private val userId: Flow<Int> = conflatedCallbackFlow {
        val callback =
            object : UserTracker.Callback {
                override fun onUserChanged(newUser: Int, userContext: Context) {
                    trySendWithFailureLogging(newUser, TAG)
                }
            }

        userTracker.addCallback(callback) { it.run() }
        trySendWithFailureLogging(userTracker.userId, TAG)

        awaitClose { userTracker.removeCallback(callback) }
    }

    private val clientOrNull: StateFlow<KeyguardQuickAffordanceProviderClient?> =
        userId
            .distinctUntilChanged()
            .map { selectedUserId ->
                if (userHandle.isSystem && userHandle.identifier != selectedUserId) {
                    clientFactory.create()
                } else {
                    null
                }
            }
            .stateIn(
                scope = scope,
                started = SharingStarted.Eagerly,
                initialValue = null,
            )

    private val _selections: StateFlow<Map<String, List<String>>> =
        clientOrNull
            .flatMapLatest { client ->
                client?.observeSelections()?.map { selections ->
                    buildMap<String, List<String>> {
                        selections.forEach { selection ->
                            val slotId = selection.slotId
                            val affordanceIds = (get(slotId) ?: emptyList()).toMutableList()
                            affordanceIds.add(selection.affordanceId)
                            put(slotId, affordanceIds)
                        }
                    }
                }
                    ?: emptyFlow()
            }
            .stateIn(
                scope = scope,
                started = SharingStarted.Eagerly,
                initialValue = emptyMap(),
            )

    override val selections: Flow<Map<String, List<String>>> = _selections

    override fun getSelections(): Map<String, List<String>> {
        return _selections.value
    }

    override fun setSelections(slotId: String, affordanceIds: List<String>) {
        clientOrNull.value?.let { client ->
            scope.launch {
                client.deleteAllSelections(slotId = slotId)
                affordanceIds.forEach { affordanceId ->
                    client.insertSelection(slotId = slotId, affordanceId = affordanceId)
                }
            }
        }
    }

    companion object {
        private const val TAG = "KeyguardQuickAffordanceMultiUserSelectionManager"
    }
}
Loading