[Contextual Edu] Simple implementation of repository
Add repository for basic use case
Test: ContextualEducationRepositoryTest
Bug: 317496783
Flag: com.android.systemui.keyboard_touchpad_contextual_education
Change-Id: I80e112328afec23c220cfe1b374aed16513cd73c
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index bd7c9a0..8f0597e 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -552,6 +552,7 @@
"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 @@
"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 0000000..4a5342a
--- /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 0000000..9a5c77a
--- /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 a7ff3c3..4803b4f 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.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 @@
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 0000000..e2bcb6b
--- /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 0000000..af35e8c
--- /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 0000000..c9dd833
--- /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 0000000..229511a
--- /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)
+}