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.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.dergoogler.mmrl.platform.Platform
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
@@ -298,7 +299,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
onClickModule = { id, name, hasWebUi -> onClickModule = { id, name, hasWebUi ->
if (hasWebUi) { if (hasWebUi) {
webUILauncher.launch( webUILauncher.launch(
if (prefs.getBoolean("use_webuix", false)) { if (prefs.getBoolean("use_webuix", false) && Platform.isAlive) {
Intent(context, WebUIXActivity::class.java) Intent(context, WebUIXActivity::class.java)
.setData(Uri.parse("kernelsu://webuix/$id")) .setData(Uri.parse("kernelsu://webuix/$id"))
.putExtra("id", 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider 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.Header
import com.maxkeppeker.sheets.core.models.base.IconSource import com.maxkeppeker.sheets.core.models.base.IconSource
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
@@ -328,6 +329,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
} }
if (ksuVersion != null) { if (ksuVersion != null) {
SwitchItem( SwitchItem(
enabled = Platform.isAlive,
icon = Icons.Filled.WebAsset, icon = Icons.Filled.WebAsset,
title = stringResource(id = R.string.use_webuix), title = stringResource(id = R.string.use_webuix),
summary = stringResource(id = R.string.use_webuix_summary), summary = stringResource(id = R.string.use_webuix_summary),

View File

@@ -44,116 +44,93 @@ fun SuperUserScreen(navigator: DestinationsNavigator) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val listState = rememberLazyListState() val listState = rememberLazyListState()
var isPlatformAlive by remember { mutableStateOf(false) }
// Continuously check if Platform.isAlive LaunchedEffect(navigator) {
LaunchedEffect(Unit) { viewModel.search = ""
while (!isPlatformAlive) { if (viewModel.appList.isEmpty()) {
isPlatformAlive = Platform.isAlive viewModel.fetchAppList()
if (!isPlatformAlive) {
kotlinx.coroutines.delay(500) // Check every 500ms
}
} }
} }
if (!isPlatformAlive) { LaunchedEffect(viewModel.search) {
// Show loading screen while Platform.isAlive is false if (viewModel.search.isEmpty()) {
Box( listState.scrollToItem(0)
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
} }
} else { }
LaunchedEffect(key1 = navigator) {
viewModel.search = "" LaunchedEffect(Unit) {
if (viewModel.appList.isEmpty()) { if (viewModel.refreshOnReturn) {
viewModel.fetchAppList() viewModel.fetchAppList()
} viewModel.refreshOnReturn = false
} }
}
LaunchedEffect(viewModel.search) { Scaffold(
if (viewModel.search.isEmpty()) { topBar = {
listState.scrollToItem(0) 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) { IconButton(
if (viewModel.refreshOnReturn) { onClick = { showDropdown = true },
viewModel.fetchAppList() ) {
viewModel.refreshOnReturn = false Icon(
} imageVector = Icons.Filled.MoreVert,
} contentDescription = stringResource(id = R.string.settings)
)
Scaffold( DropdownMenu(expanded = showDropdown, onDismissRequest = {
topBar = { showDropdown = false
SearchAppBar( }) {
title = { Text(stringResource(R.string.superuser)) }, DropdownMenuItem(text = {
searchText = viewModel.search, Text(stringResource(R.string.refresh))
onSearchTextChange = { viewModel.search = it }, }, onClick = {
onClearClick = { viewModel.search = "" }, scope.launch {
dropdownContent = { viewModel.fetchAppList()
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 = {
showDropdown = false showDropdown = false
}) { })
DropdownMenuItem(text = { DropdownMenuItem(text = {
Text(stringResource(R.string.refresh)) Text(
}, onClick = { if (viewModel.showSystemApps) {
scope.launch { stringResource(R.string.hide_system_apps)
viewModel.fetchAppList() } else {
stringResource(R.string.show_system_apps)
} }
showDropdown = false )
}) }, onClick = {
DropdownMenuItem(text = { viewModel.showSystemApps = !viewModel.showSystemApps
Text( showDropdown = false
if (viewModel.showSystemApps) { })
stringResource(R.string.hide_system_apps)
} else {
stringResource(R.string.show_system_apps)
}
)
}, 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( items(viewModel.appList, key = { it.packageName + it.uid }) { app ->
state = listState, AppItem(app) {
modifier = Modifier viewModel.refreshOnReturn = true
.fillMaxSize() navigator.navigate(AppProfileScreenDestination(app))
.nestedScroll(scrollBehavior.nestedScrollConnection)
) {
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.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.dergoogler.mmrl.platform.Platform import com.dergoogler.mmrl.platform.Platform
import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import com.rifsxd.ksunext.Natives import com.rifsxd.ksunext.Natives
import com.rifsxd.ksunext.ksuApp import com.rifsxd.ksunext.ksuApp
import com.rifsxd.ksunext.ui.util.HanziToPinyin 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.text.Collator
import java.util.* import java.util.*
class SuperUserViewModel : ViewModel() { class SuperUserViewModel : ViewModel() {
val isPlatformAlive get() = Platform.isAlive
var refreshOnReturn by mutableStateOf(false) var refreshOnReturn by mutableStateOf(false)
public set public set
@@ -93,11 +98,25 @@ class SuperUserViewModel : ViewModel() {
suspend fun fetchAppList() { suspend fun fetchAppList() {
isRefreshing = true isRefreshing = true
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
withTimeoutOrNull(TIMEOUT_MILLIS) {
while (!isPlatformAlive) {
delay(500)
}
} ?: return@withContext // Exit early if timeout
val pm = ksuApp.packageManager val pm = ksuApp.packageManager
val start = SystemClock.elapsedRealtime() 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 { apps = packages.map {
val appInfo = it.applicationInfo val appInfo = it.applicationInfo

View File

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

View File

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