Improve Platform (#345)

* Add option to use WebUI X

- Added WebUI X from MMRL to KernelSU Next

* Some improvements

* Again some improvements

* Fix NPE

* Still not fixed

Use mappings from the action to decode the stacktrace

* bump mmrl

does still not work

* Works

* WORKS

* small fixes

* manager: only launch webui x when Platform.isAlive is `true`

* manager: disable "Use WebUI X" option when Platform.isAlive is `false`

* manager: improve app list loading

* manager: use system packageManager api

* ci: add spoofed manager workflow

* ci: add spoofed manager workflow

---------

Co-authored-by: rifsxd <rifat.44.azad.rifs@gmail.com>
Co-authored-by: Rifat Azad <33044977+rifsxd@users.noreply.github.com>
This commit is contained in:
Der_Googler
2025-05-01 16:24:00 +02:00
committed by GitHub
parent b112513df0
commit 298aa7960e
7 changed files with 103 additions and 203 deletions

View File

@@ -1,84 +0,0 @@
package com.rifsxd.ksunext.ui
import android.app.ActivityThread
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.PackageInfo
import android.os.IBinder
import android.os.UserManager
import android.util.Log
import com.dergoogler.mmrl.platform.content.IService
import com.dergoogler.mmrl.platform.stub.IServiceManager
import com.rifsxd.ksunext.IKsuInterface
import com.topjohnwu.superuser.ipc.RootService.USER_SERVICE
import rikka.parcelablelist.ParcelableListSlice
class KsuService: IService {
override val name: String
get() = "ksuService"
override fun create(manager: IServiceManager): IBinder = KsuServiceImpl()
}
class KsuServiceImpl : IKsuInterface.Stub() {
val context: Context
get() {
var context: Context = ActivityThread.currentApplication()
while (context is ContextWrapper) {
context = context.baseContext
}
return context
}
override fun getPackages(flags: Int): ParcelableListSlice<PackageInfo> {
val list: List<PackageInfo> = getInstalledPackagesAll(flags)
Log.i(TAG, "getPackages: " + list.size)
return ParcelableListSlice(list)
}
private val userIds: List<Int>
get() {
val result: MutableList<Int> =
ArrayList()
val um =
context.getSystemService(USER_SERVICE) as UserManager
val userProfiles = um.userProfiles
for (userProfile in userProfiles) {
val userId = userProfile.hashCode()
result.add(userProfile.hashCode())
}
return result
}
private fun getInstalledPackagesAll(flags: Int): ArrayList<PackageInfo> {
val packages = ArrayList<PackageInfo>()
for (userId in userIds) {
Log.i(TAG, "getInstalledPackagesAll: $userId")
packages.addAll(getInstalledPackagesAsUser(flags, userId))
}
return packages
}
private fun getInstalledPackagesAsUser(flags: Int, userId: Int): List<PackageInfo> {
try {
val pm = context.packageManager
val getInstalledPackagesAsUser = pm.javaClass.getDeclaredMethod(
"getInstalledPackagesAsUser",
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType
)
return getInstalledPackagesAsUser.invoke(pm, flags, userId) as List<PackageInfo>
} catch (e: Throwable) {
Log.e(TAG, "err", e)
}
return ArrayList()
}
private companion object Default {
const val TAG = "KsuService"
}
}

View File

@@ -81,6 +81,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.dergoogler.mmrl.platform.Platform
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
@@ -298,7 +299,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
onClickModule = { id, name, hasWebUi ->
if (hasWebUi) {
webUILauncher.launch(
if (prefs.getBoolean("use_webuix", false)) {
if (prefs.getBoolean("use_webuix", false) && Platform.isAlive) {
Intent(context, WebUIXActivity::class.java)
.setData(Uri.parse("kernelsu://webuix/$id"))
.putExtra("id", id)

View File

@@ -53,6 +53,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import com.dergoogler.mmrl.platform.Platform
import com.maxkeppeker.sheets.core.models.base.Header
import com.maxkeppeker.sheets.core.models.base.IconSource
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
@@ -328,6 +329,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
}
if (ksuVersion != null) {
SwitchItem(
enabled = Platform.isAlive,
icon = Icons.Filled.WebAsset,
title = stringResource(id = R.string.use_webuix),
summary = stringResource(id = R.string.use_webuix_summary),

View File

@@ -44,116 +44,93 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
val scope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val listState = rememberLazyListState()
var isPlatformAlive by remember { mutableStateOf(false) }
// Continuously check if Platform.isAlive
LaunchedEffect(Unit) {
while (!isPlatformAlive) {
isPlatformAlive = Platform.isAlive
if (!isPlatformAlive) {
kotlinx.coroutines.delay(500) // Check every 500ms
}
LaunchedEffect(navigator) {
viewModel.search = ""
if (viewModel.appList.isEmpty()) {
viewModel.fetchAppList()
}
}
if (!isPlatformAlive) {
// Show loading screen while Platform.isAlive is false
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
LaunchedEffect(viewModel.search) {
if (viewModel.search.isEmpty()) {
listState.scrollToItem(0)
}
} else {
LaunchedEffect(key1 = navigator) {
viewModel.search = ""
if (viewModel.appList.isEmpty()) {
viewModel.fetchAppList()
}
}
LaunchedEffect(Unit) {
if (viewModel.refreshOnReturn) {
viewModel.fetchAppList()
viewModel.refreshOnReturn = false
}
}
LaunchedEffect(viewModel.search) {
if (viewModel.search.isEmpty()) {
listState.scrollToItem(0)
}
}
Scaffold(
topBar = {
SearchAppBar(
title = { Text(stringResource(R.string.superuser)) },
searchText = viewModel.search,
onSearchTextChange = { viewModel.search = it },
onClearClick = { viewModel.search = "" },
dropdownContent = {
var showDropdown by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
if (viewModel.refreshOnReturn) {
viewModel.fetchAppList()
viewModel.refreshOnReturn = false
}
}
IconButton(
onClick = { showDropdown = true },
) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(id = R.string.settings)
)
Scaffold(
topBar = {
SearchAppBar(
title = { Text(stringResource(R.string.superuser)) },
searchText = viewModel.search,
onSearchTextChange = { viewModel.search = it },
onClearClick = { viewModel.search = "" },
dropdownContent = {
var showDropdown by remember { mutableStateOf(false) }
IconButton(
onClick = { showDropdown = true },
) {
Icon(
imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(id = R.string.settings)
)
DropdownMenu(expanded = showDropdown, onDismissRequest = {
DropdownMenu(expanded = showDropdown, onDismissRequest = {
showDropdown = false
}) {
DropdownMenuItem(text = {
Text(stringResource(R.string.refresh))
}, onClick = {
scope.launch {
viewModel.fetchAppList()
}
showDropdown = false
}) {
DropdownMenuItem(text = {
Text(stringResource(R.string.refresh))
}, onClick = {
scope.launch {
viewModel.fetchAppList()
})
DropdownMenuItem(text = {
Text(
if (viewModel.showSystemApps) {
stringResource(R.string.hide_system_apps)
} else {
stringResource(R.string.show_system_apps)
}
showDropdown = false
})
DropdownMenuItem(text = {
Text(
if (viewModel.showSystemApps) {
stringResource(R.string.hide_system_apps)
} else {
stringResource(R.string.show_system_apps)
}
)
}, onClick = {
viewModel.showSystemApps = !viewModel.showSystemApps
showDropdown = false
})
}
)
}, onClick = {
viewModel.showSystemApps = !viewModel.showSystemApps
showDropdown = false
})
}
},
scrollBehavior = scrollBehavior
)
},
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
) { innerPadding ->
PullToRefreshBox(
modifier = Modifier.padding(innerPadding),
onRefresh = {
scope.launch { viewModel.fetchAppList() }
}
},
isRefreshing = viewModel.isRefreshing
scrollBehavior = scrollBehavior
)
},
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
) { innerPadding ->
PullToRefreshBox(
modifier = Modifier.padding(innerPadding),
onRefresh = {
scope.launch { viewModel.fetchAppList() }
},
isRefreshing = viewModel.isRefreshing
) {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection)
) {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection)
) {
items(viewModel.appList, key = { it.packageName + it.uid }) { app ->
AppItem(app) {
viewModel.refreshOnReturn = true
navigator.navigate(AppProfileScreenDestination(app))
}
items(viewModel.appList, key = { it.packageName + it.uid }) { app ->
AppItem(app) {
viewModel.refreshOnReturn = true
navigator.navigate(AppProfileScreenDestination(app))
}
}
}

View File

@@ -11,17 +11,22 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.dergoogler.mmrl.platform.Platform
import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import com.rifsxd.ksunext.Natives
import com.rifsxd.ksunext.ksuApp
import com.rifsxd.ksunext.ui.util.HanziToPinyin
import com.rifsxd.ksunext.ui.webui.getPackages
import com.rifsxd.ksunext.ui.webui.packageManager
import com.rifsxd.ksunext.ui.webui.userManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeoutOrNull
import java.text.Collator
import java.util.*
class SuperUserViewModel : ViewModel() {
val isPlatformAlive get() = Platform.isAlive
var refreshOnReturn by mutableStateOf(false)
public set
@@ -93,11 +98,25 @@ class SuperUserViewModel : ViewModel() {
suspend fun fetchAppList() {
isRefreshing = true
withContext(Dispatchers.IO) {
withTimeoutOrNull(TIMEOUT_MILLIS) {
while (!isPlatformAlive) {
delay(500)
}
} ?: return@withContext // Exit early if timeout
val pm = ksuApp.packageManager
val start = SystemClock.elapsedRealtime()
val packages = Platform.getPackages(0).list
val userInfos = Platform.userManager.getUsers()
val packages = mutableListOf<PackageInfo>()
val packageManager = Platform.packageManager
for (userInfo in userInfos) {
Log.i(TAG, "fetchAppList: ${userInfo.id}")
packages.addAll(packageManager.getInstalledPackages(0, userInfo.id))
}
apps = packages.map {
val appInfo = it.applicationInfo

View File

@@ -1,22 +1,18 @@
package com.rifsxd.ksunext.ui.webui
import android.content.Context
import android.content.ServiceConnection
import android.content.pm.PackageInfo
import android.util.Log
import com.dergoogler.mmrl.platform.Platform
import com.dergoogler.mmrl.platform.content.Service
import com.dergoogler.mmrl.platform.hiddenApi.HiddenPackageManager
import com.dergoogler.mmrl.platform.hiddenApi.HiddenUserManager
import com.dergoogler.mmrl.platform.model.IProvider
import com.dergoogler.mmrl.platform.model.PlatformIntent
import com.rifsxd.ksunext.IKsuInterface
import com.rifsxd.ksunext.Natives
import com.rifsxd.ksunext.ksuApp
import com.rifsxd.ksunext.ui.KsuService
import com.topjohnwu.superuser.ipc.RootService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import rikka.parcelablelist.ParcelableListSlice
class KsuLibSuProvider : IProvider {
override val name = "KsuLibSu"
@@ -53,10 +49,6 @@ suspend fun initPlatform() = withContext(Dispatchers.IO) {
delay(1000)
}
Platform.mService.addService(
Service(KsuService::class.java)
)
return@withContext active
} catch (e: Exception) {
Log.e("KsuLibSu", "Failed to initialize platform", e)
@@ -64,12 +56,5 @@ suspend fun initPlatform() = withContext(Dispatchers.IO) {
}
}
fun Platform.Companion.getPackages(flags: Int): ParcelableListSlice<PackageInfo> {
val ksuService: IKsuInterface by lazy {
IKsuInterface.Stub.asInterface(
this.mService.getService("ksuService")
)
}
return ksuService.getPackages(flags)
}
val Platform.Companion.packageManager get(): HiddenPackageManager = HiddenPackageManager(this.mService)
val Platform.Companion.userManager get(): HiddenUserManager = HiddenUserManager(this.mService)

View File

@@ -17,7 +17,7 @@ parcelablelist = "2.0.1"
libsu = "6.0.0"
apksign = "1.4"
cmaker = "1.2"
mmrl = "785605169b"
mmrl = "b3c7ca55e0"
[plugins]
agp-app = { id = "com.android.application", version.ref = "agp" }