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

Commit 50e888e1 authored by Lucas Silva's avatar Lucas Silva Committed by Android (Google) Code Review
Browse files

Merge "Add bottom sheet describing lock screen widgets" into main

parents e132fe67 735cff3b
Loading
Loading
Loading
Loading
+74 −1
Original line number Diff line number Diff line
@@ -52,10 +52,12 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
@@ -75,12 +77,15 @@ import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonColors
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
@@ -153,7 +158,7 @@ import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SystemUIDialogFactory
import kotlinx.coroutines.launch

@OptIn(ExperimentalComposeUiApi::class)
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
@Composable
fun CommunalHub(
    modifier: Modifier = Modifier,
@@ -378,6 +383,33 @@ fun CommunalHub(
                onCancel = viewModel::onEnableWorkProfileDialogCancel
            )
        }

        if (viewModel is CommunalEditModeViewModel) {
            val showBottomSheet by viewModel.showDisclaimer.collectAsStateWithLifecycle(false)

            if (showBottomSheet) {
                val scope = rememberCoroutineScope()
                val sheetState = rememberModalBottomSheetState()
                val colors = LocalAndroidColorScheme.current

                ModalBottomSheet(
                    onDismissRequest = viewModel::onDisclaimerDismissed,
                    sheetState = sheetState,
                    dragHandle = null,
                    containerColor = colors.surfaceContainer,
                ) {
                    DisclaimerBottomSheetContent {
                        scope
                            .launch { sheetState.hide() }
                            .invokeOnCompletion {
                                if (!sheetState.isVisible) {
                                    viewModel.onDisclaimerDismissed()
                                }
                            }
                    }
                }
            }
        }
    }
}

@@ -389,6 +421,47 @@ private fun onMotionEvent(viewModel: BaseCommunalViewModel) {
    viewModel.signalUserInteraction()
}

@Composable
private fun DisclaimerBottomSheetContent(onButtonClicked: () -> Unit) {
    val colors = LocalAndroidColorScheme.current

    Column(
        modifier = Modifier.fillMaxWidth().padding(horizontal = 32.dp, vertical = 24.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Icon(
            imageVector = Icons.Outlined.Widgets,
            contentDescription = null,
            tint = colors.primary,
            modifier = Modifier.size(32.dp)
        )
        Spacer(modifier = Modifier.height(16.dp))
        Text(
            text = stringResource(R.string.communal_widgets_disclaimer_title),
            style = MaterialTheme.typography.headlineMedium,
            color = colors.onSurface,
        )
        Spacer(modifier = Modifier.height(16.dp))
        Text(
            text = stringResource(R.string.communal_widgets_disclaimer_text),
            color = colors.onSurfaceVariant,
        )
        Button(
            modifier =
                Modifier.padding(horizontal = 26.dp, vertical = 16.dp)
                    .widthIn(min = 200.dp)
                    .heightIn(min = 56.dp),
            onClick = { onButtonClicked() }
        ) {
            Text(
                stringResource(R.string.communal_widgets_disclaimer_button),
                style = MaterialTheme.typography.labelLarge,
            )
        }
    }
}

/**
 * Observes communal content and scrolls to any added or updated live content, e.g. a new media
 * session is started, or a paused timer is resumed.
+39 −96
Original line number Diff line number Diff line
@@ -18,8 +18,8 @@ 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 android.content.pm.UserInfo.FLAG_MAIN
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -30,108 +30,87 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.fakeUserFileManager
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.FakeUserRepository
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
import org.mockito.kotlin.spy

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class CommunalPrefsRepositoryImplTest : SysuiTestCase() {
    @Mock private lateinit var tableLogBuffer: TableLogBuffer

    private lateinit var underTest: CommunalPrefsRepositoryImpl

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private lateinit var userRepository: FakeUserRepository
    private lateinit var userFileManager: UserFileManager

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
    private val userFileManager: UserFileManager = spy(kosmos.fakeUserFileManager)

        userRepository = kosmos.fakeUserRepository
        userRepository.setUserInfos(USER_INFOS)

        userFileManager =
            FakeUserFileManager(
                mapOf(
                    USER_INFOS[0].id to FakeSharedPreferences(),
                    USER_INFOS[1].id to FakeSharedPreferences()
                )
    private val underTest: CommunalPrefsRepositoryImpl by lazy {
        CommunalPrefsRepositoryImpl(
            kosmos.testDispatcher,
            userFileManager,
            kosmos.broadcastDispatcher,
            logcatLogBuffer("CommunalPrefsRepositoryImplTest"),
        )
    }

    @Test
    fun isCtaDismissedValue_byDefault_isFalse() =
        testScope.runTest {
            underTest = createCommunalPrefsRepositoryImpl(userFileManager)
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed(MAIN_USER))
            assertThat(isCtaDismissed).isFalse()
        }

    @Test
    fun isCtaDismissedValue_onSet_isTrue() =
        testScope.runTest {
            underTest = createCommunalPrefsRepositoryImpl(userFileManager)
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed(MAIN_USER))

            underTest.setCtaDismissedForCurrentUser()
            underTest.setCtaDismissed(MAIN_USER)
            assertThat(isCtaDismissed).isTrue()
        }

    @Test
    fun isCtaDismissedValue_whenSwitchUser() =
    fun isCtaDismissedValue_onSetForDifferentUser_isStillFalse() =
        testScope.runTest {
            underTest = createCommunalPrefsRepositoryImpl(userFileManager)
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)
            underTest.setCtaDismissedForCurrentUser()
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed(MAIN_USER))

            // dismissed true for primary user
            assertThat(isCtaDismissed).isTrue()

            // switch to secondary user
            userRepository.setSelectedUserInfo(USER_INFOS[1])

            // dismissed is false for secondary user
            underTest.setCtaDismissed(SECONDARY_USER)
            assertThat(isCtaDismissed).isFalse()
        }

            // switch back to primary user
            userRepository.setSelectedUserInfo(USER_INFOS[0])
    @Test
    fun isDisclaimerDismissed_byDefault_isFalse() =
        testScope.runTest {
            val isDisclaimerDismissed by
                collectLastValue(underTest.isDisclaimerDismissed(MAIN_USER))
            assertThat(isDisclaimerDismissed).isFalse()
        }

            // dismissed is true for primary user
            assertThat(isCtaDismissed).isTrue()
    @Test
    fun isDisclaimerDismissed_onSet_isTrue() =
        testScope.runTest {
            val isDisclaimerDismissed by
                collectLastValue(underTest.isDisclaimerDismissed(MAIN_USER))

            underTest.setDisclaimerDismissed(MAIN_USER)
            assertThat(isDisclaimerDismissed).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])
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed(MAIN_USER))
            assertThat(isCtaDismissed).isFalse()
            clearInvocations(userFileManagerSpy)
            clearInvocations(userFileManager)

            // Received restore finished event.
            kosmos.broadcastDispatcher.sendIntentToMatchingReceiversOnly(
@@ -141,48 +120,12 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() {
            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 {
            throw UnsupportedOperationException()
        }

        override fun getSharedPreferences(
            fileName: String,
            mode: Int,
            userId: Int
        ): SharedPreferences {
            if (fileName != FILE_NAME) {
                throw IllegalArgumentException("Preference files must be $FILE_NAME")
            }
            return sharedPrefs.getValue(userId)
        }
            verify(userFileManager, atLeastOnce())
                .getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, MAIN_USER.id)
        }

    companion object {
        val USER_INFOS =
            listOf(
                UserInfo(/* id= */ 0, "zero", /* flags= */ 0),
                UserInfo(/* id= */ 1, "secondary", /* flags= */ 0),
            )
        val MAIN_USER = UserInfo(0, "main", FLAG_MAIN)
        val SECONDARY_USER = UserInfo(1, "secondary", 0)
    }
}
+9 −1
Original line number Diff line number Diff line
@@ -488,8 +488,16 @@ class CommunalInteractorTest : SysuiTestCase() {
    @Test
    fun ctaTile_afterDismiss_doesNotShow() =
        testScope.runTest {
            // Set to main user, so we can dismiss the tile for the main user.
            val user = userRepository.asMainUser()
            userTracker.set(
                userInfos = listOf(user),
                selectedUserIndex = 0,
            )
            runCurrent()

            tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
            communalPrefsRepository.setCtaDismissedForCurrentUser()
            communalPrefsRepository.setCtaDismissed(user)

            val ctaTileContent by collectLastValue(underTest.ctaTileContent)

+123 −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.interactor

import android.content.pm.UserInfo
import android.content.pm.UserInfo.FLAG_MAIN
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.kosmos.testScope
import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class CommunalPrefsInteractorTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope

    private val underTest by lazy { kosmos.communalPrefsInteractor }

    @Test
    fun setCtaDismissed_currentUser() =
        testScope.runTest {
            setSelectedUser(MAIN_USER)
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)

            assertThat(isCtaDismissed).isFalse()
            underTest.setCtaDismissed(MAIN_USER)
            assertThat(isCtaDismissed).isTrue()
        }

    @Test
    fun setCtaDismissed_anotherUser() =
        testScope.runTest {
            setSelectedUser(MAIN_USER)
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)

            assertThat(isCtaDismissed).isFalse()
            underTest.setCtaDismissed(SECONDARY_USER)
            assertThat(isCtaDismissed).isFalse()
        }

    @Test
    fun isCtaDismissed_userSwitch() =
        testScope.runTest {
            setSelectedUser(MAIN_USER)
            underTest.setCtaDismissed(MAIN_USER)
            val isCtaDismissed by collectLastValue(underTest.isCtaDismissed)

            assertThat(isCtaDismissed).isTrue()
            setSelectedUser(SECONDARY_USER)
            assertThat(isCtaDismissed).isFalse()
        }

    @Test
    fun setDisclaimerDismissed_currentUser() =
        testScope.runTest {
            setSelectedUser(MAIN_USER)
            val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)

            assertThat(isDisclaimerDismissed).isFalse()
            underTest.setDisclaimerDismissed(MAIN_USER)
            assertThat(isDisclaimerDismissed).isTrue()
        }

    @Test
    fun setDisclaimerDismissed_anotherUser() =
        testScope.runTest {
            setSelectedUser(MAIN_USER)
            val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)

            assertThat(isDisclaimerDismissed).isFalse()
            underTest.setDisclaimerDismissed(SECONDARY_USER)
            assertThat(isDisclaimerDismissed).isFalse()
        }

    @Test
    fun isDisclaimerDismissed_userSwitch() =
        testScope.runTest {
            setSelectedUser(MAIN_USER)
            underTest.setDisclaimerDismissed(MAIN_USER)
            val isDisclaimerDismissed by collectLastValue(underTest.isDisclaimerDismissed)

            assertThat(isDisclaimerDismissed).isTrue()
            setSelectedUser(SECONDARY_USER)
            assertThat(isDisclaimerDismissed).isFalse()
        }

    private suspend fun setSelectedUser(user: UserInfo) {
        with(kosmos.fakeUserRepository) {
            setUserInfos(listOf(user))
            setSelectedUserInfo(user)
        }
        kosmos.fakeUserTracker.set(userInfos = listOf(user), selectedUserIndex = 0)
    }

    private companion object {
        val MAIN_USER = UserInfo(0, "main", FLAG_MAIN)
        val SECONDARY_USER = UserInfo(1, "secondary", 0)
    }
}
+30 −0
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ import com.android.systemui.communal.data.repository.fakeCommunalTutorialReposit
import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.communalPrefsInteractor
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
@@ -48,6 +49,8 @@ import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.shared.model.EditModeState
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
@@ -57,6 +60,7 @@ import com.android.systemui.settings.fakeUserTracker
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -104,10 +108,12 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
        smartspaceRepository = kosmos.fakeSmartspaceRepository
        mediaRepository = kosmos.fakeCommunalMediaRepository
        communalSceneInteractor = kosmos.communalSceneInteractor
        kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
        kosmos.fakeUserTracker.set(
            userInfos = listOf(MAIN_USER_INFO),
            selectedUserIndex = 0,
        )
        kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
        whenever(providerInfo.profile).thenReturn(UserHandle(MAIN_USER_INFO.id))

        underTest =
@@ -120,6 +126,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
                uiEventLogger,
                logcatLogBuffer("CommunalEditModeViewModelTest"),
                kosmos.testDispatcher,
                kosmos.communalPrefsInteractor,
            )
    }

@@ -312,6 +319,29 @@ class CommunalEditModeViewModelTest : SysuiTestCase() {
        }
    }

    @Test
    fun showDisclaimer_trueAfterEditModeShowing() =
        testScope.runTest {
            val showDisclaimer by collectLastValue(underTest.showDisclaimer)

            assertThat(showDisclaimer).isFalse()
            underTest.setEditModeState(EditModeState.SHOWING)
            assertThat(showDisclaimer).isTrue()
        }

    @Test
    fun showDisclaimer_falseWhenDismissed() =
        testScope.runTest {
            underTest.setEditModeState(EditModeState.SHOWING)
            kosmos.fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)

            val showDisclaimer by collectLastValue(underTest.showDisclaimer)

            assertThat(showDisclaimer).isTrue()
            underTest.onDisclaimerDismissed()
            assertThat(showDisclaimer).isFalse()
        }

    private companion object {
        val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
        const val WIDGET_PICKER_PACKAGE_NAME = "widget_picker_package_name"
Loading