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

Commit edf33ae6 authored by Ale Nijamkin's avatar Ale Nijamkin
Browse files

[flexiglass] DataStoreWrapper

A reusable utility to create wrapper objects of Jetpack DataStore in a
way that they can be used in unit tests.

DataStore creates files and basically forces developers to rely on on-device,
instrumented tests only. This wrapper fixes it by skipping the file creation
part, allowing everyone to use DataStore and have it supported in
deviceless tests.

The problem is that there's no way to implement a fake that depends on
the actual DataStore type because its APIs (Preferences and
Preferences.Key) have internal constructors and use type inference to
know what value should be associated with each key.

By using a Map to avoid Preferences, we circumvent the former problem.

By limiting the wrapper to String values, we circumvent the latter
problem.

This is used in the next CL.

Bug: 406213664
Test: tested in the next CL
Flag: com.android.systemui.scene_container
Change-Id: I7725f861c5c06dd6ccd028e4703363bc1505866b
parent c04b1027
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.common.data

import com.android.systemui.common.data.datastore.DataStoreWrapperFactory
import com.android.systemui.common.data.datastore.DataStoreWrapperFactoryImpl
import com.android.systemui.common.data.repository.BatteryRepository
import com.android.systemui.common.data.repository.BatteryRepositoryImpl
import com.android.systemui.common.data.repository.PackageChangeRepository
@@ -31,4 +33,9 @@ abstract class CommonDataLayerModule {
    ): PackageChangeRepository

    @Binds abstract fun bindBatteryRepository(impl: BatteryRepositoryImpl): BatteryRepository

    @Binds
    abstract fun bindDataStoreWrapperFactory(
        impl: DataStoreWrapperFactoryImpl
    ): DataStoreWrapperFactory
}
+54 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.common.data.datastore

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

/**
 * Wraps an instance of Jetpack [DataStore]; intended to make unit testing possible.
 *
 * The sacrifice made is that all values must be [String] (but that's okay, you can convert anything
 * to/from `String`).
 */
interface DataStoreWrapper {
    val data: Flow<Map<String, String>>

    suspend fun edit(block: (MutableMap<String, String>) -> Unit)
}

class DataStoreWrapperImpl(private val dataStore: DataStore<Preferences>) : DataStoreWrapper {

    override val data: Flow<Map<String, String>> = dataStore.data.map { it.toStringMap() }

    override suspend fun edit(block: (MutableMap<String, String>) -> Unit) {
        dataStore.edit { prefs ->
            val mutableMap = prefs.toStringMap().toMutableMap()
            block(mutableMap)
            prefs.clear()
            mutableMap.forEach { (name, value) -> prefs[stringPreferencesKey(name)] = value }
        }
    }

    private fun Preferences.toStringMap(): Map<String, String> {
        return this.asMap().map { (key, value) -> key.name to value.toString() }.toMap()
    }
}
+51 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.common.data.datastore

import android.annotation.UserIdInt
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.emptyPreferences
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.settings.UserFileManager
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope

interface DataStoreWrapperFactory {
    fun create(dataStoreFileName: String, @UserIdInt userId: Int): DataStoreWrapper
}

class DataStoreWrapperFactoryImpl
@Inject
constructor(
    @Background private val backgroundScope: CoroutineScope,
    private val userFileManager: UserFileManager,
) : DataStoreWrapperFactory {

    override fun create(dataStoreFileName: String, userId: Int): DataStoreWrapper {
        return DataStoreWrapperImpl(
            dataStore =
                PreferenceDataStoreFactory.create(
                    corruptionHandler =
                        ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
                    scope = backgroundScope,
                ) {
                    userFileManager.getFile(dataStoreFileName, userId)
                }
        )
    }
}
+30 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.common.data.datastore

import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture

val Kosmos.dataStoreWrapperFactory by Fixture {
    object : DataStoreWrapperFactory {
        private val cache = mutableMapOf<Int, DataStoreWrapper>()

        override fun create(dataStoreFileName: String, userId: Int): DataStoreWrapper {
            return cache.getOrPut(userId) { FakeDataStoreWrapperImpl() }
        }
    }
}
+32 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.common.data.datastore

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow

class FakeDataStoreWrapperImpl : DataStoreWrapper {

    private val _data = MutableStateFlow(emptyMap<String, String>())
    override val data: Flow<Map<String, String>> = _data

    override suspend fun edit(block: (MutableMap<String, String>) -> Unit) {
        val mutableMap = _data.value.toMutableMap()
        block(mutableMap)
        _data.value = mutableMap.toMap()
    }
}