Merge "Refresh the AppList for changes"
This commit is contained in:
33
packages/SettingsLib/Spa/testutils/src/SpaTest.kt
Normal file
33
packages/SettingsLib/Spa/testutils/src/SpaTest.kt
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 java.util.concurrent.TimeoutException
|
||||
|
||||
/**
|
||||
* Blocks until the given condition is satisfied.
|
||||
*/
|
||||
fun waitUntil(timeoutMillis: Long = 1000, condition: () -> Boolean) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
while (!condition()) {
|
||||
// Let Android run measure, draw and in general any other async operations.
|
||||
Thread.sleep(10)
|
||||
if (System.currentTimeMillis() - startTime > timeoutMillis) {
|
||||
throw TimeoutException("Condition still not satisfied after $timeoutMillis ms")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import android.content.IntentFilter
|
||||
import android.os.UserHandle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -34,24 +33,25 @@ import androidx.lifecycle.LifecycleEventObserver
|
||||
*/
|
||||
@Composable
|
||||
fun DisposableBroadcastReceiverAsUser(
|
||||
userId: Int,
|
||||
intentFilter: IntentFilter,
|
||||
userHandle: UserHandle,
|
||||
onStart: () -> Unit = {},
|
||||
onReceive: (Intent) -> Unit,
|
||||
) {
|
||||
val broadcastReceiver = remember {
|
||||
object : BroadcastReceiver() {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val broadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
onReceive(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_START) {
|
||||
context.registerReceiverAsUser(
|
||||
broadcastReceiver, UserHandle.of(userId), intentFilter, null, null)
|
||||
broadcastReceiver, userHandle, intentFilter, null, null
|
||||
)
|
||||
onStart()
|
||||
} else if (event == Lifecycle.Event.ON_STOP) {
|
||||
context.unregisterReceiver(broadcastReceiver)
|
||||
}
|
||||
|
||||
@@ -21,13 +21,10 @@ import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
/**
|
||||
* The config used to load the App List.
|
||||
@@ -40,36 +37,42 @@ internal data class AppListConfig(
|
||||
/**
|
||||
* The repository to load the App List data.
|
||||
*/
|
||||
internal class AppListRepository(context: Context) {
|
||||
internal interface AppListRepository {
|
||||
/** Loads the list of [ApplicationInfo]. */
|
||||
suspend fun loadApps(config: AppListConfig): List<ApplicationInfo>
|
||||
|
||||
/** Gets the flow of predicate that could used to filter system app. */
|
||||
fun showSystemPredicate(
|
||||
userIdFlow: Flow<Int>,
|
||||
showSystemFlow: Flow<Boolean>,
|
||||
): Flow<(app: ApplicationInfo) -> Boolean>
|
||||
}
|
||||
|
||||
|
||||
internal class AppListRepositoryImpl(context: Context) : AppListRepository {
|
||||
private val packageManager = context.packageManager
|
||||
|
||||
fun loadApps(configFlow: Flow<AppListConfig>): Flow<List<ApplicationInfo>> = configFlow
|
||||
.map { loadApps(it) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
override suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> = coroutineScope {
|
||||
val hiddenSystemModulesDeferred = async {
|
||||
packageManager.getInstalledModules(0)
|
||||
.filter { it.isHidden }
|
||||
.map { it.packageName }
|
||||
.toSet()
|
||||
}
|
||||
val flags = PackageManager.ApplicationInfoFlags.of(
|
||||
(PackageManager.MATCH_DISABLED_COMPONENTS or
|
||||
PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
|
||||
)
|
||||
val installedApplicationsAsUser =
|
||||
packageManager.getInstalledApplicationsAsUser(flags, config.userId)
|
||||
|
||||
private suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> {
|
||||
return coroutineScope {
|
||||
val hiddenSystemModulesDeferred = async {
|
||||
packageManager.getInstalledModules(0)
|
||||
.filter { it.isHidden }
|
||||
.map { it.packageName }
|
||||
.toSet()
|
||||
}
|
||||
val flags = PackageManager.ApplicationInfoFlags.of(
|
||||
(PackageManager.MATCH_DISABLED_COMPONENTS or
|
||||
PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong()
|
||||
)
|
||||
val installedApplicationsAsUser =
|
||||
packageManager.getInstalledApplicationsAsUser(flags, config.userId)
|
||||
|
||||
val hiddenSystemModules = hiddenSystemModulesDeferred.await()
|
||||
installedApplicationsAsUser.filter { app ->
|
||||
app.isInAppList(config.showInstantApps, hiddenSystemModules)
|
||||
}
|
||||
val hiddenSystemModules = hiddenSystemModulesDeferred.await()
|
||||
installedApplicationsAsUser.filter { app ->
|
||||
app.isInAppList(config.showInstantApps, hiddenSystemModules)
|
||||
}
|
||||
}
|
||||
|
||||
fun showSystemPredicate(
|
||||
override fun showSystemPredicate(
|
||||
userIdFlow: Flow<Int>,
|
||||
showSystemFlow: Flow<Boolean>,
|
||||
): Flow<(app: ApplicationInfo) -> Boolean> =
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.android.settingslib.spaprivileged.model.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.icu.text.Collator
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
@@ -27,12 +28,16 @@ import com.android.settingslib.spa.framework.util.waitFirst
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
|
||||
internal data class AppListData<T : AppRecord>(
|
||||
@@ -43,9 +48,15 @@ internal data class AppListData<T : AppRecord>(
|
||||
AppListData(appEntries.filter(predicate), option)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class AppListViewModel<T : AppRecord>(
|
||||
application: Application,
|
||||
) : AppListViewModelImpl<T>(application)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal open class AppListViewModelImpl<T : AppRecord>(
|
||||
application: Application,
|
||||
appListRepositoryFactory: (Context) -> AppListRepository = ::AppListRepositoryImpl,
|
||||
appRepositoryFactory: (Context) -> AppRepository = ::AppRepositoryImpl,
|
||||
) : AndroidViewModel(application) {
|
||||
val appListConfig = StateFlowBridge<AppListConfig>()
|
||||
val listModel = StateFlowBridge<AppListModel<T>>()
|
||||
@@ -53,16 +64,18 @@ internal class AppListViewModel<T : AppRecord>(
|
||||
val option = StateFlowBridge<Int>()
|
||||
val searchQuery = StateFlowBridge<String>()
|
||||
|
||||
private val appListRepository = AppListRepository(application)
|
||||
private val appRepository = AppRepositoryImpl(application)
|
||||
private val appListRepository = appListRepositoryFactory(application)
|
||||
private val appRepository = appRepositoryFactory(application)
|
||||
private val collator = Collator.getInstance().freeze()
|
||||
private val labelMap = ConcurrentHashMap<String, String>()
|
||||
private val scope = viewModelScope + Dispatchers.Default
|
||||
private val scope = viewModelScope + Dispatchers.IO
|
||||
|
||||
private val userIdFlow = appListConfig.flow.map { it.userId }
|
||||
|
||||
private val appsStateFlow = MutableStateFlow<List<ApplicationInfo>?>(null)
|
||||
|
||||
private val recordListFlow = listModel.flow
|
||||
.flatMapLatest { it.transform(userIdFlow, appListRepository.loadApps(appListConfig.flow)) }
|
||||
.flatMapLatest { it.transform(userIdFlow, appsStateFlow.filterNotNull()) }
|
||||
.shareIn(scope = scope, started = SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
private val systemFilteredFlow =
|
||||
@@ -83,6 +96,12 @@ internal class AppListViewModel<T : AppRecord>(
|
||||
scheduleOnFirstLoaded()
|
||||
}
|
||||
|
||||
fun reloadApps() {
|
||||
viewModelScope.launch {
|
||||
appsStateFlow.value = appListRepository.loadApps(appListConfig.flow.first())
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterAndSort(option: Int) = listModel.flow.flatMapLatest { listModel ->
|
||||
listModel.filter(userIdFlow, option, systemFilteredFlow)
|
||||
.asyncMapItem { record ->
|
||||
|
||||
@@ -22,8 +22,10 @@ import android.graphics.drawable.Drawable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.android.settingslib.Utils
|
||||
import com.android.settingslib.spa.framework.compose.rememberContext
|
||||
import com.android.settingslib.spaprivileged.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -34,7 +36,12 @@ interface AppRepository {
|
||||
fun loadLabel(app: ApplicationInfo): String
|
||||
|
||||
@Composable
|
||||
fun produceLabel(app: ApplicationInfo): State<String>
|
||||
fun produceLabel(app: ApplicationInfo) =
|
||||
produceState(initialValue = stringResource(R.string.summary_placeholder), app) {
|
||||
withContext(Dispatchers.IO) {
|
||||
value = loadLabel(app)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun produceIcon(app: ApplicationInfo): State<Drawable?>
|
||||
@@ -45,13 +52,6 @@ internal class AppRepositoryImpl(private val context: Context) : AppRepository {
|
||||
|
||||
override fun loadLabel(app: ApplicationInfo): String = app.loadLabel(packageManager).toString()
|
||||
|
||||
@Composable
|
||||
override fun produceLabel(app: ApplicationInfo) = produceState(initialValue = "", app) {
|
||||
withContext(Dispatchers.Default) {
|
||||
value = app.loadLabel(packageManager).toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun produceIcon(app: ApplicationInfo) =
|
||||
produceState<Drawable?>(initialValue = null, app) {
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package com.android.settingslib.spaprivileged.template.app
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.UserHandle
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -33,6 +36,7 @@ import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHid
|
||||
import com.android.settingslib.spa.framework.compose.toState
|
||||
import com.android.settingslib.spa.widget.ui.PlaceholderTitle
|
||||
import com.android.settingslib.spaprivileged.R
|
||||
import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListConfig
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListData
|
||||
import com.android.settingslib.spaprivileged.model.app.AppListModel
|
||||
@@ -120,5 +124,15 @@ private fun <T : AppRecord> loadAppListData(
|
||||
viewModel.option.Sync(state.option)
|
||||
viewModel.searchQuery.Sync(state.searchQuery)
|
||||
|
||||
return viewModel.appListDataFlow.collectAsState(null, Dispatchers.Default)
|
||||
DisposableBroadcastReceiverAsUser(
|
||||
intentFilter = IntentFilter(Intent.ACTION_PACKAGE_ADDED).apply {
|
||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
||||
addDataScheme("package")
|
||||
},
|
||||
userHandle = UserHandle.of(config.userId),
|
||||
onStart = { viewModel.reloadApps() },
|
||||
) { viewModel.reloadApps() }
|
||||
|
||||
return viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ android_test {
|
||||
],
|
||||
|
||||
static_libs: [
|
||||
"SpaLibTestUtils",
|
||||
"androidx.compose.ui_ui-test-junit4",
|
||||
"androidx.compose.ui_ui-test-manifest",
|
||||
"androidx.test.ext.junit",
|
||||
|
||||
@@ -24,9 +24,6 @@ import android.content.pm.PackageManager.ResolveInfoFlags
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
@@ -36,11 +33,9 @@ import org.mockito.Mock
|
||||
import org.mockito.Mockito.any
|
||||
import org.mockito.Mockito.anyInt
|
||||
import org.mockito.Mockito.eq
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
import org.mockito.junit.MockitoRule
|
||||
|
||||
private const val USER_ID = 0
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -80,36 +75,28 @@ class AppListRepositoryTest {
|
||||
packageManager.queryIntentActivitiesAsUser(any(), any<ResolveInfoFlags>(), eq(USER_ID))
|
||||
).thenReturn(emptyList())
|
||||
|
||||
repository = AppListRepository(context)
|
||||
repository = AppListRepositoryImpl(context)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun notShowInstantApps() = runTest {
|
||||
val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = false)
|
||||
|
||||
val appListFlow = repository.loadApps(flowOf(appListConfig))
|
||||
val appListFlow = repository.loadApps(appListConfig)
|
||||
|
||||
launch {
|
||||
val flowValues = mutableListOf<List<ApplicationInfo>>()
|
||||
appListFlow.toList(flowValues)
|
||||
assertThat(flowValues).hasSize(1)
|
||||
|
||||
assertThat(flowValues[0]).containsExactly(normalApp)
|
||||
}
|
||||
assertThat(appListFlow).containsExactly(normalApp)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun showInstantApps() = runTest {
|
||||
val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = true)
|
||||
|
||||
val appListFlow = repository.loadApps(flowOf(appListConfig))
|
||||
val appListFlow = repository.loadApps(appListConfig)
|
||||
|
||||
launch {
|
||||
val flowValues = mutableListOf<List<ApplicationInfo>>()
|
||||
appListFlow.toList(flowValues)
|
||||
assertThat(flowValues).hasSize(1)
|
||||
assertThat(appListFlow).containsExactly(normalApp, instantApp)
|
||||
}
|
||||
|
||||
assertThat(flowValues[0]).containsExactly(normalApp, instantApp)
|
||||
}
|
||||
private companion object {
|
||||
const val USER_ID = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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.spaprivileged.model.app
|
||||
|
||||
import android.app.Application
|
||||
import android.content.pm.ApplicationInfo
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.android.settingslib.spa.framework.compose.stateOf
|
||||
import com.android.settingslib.spa.framework.util.asyncMapItem
|
||||
import com.android.settingslib.spa.testutils.waitUntil
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
import org.mockito.junit.MockitoRule
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AppListViewModelTest {
|
||||
@JvmField
|
||||
@Rule
|
||||
val mockito: MockitoRule = MockitoJUnit.rule()
|
||||
|
||||
@Mock
|
||||
private lateinit var application: Application
|
||||
|
||||
private val listModel = TestAppListModel()
|
||||
|
||||
private fun createViewModel(): AppListViewModelImpl<TestAppRecord> {
|
||||
val viewModel = AppListViewModelImpl<TestAppRecord>(
|
||||
application = application,
|
||||
appListRepositoryFactory = { FakeAppListRepository },
|
||||
appRepositoryFactory = { FakeAppRepository },
|
||||
)
|
||||
viewModel.appListConfig.setIfAbsent(CONFIG)
|
||||
viewModel.listModel.setIfAbsent(listModel)
|
||||
viewModel.showSystem.setIfAbsent(false)
|
||||
viewModel.option.setIfAbsent(0)
|
||||
viewModel.searchQuery.setIfAbsent("")
|
||||
viewModel.reloadApps()
|
||||
return viewModel
|
||||
}
|
||||
|
||||
@Test
|
||||
fun appListDataFlow() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
val (appEntries, option) = viewModel.appListDataFlow.first()
|
||||
|
||||
assertThat(appEntries).hasSize(1)
|
||||
assertThat(appEntries[0].record.app).isSameInstanceAs(APP)
|
||||
assertThat(appEntries[0].label).isEqualTo(LABEL)
|
||||
assertThat(option).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onFirstLoaded_calledWhenLoaded() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.appListDataFlow.first()
|
||||
|
||||
waitUntil { listModel.onFirstLoadedCalled }
|
||||
}
|
||||
|
||||
private object FakeAppListRepository : AppListRepository {
|
||||
override suspend fun loadApps(config: AppListConfig) = listOf(APP)
|
||||
|
||||
override fun showSystemPredicate(
|
||||
userIdFlow: Flow<Int>,
|
||||
showSystemFlow: Flow<Boolean>,
|
||||
): Flow<(app: ApplicationInfo) -> Boolean> = flowOf { true }
|
||||
}
|
||||
|
||||
private object FakeAppRepository : AppRepository {
|
||||
override fun loadLabel(app: ApplicationInfo) = LABEL
|
||||
|
||||
@Composable
|
||||
override fun produceIcon(app: ApplicationInfo) = stateOf(null)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val USER_ID = 0
|
||||
const val PACKAGE_NAME = "package.name"
|
||||
const val LABEL = "Label"
|
||||
val CONFIG = AppListConfig(userId = USER_ID, showInstantApps = false)
|
||||
val APP = ApplicationInfo().apply {
|
||||
packageName = PACKAGE_NAME
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class TestAppRecord(override val app: ApplicationInfo) : AppRecord
|
||||
|
||||
private class TestAppListModel : AppListModel<TestAppRecord> {
|
||||
var onFirstLoadedCalled = false
|
||||
|
||||
override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
|
||||
appListFlow.asyncMapItem { TestAppRecord(it) }
|
||||
|
||||
@Composable
|
||||
override fun getSummary(option: Int, record: TestAppRecord) = null
|
||||
|
||||
override fun filter(
|
||||
userIdFlow: Flow<Int>,
|
||||
option: Int,
|
||||
recordListFlow: Flow<List<TestAppRecord>>,
|
||||
) = recordListFlow
|
||||
|
||||
override suspend fun onFirstLoaded(recordList: List<TestAppRecord>) {
|
||||
onFirstLoadedCalled = true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user