[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)
+}