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

Commit 8744ef63 authored by Coco Duan's avatar Coco Duan
Browse files

Bakup and restore communal shared preferences

Create a SharedPreferencesBackupHelper for communal shared preferences
in order to backup and restore communal_shared_prefs.xml file on device.

Bug: b/330945464
Test: dismiss CTA tile; run commands in go/localtransport to backup;
clear `communal_shared_prefs.xml` from the device; CTA shows; restore
with commands; verify CTA tile state is restored as dismissed.
Test: atest CommunalPrefsRepositoryImplTest
Flag: ACONFIG com.android.systemui.communal_hub TEAMFOOD

Change-Id: Ia9e5df9c86ea776899d565bb8c14e5b829627e75
parent 45466a19
Loading
Loading
Loading
Loading
+52 −10
Original line number Diff line number Diff line
@@ -16,11 +16,15 @@

package com.android.systemui.communal.data.repository

import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.UserInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.backup.BackupHelper
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryImpl.Companion.FILE_NAME
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testDispatcher
@@ -34,16 +38,22 @@ import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.FakeSharedPreferences
import com.google.common.truth.Truth.assertThat
import java.io.File
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@android.platform.test.annotations.EnabledOnRavenwood
class CommunalPrefsRepositoryImplTest : SysuiTestCase() {
    @Mock private lateinit var tableLogBuffer: TableLogBuffer

@@ -69,20 +79,12 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() {
                    USER_INFOS[1].id to FakeSharedPreferences()
                )
            )
        underTest =
            CommunalPrefsRepositoryImpl(
                testScope.backgroundScope,
                kosmos.testDispatcher,
                userRepository,
                userFileManager,
                logcatLogBuffer("CommunalPrefsRepositoryImplTest"),
                tableLogBuffer,
            )
    }

    @Test
    fun isCtaDismissedValue_byDefault_isFalse() =
        testScope.runTest {
            underTest = createCommunalPrefsRepositoryImpl(userFileManager)
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)
            assertThat(isCtaDismissed).isFalse()
        }
@@ -90,6 +92,7 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() {
    @Test
    fun isCtaDismissedValue_onSet_isTrue() =
        testScope.runTest {
            underTest = createCommunalPrefsRepositoryImpl(userFileManager)
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)

            underTest.setCtaDismissedForCurrentUser()
@@ -99,6 +102,7 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() {
    @Test
    fun isCtaDismissedValue_whenSwitchUser() =
        testScope.runTest {
            underTest = createCommunalPrefsRepositoryImpl(userFileManager)
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)
            underTest.setCtaDismissedForCurrentUser()

@@ -118,6 +122,44 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() {
            assertThat(isCtaDismissed).isTrue()
        }

    @Test
    fun getSharedPreferences_whenFileRestored() =
        testScope.runTest {
            val userFileManagerSpy = Mockito.spy(userFileManager)
            underTest = createCommunalPrefsRepositoryImpl(userFileManagerSpy)

            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)
            userRepository.setSelectedUserInfo(USER_INFOS[0])
            assertThat(isCtaDismissed).isFalse()
            clearInvocations(userFileManagerSpy)

            // Received restore finished event.
            kosmos.broadcastDispatcher.sendIntentToMatchingReceiversOnly(
                context,
                Intent(BackupHelper.ACTION_RESTORE_FINISHED),
            )
            runCurrent()

            // Get shared preferences from the restored file.
            verify(userFileManagerSpy, atLeastOnce())
                .getSharedPreferences(
                    FILE_NAME,
                    Context.MODE_PRIVATE,
                    userRepository.getSelectedUserInfo().id
                )
        }

    private fun createCommunalPrefsRepositoryImpl(userFileManager: UserFileManager) =
        CommunalPrefsRepositoryImpl(
            testScope.backgroundScope,
            kosmos.testDispatcher,
            userRepository,
            userFileManager,
            kosmos.broadcastDispatcher,
            logcatLogBuffer("CommunalPrefsRepositoryImplTest"),
            tableLogBuffer,
        )

    private class FakeUserFileManager(private val sharedPrefs: Map<Int, SharedPreferences>) :
        UserFileManager {
        override fun getFile(fileName: String, userId: Int): File {
+19 −0
Original line number Diff line number Diff line
@@ -28,10 +28,14 @@ import android.os.ParcelFileDescriptor
import android.os.UserHandle
import android.util.Log
import com.android.app.tracing.traceSection
import com.android.systemui.Flags.communalHub
import com.android.systemui.backup.BackupHelper.Companion.ACTION_RESTORE_FINISHED
import com.android.systemui.communal.domain.backup.CommunalPrefsBackupHelper
import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper
import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper
import com.android.systemui.keyguard.domain.backup.KeyguardQuickAffordanceBackupHelper
import com.android.systemui.people.widget.PeopleBackupHelper
import com.android.systemui.res.R
import com.android.systemui.settings.UserFileManagerImpl

/**
@@ -53,6 +57,8 @@ open class BackupHelper : BackupAgentHelper() {
        private const val PEOPLE_TILES_BACKUP_KEY = "systemui.people.shared_preferences"
        private const val KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY =
            "systemui.keyguard.quickaffordance.shared_preferences"
        private const val COMMUNAL_PREFS_BACKUP_KEY =
            "systemui.communal.shared_preferences"
        val controlsDataLock = Any()
        const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED"
        const val PERMISSION_SELF = "com.android.systemui.permission.SELF"
@@ -75,6 +81,15 @@ open class BackupHelper : BackupAgentHelper() {
                userId = userHandle.identifier,
            ),
        )
        if (communalEnabled()) {
            addHelper(
                COMMUNAL_PREFS_BACKUP_KEY,
                CommunalPrefsBackupHelper(
                    context = this,
                    userId = userHandle.identifier,
                )
            )
        }
    }

    override fun onRestoreFinished() {
@@ -100,6 +115,10 @@ open class BackupHelper : BackupAgentHelper() {
        }
    }

    private fun communalEnabled(): Boolean {
        return resources.getBoolean(R.bool.config_communalServiceEnabled) && communalHub()
    }

    /**
     * Helper class for restoring files ONLY if they are not present.
     *
+29 −1
Original line number Diff line number Diff line
@@ -17,8 +17,11 @@
package com.android.systemui.communal.data.repository

import android.content.Context
import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.pm.UserInfo
import com.android.systemui.backup.BackupHelper
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.LogBuffer
@@ -30,15 +33,18 @@ import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.settings.UserFileManager
import com.android.systemui.user.data.repository.UserRepository
import com.android.systemui.util.kotlin.SharedPreferencesExt.observe
import com.android.systemui.util.kotlin.emitOnStart
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
@@ -65,14 +71,36 @@ constructor(
    @Background private val bgDispatcher: CoroutineDispatcher,
    private val userRepository: UserRepository,
    private val userFileManager: UserFileManager,
    broadcastDispatcher: BroadcastDispatcher,
    @CommunalLog logBuffer: LogBuffer,
    @CommunalTableLog tableLogBuffer: TableLogBuffer,
) : CommunalPrefsRepository {

    private val logger = Logger(logBuffer, "CommunalPrefsRepositoryImpl")

    /**
     * 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 isCtaDismissed: Flow<Boolean> =
        userRepository.selectedUserInfo
        combine(
                userRepository.selectedUserInfo,
                // Make sure combine can emit even if we never get a Backup & Restore event,
                // which is the most common case as restoration only happens on initial device
                // setup.
                backupRestorationEvents.emitOnStart().onEach {
                    logger.i("Restored state for communal preferences.")
                },
            ) { user, _ ->
                user
            }
            .flatMapLatest(::observeCtaDismissState)
            .logDiffsForTable(
                tableLogBuffer = tableLogBuffer,
+37 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.communal.domain.backup

import android.app.backup.SharedPreferencesBackupHelper
import android.content.Context
import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryImpl.Companion.FILE_NAME
import com.android.systemui.settings.UserFileManagerImpl

/** Helper to backup & restore the shared preferences in glanceable hub for the current user. */
class CommunalPrefsBackupHelper(
    context: Context,
    userId: Int,
) :
    SharedPreferencesBackupHelper(
        context,
        UserFileManagerImpl.createFile(
                userId = userId,
                fileName = FILE_NAME,
            )
            .path
    )