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

Commit cc4e5b4c authored by Fabián Kozynski's avatar Fabián Kozynski
Browse files

Restore work tile position after restore

When the device is restored from a source device that currently had the
work profile tile, the tile will fail to be present in the target device
when the user enables the work profile. This is due to the following:

* Right on restore, the tile is restored, but because the work profile
  is not ready yet, it will be destroyed.
* Along with the restore, we have the fact that the tile was auto-added
  in the source device, meaning it will fail to auto-add once the work
  profile is enabled.

Even if it restored, it will be auto-added at the end, because that's
the default.

This change (effective only in the new pipeline) adds extra processing
when restoring if the work tile is present in the restore.

* If the work profile is already enabled, do nothing as the tile won't
  be restored.
* If the work profile is not enabled, remove the fact that it was
  auto-added + store the position in which it was. Then when the work
  profile is enabled it will be restored in the correct position.

This CL also:

* Adds Kosmos support for the new pipeline
* Drive by fixes FakeUserTracker to dispatch when the profiles change.

Test: atest com.android.systemui.qs.pipeline
Fixes: 314781280
Flag: ACONFIG com.android.systemui.qs_new_pipeline TEAMFOOD

Change-Id: Ib943146659e79d28c876f0d09bda451b5020bb6e
parent 330a4fc2
Loading
Loading
Loading
Loading
+95 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.qs.pipeline.data.restoreprocessors

import android.os.UserHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class WorkTileRestoreProcessorTest : SysuiTestCase() {

    private val underTest = WorkTileRestoreProcessor()
    @Test
    fun restoreWithWorkTile_removeTracking() = runTest {
        val removeTracking by collectLastValue(underTest.removeTrackingForUser(UserHandle.of(USER)))
        runCurrent()

        val restoreData =
            RestoreData(
                restoredTiles = listOf(TILE_SPEC),
                restoredAutoAddedTiles = setOf(TILE_SPEC),
                USER,
            )

        underTest.postProcessRestore(restoreData)

        assertThat(removeTracking).isEqualTo(Unit)
    }

    @Test
    fun restoreWithWorkTile_otherUser_noRemoveTracking() = runTest {
        val removeTracking by
            collectLastValue(underTest.removeTrackingForUser(UserHandle.of(USER + 1)))
        runCurrent()

        val restoreData =
            RestoreData(
                restoredTiles = listOf(TILE_SPEC),
                restoredAutoAddedTiles = setOf(TILE_SPEC),
                USER,
            )

        underTest.postProcessRestore(restoreData)

        assertThat(removeTracking).isNull()
    }

    @Test
    fun restoreWithoutWorkTile_noSignal() = runTest {
        val removeTracking by collectLastValue(underTest.removeTrackingForUser(UserHandle.of(USER)))
        runCurrent()

        val restoreData =
            RestoreData(
                restoredTiles = emptyList(),
                restoredAutoAddedTiles = emptySet(),
                USER,
            )

        underTest.postProcessRestore(restoreData)

        assertThat(removeTracking).isNull()
    }

    companion object {
        private const val USER = 10
        private val TILE_SPEC = TileSpec.Companion.create("work")
    }
}
+82 −4
Original line number Diff line number Diff line
@@ -25,6 +25,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.data.model.RestoreProcessor
import com.android.systemui.qs.pipeline.data.model.workTileRestoreProcessor
import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
import com.android.systemui.qs.pipeline.shared.TileSpec
@@ -32,25 +37,28 @@ import com.android.systemui.qs.tiles.WorkModeTile
import com.android.systemui.settings.FakeUserTracker
import com.google.common.truth.Truth.assertThat
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.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class WorkTileAutoAddableTest : SysuiTestCase() {

    private val kosmos = Kosmos()

    private val restoreProcessor: RestoreProcessor
        get() = kosmos.workTileRestoreProcessor

    private lateinit var userTracker: FakeUserTracker

    private lateinit var underTest: WorkTileAutoAddable

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)

        userTracker =
            FakeUserTracker(
                _userId = USER_INFO_0.id,
@@ -58,7 +66,7 @@ class WorkTileAutoAddableTest : SysuiTestCase() {
                _userProfiles = listOf(USER_INFO_0)
            )

        underTest = WorkTileAutoAddable(userTracker)
        underTest = WorkTileAutoAddable(userTracker, kosmos.workTileRestoreProcessor)
    }

    @Test
@@ -114,10 +122,80 @@ class WorkTileAutoAddableTest : SysuiTestCase() {
        assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Always)
    }

    @Test
    fun restoreDataWithWorkTile_noCurrentManagedProfile_triggersRemove() = runTest {
        val userId = 0
        val signal by collectLastValue(underTest.autoAddSignal(userId))
        runCurrent()

        val restoreData = createRestoreWithWorkTile(userId)

        restoreProcessor.postProcessRestore(restoreData)

        assertThat(signal!!).isEqualTo(AutoAddSignal.RemoveTracking(SPEC))
    }

    @Test
    fun restoreDataWithWorkTile_currentlyManagedProfile_doesntTriggerRemove() = runTest {
        userTracker.set(listOf(USER_INFO_0, USER_INFO_WORK), selectedUserIndex = 0)
        val userId = 0
        val signals by collectValues(underTest.autoAddSignal(userId))
        runCurrent()

        val restoreData = createRestoreWithWorkTile(userId)

        restoreProcessor.postProcessRestore(restoreData)

        assertThat(signals).doesNotContain(AutoAddSignal.RemoveTracking(SPEC))
    }

    @Test
    fun restoreDataWithoutWorkTile_noManagedProfile_doesntTriggerRemove() = runTest {
        val userId = 0
        val signals by collectValues(underTest.autoAddSignal(userId))
        runCurrent()

        val restoreData = createRestoreWithoutWorkTile(userId)

        restoreProcessor.postProcessRestore(restoreData)

        assertThat(signals).doesNotContain(AutoAddSignal.RemoveTracking(SPEC))
    }

    @Test
    fun restoreDataWithoutWorkTile_managedProfile_doesntTriggerRemove() = runTest {
        userTracker.set(listOf(USER_INFO_0, USER_INFO_WORK), selectedUserIndex = 0)
        val userId = 0
        val signals by collectValues(underTest.autoAddSignal(userId))
        runCurrent()

        val restoreData = createRestoreWithoutWorkTile(userId)

        restoreProcessor.postProcessRestore(restoreData)

        assertThat(signals).doesNotContain(AutoAddSignal.RemoveTracking(SPEC))
    }

    companion object {
        private val SPEC = TileSpec.create(WorkModeTile.TILE_SPEC)
        private val USER_INFO_0 = UserInfo(0, "", FLAG_PRIMARY or FLAG_FULL)
        private val USER_INFO_1 = UserInfo(1, "", FLAG_FULL)
        private val USER_INFO_WORK = UserInfo(10, "", FLAG_PROFILE or FLAG_MANAGED_PROFILE)

        private fun createRestoreWithWorkTile(userId: Int): RestoreData {
            return RestoreData(
                listOf(TileSpec.create("a"), SPEC, TileSpec.create("b")),
                setOf(SPEC),
                userId,
            )
        }

        private fun createRestoreWithoutWorkTile(userId: Int): RestoreData {
            return RestoreData(
                listOf(TileSpec.create("a"), TileSpec.create("b")),
                emptySet(),
                userId,
            )
        }
    }
}
+16 −0
Original line number Diff line number Diff line
@@ -183,6 +183,22 @@ class AutoAddInteractorTest : SysuiTestCase() {
            assertThat(autoAddedTiles).contains(SPEC)
        }

    @Test
    fun autoAddable_removeTrackingSignal_notRemovedButUnmarked() =
        testScope.runTest {
            autoAddRepository.markTileAdded(USER, SPEC)
            val autoAddedTiles by collectLastValue(autoAddRepository.autoAddedTiles(USER))
            val fakeAutoAddable = FakeAutoAddable(SPEC, AutoAddTracking.Always)

            underTest = createInteractor(setOf(fakeAutoAddable))

            fakeAutoAddable.sendRemoveTrackingSignal(USER)
            runCurrent()

            verify(currentTilesInteractor, never()).removeTiles(any())
            assertThat(autoAddedTiles).doesNotContain(SPEC)
        }

    private fun createInteractor(autoAddables: Set<AutoAddable>): AutoAddInteractor {
        return AutoAddInteractor(
                autoAddables,
+49 −3
Original line number Diff line number Diff line
@@ -5,10 +5,15 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.data.model.RestoreProcessor
import com.android.systemui.qs.pipeline.data.repository.FakeAutoAddRepository
import com.android.systemui.qs.pipeline.data.repository.FakeQSSettingsRestoredRepository
import com.android.systemui.qs.pipeline.data.repository.FakeTileSpecRepository
import com.android.systemui.qs.pipeline.data.repository.TilesSettingConverter
import com.android.systemui.qs.pipeline.domain.interactor.RestoreReconciliationInteractorTest.TestableRestoreProcessor.Companion.POSTPROCESS
import com.android.systemui.qs.pipeline.domain.interactor.RestoreReconciliationInteractorTest.TestableRestoreProcessor.Companion.PREPROCESS
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -17,7 +22,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
import org.mockito.Mockito.inOrder

@RunWith(AndroidJUnit4::class)
@SmallTest
@@ -28,6 +33,9 @@ class RestoreReconciliationInteractorTest : SysuiTestCase() {

    private val qsSettingsRestoredRepository = FakeQSSettingsRestoredRepository()

    private val restoreProcessor: TestableRestoreProcessor = TestableRestoreProcessor()
    private val qsLogger: QSPipelineLogger = mock()

    private lateinit var underTest: RestoreReconciliationInteractor

    private val testDispatcher = StandardTestDispatcher()
@@ -35,13 +43,13 @@ class RestoreReconciliationInteractorTest : SysuiTestCase() {

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        underTest =
            RestoreReconciliationInteractor(
                tileSpecRepository,
                autoAddRepository,
                qsSettingsRestoredRepository,
                setOf(restoreProcessor),
                qsLogger,
                testScope.backgroundScope,
                testDispatcher
            )
@@ -85,6 +93,44 @@ class RestoreReconciliationInteractorTest : SysuiTestCase() {
            assertThat(autoAdd).isEqualTo(expectedAutoAdd.toTilesSet())
        }

    @Test
    fun restoreProcessorsCalled() =
        testScope.runTest {
            val user = 10

            val restoredSpecs = "a,c,d,f"
            val restoredAutoAdded = "d,e"

            val restoreData =
                RestoreData(
                    restoredSpecs.toTilesList(),
                    restoredAutoAdded.toTilesSet(),
                    user,
                )

            qsSettingsRestoredRepository.onDataRestored(restoreData)
            runCurrent()

            assertThat(restoreProcessor.calls).containsExactly(PREPROCESS, POSTPROCESS).inOrder()
        }

    private class TestableRestoreProcessor : RestoreProcessor {
        val calls = mutableListOf<Any>()

        override suspend fun preProcessRestore(restoreData: RestoreData) {
            calls.add(PREPROCESS)
        }

        override suspend fun postProcessRestore(restoreData: RestoreData) {
            calls.add(POSTPROCESS)
        }

        companion object {
            val PREPROCESS = Any()
            val POSTPROCESS = Any()
        }
    }

    companion object {
        private fun String.toTilesList() = TilesSettingConverter.toTilesList(this)
        private fun String.toTilesSet() = TilesSettingConverter.toTilesSet(this)
+176 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.qs.pipeline.domain.interactor

import android.content.pm.UserInfo
import android.os.UserManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.android.systemui.Flags.FLAG_QS_NEW_PIPELINE
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.FakeQSFactory
import com.android.systemui.qs.pipeline.data.model.RestoreData
import com.android.systemui.qs.pipeline.data.repository.fakeRestoreRepository
import com.android.systemui.qs.pipeline.data.repository.fakeTileSpecRepository
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.qsTileFactory
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.settings.userTracker
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

/**
 * This integration test is for testing the solution to b/314781280. In particular, there are two
 * issues we want to verify after a restore of a device with a work profile and a work mode tile:
 * * When the work profile is re-enabled in the target device, it is auto-added.
 * * The tile is auto-added in the same position that it was in the restored device.
 */
@MediumTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class WorkProfileAutoAddedAfterRestoreTest : SysuiTestCase() {

    private val kosmos = Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) }
    // Getter here so it can change when there is a managed profile.
    private val workTileAvailable: Boolean
        get() = hasManagedProfile()
    private val currentUser: Int
        get() = kosmos.userTracker.userId

    private val testScope: TestScope
        get() = kosmos.testScope

    @Before
    fun setUp() {
        mSetFlagsRule.enableFlags(FLAG_QS_NEW_PIPELINE)

        kosmos.qsTileFactory = FakeQSFactory(::tileCreator)
        kosmos.restoreReconciliationInteractor.start()
        kosmos.autoAddInteractor.init(kosmos.currentTilesInteractor)
    }

    @Test
    fun workTileRestoredAndPreviouslyAutoAdded_notAvailable_willBeAutoaddedInCorrectPosition() =
        testScope.runTest {
            val tiles by collectLastValue(kosmos.currentTilesInteractor.currentTiles)

            // Set up
            val currentTiles = listOf("a".toTileSpec())
            kosmos.fakeTileSpecRepository.setTiles(currentUser, currentTiles)

            val restoredTiles =
                listOf(WORK_TILE_SPEC) + listOf("b", "c", "d").map { it.toTileSpec() }
            val restoredAutoAdded = setOf(WORK_TILE_SPEC)

            val restoreData = RestoreData(restoredTiles, restoredAutoAdded, currentUser)

            // WHEN we restore tiles that auto-added the WORK tile and it's not available (there
            // are no managed profiles)
            kosmos.fakeRestoreRepository.onDataRestored(restoreData)

            // THEN the work tile is not part of the current tiles
            assertThat(tiles!!).hasSize(3)
            assertThat(tiles!!.map { it.spec }).doesNotContain(WORK_TILE_SPEC)

            // WHEN we add a work profile
            createManagedProfileAndAdd()

            // THEN the work profile is added in the correct place
            assertThat(tiles!!.first().spec).isEqualTo(WORK_TILE_SPEC)
        }

    @Test
    fun workTileNotRestoredAndPreviouslyAutoAdded_wontBeAutoAddedWhenWorkProfileIsAdded() =
        testScope.runTest {
            val tiles by collectLastValue(kosmos.currentTilesInteractor.currentTiles)

            // Set up
            val currentTiles = listOf("a".toTileSpec())
            kosmos.fakeTileSpecRepository.setTiles(currentUser, currentTiles)
            runCurrent()

            val restoredTiles = listOf("b", "c", "d").map { it.toTileSpec() }
            val restoredAutoAdded = setOf(WORK_TILE_SPEC)

            val restoreData = RestoreData(restoredTiles, restoredAutoAdded, currentUser)

            // WHEN we restore tiles that auto-added the WORK tile
            kosmos.fakeRestoreRepository.onDataRestored(restoreData)

            // THEN the work tile is not part of the current tiles
            assertThat(tiles!!).hasSize(3)
            assertThat(tiles!!.map { it.spec }).doesNotContain(WORK_TILE_SPEC)

            // WHEN we add a work profile
            createManagedProfileAndAdd()

            // THEN the work profile is not added because the user had manually removed it in the
            // past
            assertThat(tiles!!.map { it.spec }).doesNotContain(WORK_TILE_SPEC)
        }

    private fun tileCreator(spec: String): QSTile {
        return if (spec == WORK_TILE_SPEC.spec) {
            FakeQSTile(currentUser, workTileAvailable)
        } else {
            FakeQSTile(currentUser)
        }
    }

    private fun hasManagedProfile(): Boolean {
        return kosmos.userTracker.userProfiles.any { it.isManagedProfile }
    }

    private fun TestScope.createManagedProfileAndAdd() {
        kosmos.fakeUserTracker.set(
            listOf(USER_0_INFO, MANAGED_USER_INFO),
            0,
        )
        runCurrent()
    }

    private companion object {
        val WORK_TILE_SPEC = "work".toTileSpec()
        val USER_0_INFO =
            UserInfo(
                0,
                "zero",
                "",
                UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL,
            )
        val MANAGED_USER_INFO =
            UserInfo(
                10,
                "ten-managed",
                "",
                0,
                UserManager.USER_TYPE_PROFILE_MANAGED,
            )

        fun String.toTileSpec() = TileSpec.create(this)
    }
}
Loading