Merge "Adds Retail mode repository" into udc-dev

This commit is contained in:
Fabian Kozynski
2023-05-03 13:26:01 +00:00
committed by Android (Google) Code Review
12 changed files with 420 additions and 2 deletions

View File

@@ -69,6 +69,7 @@ import com.android.systemui.qs.FgsManagerControllerImpl;
import com.android.systemui.qs.QSFragmentStartableModule;
import com.android.systemui.qs.footer.dagger.FooterActionsModule;
import com.android.systemui.recents.Recents;
import com.android.systemui.retail.dagger.RetailModeModule;
import com.android.systemui.screenrecord.ScreenRecordModule;
import com.android.systemui.screenshot.dagger.ScreenshotModule;
import com.android.systemui.security.data.repository.SecurityRepositoryModule;
@@ -179,6 +180,7 @@ import javax.inject.Named;
PrivacyModule.class,
QRCodeScannerModule.class,
QSFragmentStartableModule.class,
RetailModeModule.class,
ScreenshotModule.class,
SensorModule.class,
SecurityRepositoryModule.class,

View File

@@ -27,6 +27,7 @@ import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.qs.dagger.QSScope;
import com.android.systemui.retail.domain.interactor.RetailModeInteractor;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.util.ViewController;
@@ -45,18 +46,22 @@ public class QSFooterViewController extends ViewController<QSFooterView> impleme
private final View mEditButton;
private final FalsingManager mFalsingManager;
private final ActivityStarter mActivityStarter;
private final RetailModeInteractor mRetailModeInteractor;
@Inject
QSFooterViewController(QSFooterView view,
UserTracker userTracker,
FalsingManager falsingManager,
ActivityStarter activityStarter,
QSPanelController qsPanelController) {
QSPanelController qsPanelController,
RetailModeInteractor retailModeInteractor
) {
super(view);
mUserTracker = userTracker;
mQsPanelController = qsPanelController;
mFalsingManager = falsingManager;
mActivityStarter = activityStarter;
mRetailModeInteractor = retailModeInteractor;
mBuildText = mView.findViewById(R.id.build);
mPageIndicator = mView.findViewById(R.id.footer_page_indicator);
@@ -96,6 +101,8 @@ public class QSFooterViewController extends ViewController<QSFooterView> impleme
@Override
public void setVisibility(int visibility) {
mView.setVisibility(visibility);
mEditButton
.setVisibility(mRetailModeInteractor.isInRetailMode() ? View.GONE : View.VISIBLE);
mEditButton.setClickable(visibility == View.VISIBLE);
}

View File

@@ -20,6 +20,7 @@ import android.annotation.UserIdInt
import android.content.res.Resources
import android.database.ContentObserver
import android.provider.Settings
import com.android.systemui.R
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
@@ -27,12 +28,16 @@ import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.retail.data.repository.RetailModeRepository
import com.android.systemui.util.settings.SecureSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@@ -84,6 +89,9 @@ interface TileSpecRepository {
* [Settings.Secure.QS_TILES].
*
* All operations against [Settings] will be performed in a background thread.
*
* If the device is in retail mode, the tiles are fixed to the value of
* [R.string.quick_settings_tiles_retail_mode].
*/
@SysUISingleton
class TileSpecSettingsRepository
@@ -92,9 +100,31 @@ constructor(
private val secureSettings: SecureSettings,
@Main private val resources: Resources,
private val logger: QSPipelineLogger,
private val retailModeRepository: RetailModeRepository,
@Background private val backgroundDispatcher: CoroutineDispatcher,
) : TileSpecRepository {
private val retailModeTiles by lazy {
resources
.getString(R.string.quick_settings_tiles_retail_mode)
.split(DELIMITER)
.map(TileSpec::create)
.filter { it !is TileSpec.Invalid }
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun tilesSpecs(userId: Int): Flow<List<TileSpec>> {
return retailModeRepository.retailMode.flatMapLatest { inRetailMode ->
if (inRetailMode) {
logger.logUsingRetailTiles()
flowOf(retailModeTiles)
} else {
settingsTiles(userId)
}
}
}
private fun settingsTiles(userId: Int): Flow<List<TileSpec>> {
return conflatedCallbackFlow {
val observer =
object : ContentObserver(null) {
@@ -157,6 +187,10 @@ constructor(
}
private suspend fun storeTiles(@UserIdInt forUser: Int, tiles: List<TileSpec>) {
if (retailModeRepository.inRetailMode) {
// No storing tiles when in retail mode
return
}
val toStore =
tiles
.filter { it !is TileSpec.Invalid }

View File

@@ -120,6 +120,10 @@ constructor(
)
}
fun logUsingRetailTiles() {
tileListLogBuffer.log(TILE_LIST_TAG, LogLevel.DEBUG, {}, { "Using retail tiles" })
}
/** Reasons for destroying an existing tile. */
enum class TileDestroyedReason(val readable: String) {
TILE_REMOVED("Tile removed from current set"),

View File

@@ -0,0 +1,34 @@
/*
* 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.retail.dagger
import com.android.systemui.retail.data.repository.RetailModeRepository
import com.android.systemui.retail.data.repository.RetailModeSettingsRepository
import com.android.systemui.retail.domain.interactor.RetailModeInteractor
import com.android.systemui.retail.domain.interactor.RetailModeInteractorImpl
import dagger.Binds
import dagger.Module
@Module
abstract class RetailModeModule {
@Binds
abstract fun bindsRetailModeRepository(impl: RetailModeSettingsRepository): RetailModeRepository
@Binds
abstract fun bindsRetailModeInteractor(impl: RetailModeInteractorImpl): RetailModeInteractor
}

View File

@@ -0,0 +1,81 @@
/*
* 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.retail.data.repository
import android.database.ContentObserver
import android.provider.Settings
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.dagger.qualifiers.Background
import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
/** Repository to track if the device is in Retail mode */
interface RetailModeRepository {
/** Flow of whether the device is currently in retail mode. */
val retailMode: StateFlow<Boolean>
/** Last value of whether the device is in retail mode. */
val inRetailMode: Boolean
get() = retailMode.value
}
/**
* Tracks [Settings.Global.DEVICE_DEMO_MODE].
*
* @see UserManager.isDeviceInDemoMode
*/
@SysUISingleton
class RetailModeSettingsRepository
@Inject
constructor(
globalSettings: GlobalSettings,
@Background backgroundDispatcher: CoroutineDispatcher,
@Application scope: CoroutineScope,
) : RetailModeRepository {
override val retailMode =
conflatedCallbackFlow {
val observer =
object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
trySend(Unit)
}
}
globalSettings.registerContentObserver(RETAIL_MODE_SETTING, observer)
awaitClose { globalSettings.unregisterContentObserver(observer) }
}
.onStart { emit(Unit) }
.map { globalSettings.getInt(RETAIL_MODE_SETTING, 0) != 0 }
.flowOn(backgroundDispatcher)
.stateIn(scope, SharingStarted.Eagerly, false)
companion object {
private const val RETAIL_MODE_SETTING = Settings.Global.DEVICE_DEMO_MODE
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.retail.domain.interactor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.retail.data.repository.RetailModeRepository
import javax.inject.Inject
/** Interactor to determine if the device is currently in retail mode */
interface RetailModeInteractor {
/** Whether the device is currently in retail mode */
val isInRetailMode: Boolean
}
@SysUISingleton
class RetailModeInteractorImpl
@Inject
constructor(
private val repository: RetailModeRepository,
) : RetailModeInteractor {
override val isInRetailMode: Boolean
get() = repository.inRetailMode
}

View File

@@ -37,6 +37,8 @@ import androidx.test.filters.SmallTest;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.retail.data.repository.FakeRetailModeRepository;
import com.android.systemui.retail.domain.interactor.RetailModeInteractorImpl;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.utils.leaks.LeakCheckedTest;
@@ -67,6 +69,8 @@ public class QSFooterViewControllerTest extends LeakCheckedTest {
@Mock
private ActivityStarter mActivityStarter;
private FakeRetailModeRepository mRetailModeRepository;
private QSFooterViewController mController;
private View mEditButton;
@@ -74,6 +78,9 @@ public class QSFooterViewControllerTest extends LeakCheckedTest {
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
mRetailModeRepository = new FakeRetailModeRepository();
mRetailModeRepository.setRetailMode(false);
mEditButton = new View(mContext);
injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
@@ -89,7 +96,8 @@ public class QSFooterViewControllerTest extends LeakCheckedTest {
when(mView.findViewById(android.R.id.edit)).thenReturn(mEditButton);
mController = new QSFooterViewController(mView, mUserTracker, mFalsingManager,
mActivityStarter, mQSPanelController);
mActivityStarter, mQSPanelController,
new RetailModeInteractorImpl(mRetailModeRepository));
mController.init();
}
@@ -132,4 +140,20 @@ public class QSFooterViewControllerTest extends LeakCheckedTest {
captor.getValue().run();
verify(mQSPanelController).showEdit(mEditButton);
}
@Test
public void testEditButton_notRetailMode_visible() {
mRetailModeRepository.setRetailMode(false);
mController.setVisibility(View.VISIBLE);
assertThat(mEditButton.getVisibility()).isEqualTo(View.VISIBLE);
}
@Test
public void testEditButton_retailMode_notVisible() {
mRetailModeRepository.setRetailMode(true);
mController.setVisibility(View.VISIBLE);
assertThat(mEditButton.getVisibility()).isEqualTo(View.GONE);
}
}

View File

@@ -25,6 +25,7 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger
import com.android.systemui.retail.data.repository.FakeRetailModeRepository
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -44,6 +45,7 @@ import org.mockito.MockitoAnnotations
class TileSpecSettingsRepositoryTest : SysuiTestCase() {
private lateinit var secureSettings: FakeSettings
private lateinit var retailModeRepository: FakeRetailModeRepository
@Mock private lateinit var logger: QSPipelineLogger
@@ -57,9 +59,12 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
MockitoAnnotations.initMocks(this)
secureSettings = FakeSettings()
retailModeRepository = FakeRetailModeRepository()
retailModeRepository.setRetailMode(false)
with(context.orCreateTestableResources) {
addOverride(R.string.quick_settings_tiles_default, DEFAULT_TILES)
addOverride(R.string.quick_settings_tiles_retail_mode, RETAIL_TILES)
}
underTest =
@@ -67,6 +72,7 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
secureSettings,
context.resources,
logger,
retailModeRepository,
testDispatcher,
)
}
@@ -346,6 +352,26 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
assertThat(tiles).isEqualTo("b".toTileSpecs())
}
@Test
fun retailMode_usesRetailTiles() =
testScope.runTest {
retailModeRepository.setRetailMode(true)
val tiles by collectLastValue(underTest.tilesSpecs(0))
assertThat(tiles).isEqualTo(RETAIL_TILES.toTileSpecs())
}
@Test
fun retailMode_cannotModifyTiles() =
testScope.runTest {
retailModeRepository.setRetailMode(true)
underTest.setTiles(0, DEFAULT_TILES.toTileSpecs())
assertThat(loadTilesForUser(0)).isNull()
}
private fun getDefaultTileSpecs(): List<TileSpec> {
return QSHost.getDefaultSpecs(context.resources).map(TileSpec::create)
}
@@ -360,6 +386,7 @@ class TileSpecSettingsRepositoryTest : SysuiTestCase() {
companion object {
private const val DEFAULT_TILES = "a,b,c"
private const val RETAIL_TILES = "d"
private const val SETTING = Settings.Secure.QS_TILES
private fun String.toTileSpecs(): List<TileSpec> {

View File

@@ -0,0 +1,84 @@
/*
* 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.retail.data.repository
import android.provider.Settings
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidTestingRunner::class)
class RetailModeSettingsRepositoryTest : SysuiTestCase() {
private val globalSettings = FakeSettings()
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
private val underTest =
RetailModeSettingsRepository(
globalSettings,
backgroundDispatcher = testDispatcher,
scope = testScope.backgroundScope,
)
@Test
fun retailMode_defaultFalse() =
testScope.runTest {
val value by collectLastValue(underTest.retailMode)
assertThat(value).isFalse()
assertThat(underTest.inRetailMode).isFalse()
}
@Test
fun retailMode_false() =
testScope.runTest {
val value by collectLastValue(underTest.retailMode)
globalSettings.putInt(SETTING, 0)
assertThat(value).isFalse()
assertThat(underTest.inRetailMode).isFalse()
}
@Test
fun retailMode_true() =
testScope.runTest {
val value by collectLastValue(underTest.retailMode)
globalSettings.putInt(SETTING, 1)
assertThat(value).isTrue()
assertThat(underTest.inRetailMode).isTrue()
}
companion object {
private const val SETTING = Settings.Global.DEVICE_DEMO_MODE
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.retail.domain.interactor
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.retail.data.repository.FakeRetailModeRepository
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
class RetailModeInteractorImplTest : SysuiTestCase() {
private val retailModeRepository = FakeRetailModeRepository()
private val underTest = RetailModeInteractorImpl(retailModeRepository)
@Test
fun retailMode_false() {
retailModeRepository.setRetailMode(false)
assertThat(underTest.isInRetailMode).isFalse()
}
@Test
fun retailMode_true() {
retailModeRepository.setRetailMode(true)
assertThat(underTest.isInRetailMode).isTrue()
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.retail.data.repository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeRetailModeRepository : RetailModeRepository {
private val _retailMode = MutableStateFlow(false)
override val retailMode: StateFlow<Boolean> = _retailMode.asStateFlow()
private var _retailModeValue = false
override val inRetailMode: Boolean
get() = _retailModeValue
fun setRetailMode(value: Boolean) {
_retailMode.value = value
_retailModeValue = value
}
}