diff options
8 files changed, 343 insertions, 1 deletions
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index bd7c9a0e1e55..8f0597e04b0d 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -552,6 +552,7 @@ android_library { "androidx.exifinterface_exifinterface", "androidx.room_room-runtime", "androidx.room_room-ktx", + "androidx.datastore_datastore-preferences", "com.google.android.material_material", "device_state_flags_lib", "kotlinx_coroutines_android", @@ -705,6 +706,7 @@ android_library { "androidx.exifinterface_exifinterface", "androidx.room_room-runtime", "androidx.room_room-ktx", + "androidx.datastore_datastore-preferences", "device_state_flags_lib", "kotlinx-coroutines-android", "kotlinx-coroutines-core", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt new file mode 100644 index 000000000000..4a5342ac6ae1 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright 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.education.data.repository + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.SysuiTestableContext +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.shared.education.GestureType.BACK_GESTURE +import com.google.common.truth.Truth.assertThat +import java.io.File +import javax.inject.Provider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ContextualEducationRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: ContextualEducationRepository + private val kosmos = Kosmos() + private val testScope = kosmos.testScope + private val dsScopeProvider: Provider<CoroutineScope> = Provider { + TestScope(kosmos.testDispatcher).backgroundScope + } + private val testUserId = 1111 + + // For deleting any test files created after the test + @get:Rule val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() + + @Before + fun setUp() { + // Create TestContext here because TemporaryFolder.create() is called in @Before. It is + // needed before calling TemporaryFolder.newFolder(). + val testContext = TestContext(context, tmpFolder.newFolder()) + val userRepository = UserContextualEducationRepository(testContext, dsScopeProvider) + underTest = ContextualEducationRepository(userRepository) + underTest.setUser(testUserId) + } + + @Test + fun changeRetrievedValueForNewUser() = + testScope.runTest { + // Update data for old user. + underTest.incrementSignalCount(BACK_GESTURE) + val model by collectLastValue(underTest.readGestureEduModelFlow(BACK_GESTURE)) + assertThat(model?.signalCount).isEqualTo(1) + + // User is changed. + underTest.setUser(1112) + // Assert count is 0 after user is changed. + assertThat(model?.signalCount).isEqualTo(0) + } + + @Test + fun incrementSignalCount() = + testScope.runTest { + underTest.incrementSignalCount(BACK_GESTURE) + val model by collectLastValue(underTest.readGestureEduModelFlow(BACK_GESTURE)) + assertThat(model?.signalCount).isEqualTo(1) + } + + /** Test context which allows overriding getFilesDir path */ + private class TestContext(context: Context, private val folder: File) : + SysuiTestableContext(context) { + override fun getFilesDir(): File { + return folder + } + } +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt new file mode 100644 index 000000000000..9a5c77ac1679 --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/education/GestureType.kt @@ -0,0 +1,21 @@ +/* + * Copyright 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.shared.education + +enum class GestureType { + BACK_GESTURE, +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index a7ff3c36a641..4803b4f2e4b7 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -63,6 +63,7 @@ import com.android.systemui.display.DisplayModule; import com.android.systemui.doze.dagger.DozeComponent; import com.android.systemui.dreams.dagger.DreamModule; import com.android.systemui.dump.DumpManager; +import com.android.systemui.education.dagger.ContextualEducationModule; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.FlagDependenciesModule; import com.android.systemui.flags.FlagsModule; @@ -261,7 +262,8 @@ import javax.inject.Named; UserModule.class, UtilModule.class, NoteTaskModule.class, - WalletModule.class + WalletModule.class, + ContextualEducationModule.class }, subcomponents = { ComplicationComponent.class, diff --git a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt new file mode 100644 index 000000000000..e2bcb6bc2457 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt @@ -0,0 +1,40 @@ +/* + * Copyright 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.education.dagger + +import com.android.systemui.dagger.qualifiers.Background +import dagger.Module +import dagger.Provides +import javax.inject.Qualifier +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob + +@Module +interface ContextualEducationModule { + @Qualifier annotation class EduDataStoreScope + + companion object { + @EduDataStoreScope + @Provides + fun provideEduDataStoreScope( + @Background bgDispatcher: CoroutineDispatcher + ): CoroutineScope { + return CoroutineScope(bgDispatcher + SupervisorJob()) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt new file mode 100644 index 000000000000..af35e8c3662b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/data/model/GestureEduModel.kt @@ -0,0 +1,26 @@ +/* + * Copyright 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.education.data.model + +/** + * Model to store education data related to each gesture (e.g. Back, Home, All Apps, Overview). Each + * gesture stores its own model separately. + */ +data class GestureEduModel( + val signalCount: Int, + val educationShownCount: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt new file mode 100644 index 000000000000..c9dd833dac75 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/ContextualEducationRepository.kt @@ -0,0 +1,42 @@ +/* + * Copyright 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.education.data.repository + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shared.education.GestureType +import javax.inject.Inject + +/** + * Provide methods to read and update on field level and allow setting datastore when user is + * changed + */ +@SysUISingleton +class ContextualEducationRepository +@Inject +constructor(private val userEduRepository: UserContextualEducationRepository) { + /** To change data store when user is changed */ + fun setUser(userId: Int) = userEduRepository.setUser(userId) + + fun readGestureEduModelFlow(gestureType: GestureType) = + userEduRepository.readGestureEduModelFlow(gestureType) + + suspend fun incrementSignalCount(gestureType: GestureType) { + userEduRepository.updateGestureEduModel(gestureType) { + it.copy(signalCount = it.signalCount + 1) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt new file mode 100644 index 000000000000..229511a20caf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/education/data/repository/UserContextualEducationRepository.kt @@ -0,0 +1,114 @@ +/* + * Copyright 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.education.data.repository + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.preferencesDataStoreFile +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.education.dagger.ContextualEducationModule.EduDataStoreScope +import com.android.systemui.education.data.model.GestureEduModel +import com.android.systemui.shared.education.GestureType +import javax.inject.Inject +import javax.inject.Provider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map + +/** + * A contextual education repository to: + * 1) store education data per user + * 2) provide methods to read and update data on model-level + * 3) provide method to enable changing datastore when user is changed + */ +@SysUISingleton +class UserContextualEducationRepository +@Inject +constructor( + @Application private val applicationContext: Context, + @EduDataStoreScope private val dataStoreScopeProvider: Provider<CoroutineScope> +) { + companion object { + const val SIGNAL_COUNT_SUFFIX = "_SIGNAL_COUNT" + const val NUMBER_OF_EDU_SHOWN_SUFFIX = "_NUMBER_OF_EDU_SHOWN" + + const val DATASTORE_DIR = "education/USER%s_ContextualEducation" + } + + private var dataStoreScope: CoroutineScope? = null + + private val datastore = MutableStateFlow<DataStore<Preferences>?>(null) + + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) + private val prefData: Flow<Preferences> = datastore.filterNotNull().flatMapLatest { it.data } + + internal fun setUser(userId: Int) { + dataStoreScope?.cancel() + val newDsScope = dataStoreScopeProvider.get() + datastore.value = + PreferenceDataStoreFactory.create( + produceFile = { + applicationContext.preferencesDataStoreFile( + String.format(DATASTORE_DIR, userId) + ) + }, + scope = newDsScope, + ) + dataStoreScope = newDsScope + } + + internal fun readGestureEduModelFlow(gestureType: GestureType): Flow<GestureEduModel> = + prefData.map { preferences -> getGestureEduModel(gestureType, preferences) } + + private fun getGestureEduModel( + gestureType: GestureType, + preferences: Preferences + ): GestureEduModel { + return GestureEduModel( + signalCount = preferences[getSignalCountKey(gestureType)] ?: 0, + educationShownCount = preferences[getEducationShownCountKey(gestureType)] ?: 0, + ) + } + + internal suspend fun updateGestureEduModel( + gestureType: GestureType, + transform: (GestureEduModel) -> GestureEduModel + ) { + datastore.filterNotNull().first().edit { preferences -> + val currentModel = getGestureEduModel(gestureType, preferences) + val updatedModel = transform(currentModel) + preferences[getSignalCountKey(gestureType)] = updatedModel.signalCount + preferences[getEducationShownCountKey(gestureType)] = updatedModel.educationShownCount + } + } + + private fun getSignalCountKey(gestureType: GestureType): Preferences.Key<Int> = + intPreferencesKey(gestureType.name + SIGNAL_COUNT_SUFFIX) + + private fun getEducationShownCountKey(gestureType: GestureType): Preferences.Key<Int> = + intPreferencesKey(gestureType.name + NUMBER_OF_EDU_SHOWN_SUFFIX) +} |