diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt index 489cb544..d38d211a 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.core.* import androidx.compose.animation.expandHorizontally import androidx.compose.animation.expandVertically import androidx.compose.animation.* +import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource @@ -31,6 +32,7 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -38,12 +40,15 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -51,6 +56,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage +import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import com.dergoogler.mmrl.ui.component.LabelItem import com.dergoogler.mmrl.ui.component.LabelItemDefaults @@ -314,6 +320,9 @@ private fun SuperUserContent( scope: CoroutineScope ) { val expandedGroups = remember { mutableStateOf(setOf()) } + val density = LocalDensity.current + val targetSizePx = remember(density) { with(density) { 36.dp.roundToPx() } } + val context = LocalContext.current PullToRefreshBox( modifier = Modifier.padding(innerPadding), @@ -329,6 +338,7 @@ private fun SuperUserContent( filteredAndSortedAppGroups.forEachIndexed { _, appGroup -> item(key = "${appGroup.uid}-${appGroup.mainApp.packageName}") { AppGroupItem( + expandedGroups = expandedGroups, appGroup = appGroup, isSelected = appGroup.packageNames.any { viewModel.selectedApps.contains(it) }, onToggleSelection = { @@ -357,32 +367,46 @@ private fun SuperUserContent( ) } + if (appGroup.apps.size <= 1) return@forEachIndexed + items(appGroup.apps, key = { "${it.packageName}-${it.uid}" }) { app -> + val painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(context) + .data(app.packageInfo) + .size(targetSizePx) + .crossfade(true) + .build() + ) + + val listItemContent = remember(app.packageName, appGroup.uid) { + @Composable { + ListItem( + modifier = Modifier + .clickable { navigator.navigate(AppProfileScreenDestination(app)) } + .fillMaxWidth() + .padding(start = 10.dp), + headlineContent = { Text(app.label, style = MaterialTheme.typography.bodyMedium) }, + supportingContent = { Text(app.packageName, style = MaterialTheme.typography.bodySmall) }, + leadingContent = { + Image( + painter = painter, + contentDescription = app.label, + modifier = Modifier + .padding(4.dp) + .size(36.dp), + contentScale = ContentScale.Crop + ) + } + ) + } + } + AnimatedVisibility( - visible = expandedGroups.value.contains(appGroup.uid) && appGroup.apps.size > 1, + visible = expandedGroups.value.contains(appGroup.uid), enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { - ListItem( - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp) - .clickable { - navigator.navigate(AppProfileScreenDestination(app)) - }, - headlineContent = { Text(app.label, style = MaterialTheme.typography.bodyMedium) }, - supportingContent = { Text(app.packageName, style = MaterialTheme.typography.bodySmall) }, - leadingContent = { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(app.packageInfo) - .crossfade(true) - .build(), - contentDescription = app.label, - modifier = Modifier.padding(4.dp).width(36.dp).height(36.dp) - ) - } - ) + listItemContent() } } } @@ -797,7 +821,8 @@ private fun AppGroupItem( onToggleSelection: () -> Unit, onClick: () -> Unit, onLongClick: () -> Unit, - viewModel: SuperUserViewModel + viewModel: SuperUserViewModel, + expandedGroups: MutableState> ) { val mainApp = appGroup.mainApp @@ -818,9 +843,27 @@ private fun AppGroupItem( } else { mainApp.packageName } - Text(summaryText) - Spacer(modifier = Modifier.height(4.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(summaryText) + + if (appGroup.apps.size > 1) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.rotate( + animateFloatAsState( + targetValue = if (expandedGroups.value.contains(appGroup.uid)) 180f else 0f, + animationSpec = tween(200, easing = LinearOutSlowInEasing), + label = "" + ).value + ) + ) + } + } FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { if (appGroup.allowSu) { @@ -853,7 +896,7 @@ private fun AppGroupItem( ) } if (appGroup.apps.size > 1) { - Natives.getUserName(appGroup.uid)?.let { + appGroup.userName?.let { LabelItem( text = it, style = LabelItemDefaults.style.copy( diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt index bfa29491..16cedc20 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt @@ -18,7 +18,6 @@ import com.topjohnwu.superuser.Shell import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.parcelize.Parcelize import java.text.Collator import java.util.* import java.util.concurrent.LinkedBlockingQueue @@ -27,6 +26,8 @@ import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import com.sukisu.zako.IKsuInterface +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize enum class AppCategory(val displayNameRes: Int, val persistKey: String) { ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"), @@ -77,31 +78,36 @@ class SuperUserViewModel : ViewModel() { private const val BATCH_SIZE = 20 } + @Immutable @Parcelize data class AppInfo( val label: String, val packageInfo: PackageInfo, val profile: Natives.Profile?, ) : Parcelable { - val packageName: String get() = packageInfo.packageName - val uid: Int get() = packageInfo.applicationInfo!!.uid + @IgnoredOnParcel + val packageName: String = packageInfo.packageName + @IgnoredOnParcel + val uid: Int = packageInfo.applicationInfo!!.uid } + @Immutable @Parcelize data class AppGroup( val uid: Int, val apps: List, val profile: Natives.Profile? ) : Parcelable { - val mainApp: AppInfo get() = apps.first() - val packageNames: List get() = apps.map { it.packageName } - val allowSu: Boolean get() = profile?.allowSu == true - - val userName: String? get() = Natives.getUserName(uid) - val hasCustomProfile: Boolean - get() = profile?.let { - if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault - } ?: false + @IgnoredOnParcel + val mainApp: AppInfo = apps.first() + @IgnoredOnParcel + val packageNames: List = apps.map { it.packageName } + @IgnoredOnParcel + val allowSu: Boolean = profile?.allowSu == true + @IgnoredOnParcel + val userName: String? = Natives.getUserName(uid) + @IgnoredOnParcel + val hasCustomProfile : Boolean = profile?.let { if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault } ?: false } private val appProcessingThreadPool = ThreadPoolExecutor(