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

Commit 74df4ef2 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

Multi-user customizable quick affordances.

This CL adds support for multi-users to the customizable lock screen
quick affordance system.

The process that creates windows and renders System UI is always the
primary user's system UI process. The process that owns the content
provider accessed by Wallpaper Picker is bound to the currently-selected
user on the system, which may be different than the primary user.

What this means is that, when switching to a secondary user, what the
Wallpaper Picker sees is fed to it from a secondary system UI process
which is not the same system UI process that is rendering the
affordances on the lock screen. Therefore, it didn't work.

This CL adds a "remote user" selection manager which the repository can
switch to when it's running on the primary user process but needs to
query the state of the selected affordances for a secondary user.

The "remote user" selection manager queries the content provider
associated with the system UI process linked to the secondary user. This
way, we can display the correct affordances on the screen, even when
switching to a secondary user.

Fix: 260251307
Test: included new and expanded unit tests. Manually verified that the
selection of quick affordances for the primary and secondary user are
correctly reflected on the lock screen when switching users and is
retained even after switching away and back.

Change-Id: I281577ed6efb987c23b19c2078e77c91e45ce9f2
parent e5f67447
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