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

Commit 90d4a5a5 authored by Chaohui Wang's avatar Chaohui Wang Committed by Android (Google) Code Review
Browse files

Merge changes from topic "AppOpPermissionAppListTest"

* changes:
  Refactor TogglePermissionAppInfoPageProvider
  Hide not changeable app from AppOpPermissionAppList
  Add tests for Flows & StateFlowBridge
parents 941c58de 7459ecff
Loading
Loading
Loading
Loading
+7 −32
Original line number Diff line number Diff line
@@ -16,15 +16,10 @@

package com.android.settingslib.spa.framework.util

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.take

/**
 * Returns a [Flow] whose values are a list which containing the results of applying the given
@@ -41,33 +36,13 @@ inline fun <T, R> Flow<List<T>>.asyncMapItem(crossinline transform: (T) -> R): F
    map { list -> list.asyncMap(transform) }

/**
 * Delays the flow a little bit, wait the other flow's first value.
 * Returns a [Flow] whose values are a list containing only elements matching the given [predicate].
 */
fun <T1, T2> Flow<T1>.waitFirst(otherFlow: Flow<T2>): Flow<T1> =
    combine(otherFlow.distinctUntilChangedBy {}) { value, _ -> value }
inline fun <T> Flow<List<T>>.filterItem(crossinline predicate: (T) -> Boolean): Flow<List<T>> =
    map { list -> list.filter(predicate) }

/**
 * Returns a [Flow] whose values are generated list by combining the most recently emitted non null
 * values by each flow.
 * Delays the flow a little bit, wait the other flow's first value.
 */
inline fun <reified T : Any> combineToList(vararg flows: Flow<T?>): Flow<List<T>> = combine(
    flows.asList(),
) { array: Array<T?> -> array.filterNotNull() }

class StateFlowBridge<T> {
    private val stateFlow = MutableStateFlow<T?>(null)
    val flow = stateFlow.filterNotNull()

    fun setIfAbsent(value: T) {
        if (stateFlow.value == null) {
            stateFlow.value = value
        }
    }

    @Composable
    fun Sync(state: State<T>) {
        LaunchedEffect(state.value) {
            stateFlow.value = state.value
        }
    }
}
fun <T1, T2> Flow<T1>.waitFirst(otherFlow: Flow<T2>): Flow<T1> =
    combine(otherFlow.take(1)) { value, _ -> value }
+42 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.settingslib.spa.framework.util

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull

/** A StateFlow holder which value could be set or sync from [State]. */
class StateFlowBridge<T> {
    private val stateFlow = MutableStateFlow<T?>(null)
    val flow = stateFlow.filterNotNull()

    fun setIfAbsent(value: T) {
        if (stateFlow.value == null) {
            stateFlow.value = value
        }
    }

    @Composable
    fun Sync(state: State<T>) {
        LaunchedEffect(state.value) {
            stateFlow.value = state.value
        }
    }
}
+90 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.settingslib.spa.framework.util

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.count
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class FlowsTest {
    @Test
    fun mapItem() = runTest {
        val inputFlow = flowOf(listOf("A", "BB", "CCC"))

        val outputFlow = inputFlow.mapItem { it.length }

        assertThat(outputFlow.first()).containsExactly(1, 2, 3).inOrder()
    }

    @Test
    fun asyncMapItem() = runTest {
        val inputFlow = flowOf(listOf("A", "BB", "CCC"))

        val outputFlow = inputFlow.asyncMapItem { it.length }

        assertThat(outputFlow.first()).containsExactly(1, 2, 3).inOrder()
    }

    @Test
    fun filterItem() = runTest {
        val inputFlow = flowOf(listOf("A", "BB", "CCC"))

        val outputFlow = inputFlow.filterItem { it.length >= 2 }

        assertThat(outputFlow.first()).containsExactly("BB", "CCC").inOrder()
    }

    @Test
    fun waitFirst_otherFlowEmpty() = runTest {
        val mainFlow = flowOf("A")
        val otherFlow = emptyFlow<String>()

        val outputFlow = mainFlow.waitFirst(otherFlow)

        assertThat(outputFlow.count()).isEqualTo(0)
    }

    @Test
    fun waitFirst_otherFlowOneValue() = runTest {
        val mainFlow = flowOf("A")
        val otherFlow = flowOf("B")

        val outputFlow = mainFlow.waitFirst(otherFlow)

        assertThat(outputFlow.toList()).containsExactly("A")
    }

    @Test
    fun waitFirst_otherFlowTwoValues() = runTest {
        val mainFlow = flowOf("A")
        val otherFlow = flowOf("B", "B")

        val outputFlow = mainFlow.waitFirst(otherFlow)

        assertThat(outputFlow.toList()).containsExactly("A")
    }
}
+67 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.settingslib.spa.framework.util

import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.framework.compose.stateOf
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class StateFlowBridgeTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun stateFlowBridge_initial() = runTest {
        val stateFlowBridge = StateFlowBridge<String>()

        val flow = stateFlowBridge.flow

        val first = flow.firstWithTimeoutOrNull()
        assertThat(first).isNull()
    }

    @Test
    fun stateFlowBridge_setIfAbsent() = runTest {
        val stateFlowBridge = StateFlowBridge<String>()

        stateFlowBridge.setIfAbsent("A")

        val first = stateFlowBridge.flow.firstWithTimeoutOrNull()
        assertThat(first).isEqualTo("A")
    }

    @Test
    fun stateFlowBridge_sync() = runTest {
        val stateFlowBridge = StateFlowBridge<String>()

        composeTestRule.setContent {
            stateFlowBridge.Sync(stateOf("A"))
        }

        val first = stateFlowBridge.flow.firstWithTimeoutOrNull()
        assertThat(first).isEqualTo("A")
    }
}
+26 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.settingslib.spa.testutils

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeoutOrNull

suspend fun <T> Flow<T>.firstWithTimeoutOrNull(timeMillis: Long = 500): T? =
    withTimeoutOrNull(timeMillis) {
        first()
    }
Loading