Loading packages/SettingsLib/Spa/testutils/src/SpaTest.kt 0 → 100644 +33 −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 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") } } } packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt +9 −9 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) } Loading packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt +30 −27 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -40,15 +37,22 @@ internal data class AppListConfig( /** * The repository to load the App List data. */ internal class AppListRepository(context: Context) { private val packageManager = context.packageManager internal interface AppListRepository { /** Loads the list of [ApplicationInfo]. */ suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> fun loadApps(configFlow: Flow<AppListConfig>): Flow<List<ApplicationInfo>> = configFlow .map { loadApps(it) } .flowOn(Dispatchers.Default) /** Gets the flow of predicate that could used to filter system app. */ fun showSystemPredicate( userIdFlow: Flow<Int>, showSystemFlow: Flow<Boolean>, ): Flow<(app: ApplicationInfo) -> Boolean> } private suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> { return coroutineScope { internal class AppListRepositoryImpl(context: Context) : AppListRepository { private val packageManager = context.packageManager override suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> = coroutineScope { val hiddenSystemModulesDeferred = async { packageManager.getInstalledModules(0) .filter { it.isHidden } Loading @@ -67,9 +71,8 @@ internal class AppListRepository(context: Context) { app.isInAppList(config.showInstantApps, hiddenSystemModules) } } } fun showSystemPredicate( override fun showSystemPredicate( userIdFlow: Flow<Int>, showSystemFlow: Flow<Boolean>, ): Flow<(app: ApplicationInfo) -> Boolean> = Loading packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt +24 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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>( Loading @@ -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>>() Loading @@ -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 = Loading @@ -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 -> Loading packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt +8 −8 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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?> Loading @@ -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) { Loading Loading
packages/SettingsLib/Spa/testutils/src/SpaTest.kt 0 → 100644 +33 −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 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") } } }
packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt +9 −9 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) } Loading
packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt +30 −27 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -40,15 +37,22 @@ internal data class AppListConfig( /** * The repository to load the App List data. */ internal class AppListRepository(context: Context) { private val packageManager = context.packageManager internal interface AppListRepository { /** Loads the list of [ApplicationInfo]. */ suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> fun loadApps(configFlow: Flow<AppListConfig>): Flow<List<ApplicationInfo>> = configFlow .map { loadApps(it) } .flowOn(Dispatchers.Default) /** Gets the flow of predicate that could used to filter system app. */ fun showSystemPredicate( userIdFlow: Flow<Int>, showSystemFlow: Flow<Boolean>, ): Flow<(app: ApplicationInfo) -> Boolean> } private suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> { return coroutineScope { internal class AppListRepositoryImpl(context: Context) : AppListRepository { private val packageManager = context.packageManager override suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> = coroutineScope { val hiddenSystemModulesDeferred = async { packageManager.getInstalledModules(0) .filter { it.isHidden } Loading @@ -67,9 +71,8 @@ internal class AppListRepository(context: Context) { app.isInAppList(config.showInstantApps, hiddenSystemModules) } } } fun showSystemPredicate( override fun showSystemPredicate( userIdFlow: Flow<Int>, showSystemFlow: Flow<Boolean>, ): Flow<(app: ApplicationInfo) -> Boolean> = Loading
packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt +24 −5 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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>( Loading @@ -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>>() Loading @@ -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 = Loading @@ -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 -> Loading
packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt +8 −8 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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?> Loading @@ -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) { Loading