manager: introduce webui package manager api (#2928)
this is a squash of: * manager: introduce app package info API for webui-next (KernelSU-Next/KernelSU-Next@58167a4) * manager: sort a-z order for webui-next list packages api (KernelSU-Next/KernelSU-Next@4a9733c) * manager: implement getPackagesIcons and cacheAllPackageIcons api to webui-next (KernelSU-Next/KernelSU-Next@a361fa3) * manager/webui: let getPackagesIcons generate icon and store in cache as well when called (KernelSU-Next/KernelSU-Next@6afa86d) * POC: load icon app via ksu://icon/[packageName] (KernelSU-Next#674) (KernelSU-Next/KernelSU-Next@bc9927b) * manager: refine webui package manager (KOWX712/KernelSU@0400c42) Co-Authored-By: Rifat Azad <33044977+rifsxd@users.noreply.github.com> Co-Authored-By: Fahrez256Bit <167403685+fahrez256@users.noreply.github.com> Signed-off-by: KOWX712 <leecc0503@gmail.com> --------- Signed-off-by: KOWX712 <leecc0503@gmail.com> Co-authored-by: Rifat Azad <33044977+rifsxd@users.noreply.github.com> Co-authored-by: Fahrez256Bit <167403685+fahrez256@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,13 @@ package com.sukisu.ultra
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.ViewModelStore
|
||||||
|
import androidx.lifecycle.ViewModelStoreOwner
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import com.dergoogler.mmrl.platform.Platform
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
@@ -14,14 +21,21 @@ import java.util.Locale
|
|||||||
|
|
||||||
lateinit var ksuApp: KernelSUApplication
|
lateinit var ksuApp: KernelSUApplication
|
||||||
|
|
||||||
class KernelSUApplication : Application() {
|
class KernelSUApplication : Application(), ViewModelStoreOwner {
|
||||||
|
|
||||||
lateinit var okhttpClient: OkHttpClient
|
lateinit var okhttpClient: OkHttpClient
|
||||||
|
private val appViewModelStore by lazy { ViewModelStore() }
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
ksuApp = this
|
ksuApp = this
|
||||||
|
|
||||||
|
// For faster response when first entering superuser or webui activity
|
||||||
|
val superUserViewModel = ViewModelProvider(this)[SuperUserViewModel::class.java]
|
||||||
|
CoroutineScope(Dispatchers.Main).launch {
|
||||||
|
superUserViewModel.fetchAppList()
|
||||||
|
}
|
||||||
|
|
||||||
Platform.setHiddenApiExemptions()
|
Platform.setHiddenApiExemptions()
|
||||||
|
|
||||||
val context = this
|
val context = this
|
||||||
@@ -53,4 +67,6 @@ class KernelSUApplication : Application() {
|
|||||||
)
|
)
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
override val viewModelStore: ViewModelStore
|
||||||
|
get() = appViewModelStore
|
||||||
}
|
}
|
||||||
@@ -3,14 +3,13 @@ package com.sukisu.ultra.ui.viewmodel
|
|||||||
import android.content.*
|
import android.content.*
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.disk.DiskCache
|
|
||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import com.sukisu.ultra.ksuApp
|
import com.sukisu.ultra.ksuApp
|
||||||
import com.sukisu.ultra.ui.KsuService
|
import com.sukisu.ultra.ui.KsuService
|
||||||
@@ -66,7 +65,15 @@ enum class SortType(val displayNameRes: Int, val persistKey: String) {
|
|||||||
class SuperUserViewModel : ViewModel() {
|
class SuperUserViewModel : ViewModel() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SuperUserViewModel"
|
private const val TAG = "SuperUserViewModel"
|
||||||
|
private val appsLock = Any()
|
||||||
var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getAppIconDrawable(context: Context, packageName: String): Drawable? {
|
||||||
|
val appList = synchronized(appsLock) { apps }
|
||||||
|
val appDetail = appList.find { it.packageName == packageName }
|
||||||
|
return appDetail?.packageInfo?.applicationInfo?.loadIcon(context.packageManager)
|
||||||
|
}
|
||||||
private const val PREFS_NAME = "settings"
|
private const val PREFS_NAME = "settings"
|
||||||
private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps"
|
private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps"
|
||||||
private const val KEY_SELECTED_CATEGORY = "selected_category"
|
private const val KEY_SELECTED_CATEGORY = "selected_category"
|
||||||
@@ -421,7 +428,7 @@ class SuperUserViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopKsuService() {
|
private fun stopKsuService() {
|
||||||
serviceConnection?.let { connection ->
|
serviceConnection?.let { _ ->
|
||||||
try {
|
try {
|
||||||
val intent = Intent(ksuApp, KsuService::class.java)
|
val intent = Intent(ksuApp, KsuService::class.java)
|
||||||
com.topjohnwu.superuser.ipc.RootService.stop(intent)
|
com.topjohnwu.superuser.ipc.RootService.stop(intent)
|
||||||
@@ -466,6 +473,10 @@ class SuperUserViewModel : ViewModel() {
|
|||||||
loadingProgress = start.toFloat() / total
|
loadingProgress = start.toFloat() / total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized(appsLock) {
|
||||||
|
apps
|
||||||
|
}
|
||||||
|
|
||||||
stopKsuService()
|
stopKsuService()
|
||||||
|
|
||||||
appListMutex.withLock {
|
appListMutex.withLock {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.sukisu.ultra.ui.webui;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.util.LruCache;
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel;
|
||||||
|
|
||||||
|
public class AppIconUtil {
|
||||||
|
// Limit cache size to 200 icons
|
||||||
|
private static final int CACHE_SIZE = 200;
|
||||||
|
private static final LruCache<String, Bitmap> iconCache = new LruCache<>(CACHE_SIZE);
|
||||||
|
|
||||||
|
public static synchronized Bitmap loadAppIconSync(Context context, String packageName, int sizePx) {
|
||||||
|
Bitmap cached = iconCache.get(packageName);
|
||||||
|
if (cached != null) return cached;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Drawable drawable = SuperUserViewModel.getAppIconDrawable(context, packageName);
|
||||||
|
if (drawable == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Bitmap raw = drawableToBitmap(drawable, sizePx);
|
||||||
|
Bitmap icon = Bitmap.createScaledBitmap(raw, sizePx, sizePx, true);
|
||||||
|
if (raw != icon) raw.recycle();
|
||||||
|
iconCache.put(packageName, icon);
|
||||||
|
return icon;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bitmap drawableToBitmap(Drawable drawable, int size) {
|
||||||
|
if (drawable instanceof BitmapDrawable) return ((BitmapDrawable) drawable).getBitmap();
|
||||||
|
|
||||||
|
int width = drawable.getIntrinsicWidth() > 0 ? drawable.getIntrinsicWidth() : size;
|
||||||
|
int height = drawable.getIntrinsicHeight() > 0 ? drawable.getIntrinsicHeight() : size;
|
||||||
|
|
||||||
|
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||||
|
Canvas canvas = new Canvas(bmp);
|
||||||
|
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||||
|
drawable.draw(canvas);
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,18 +11,23 @@ import android.webkit.WebView
|
|||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.viewModels
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.webkit.WebViewAssetLoader
|
import androidx.webkit.WebViewAssetLoader
|
||||||
import com.dergoogler.mmrl.platform.model.ModId
|
import com.dergoogler.mmrl.platform.model.ModId
|
||||||
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
||||||
import com.sukisu.ultra.ui.util.createRootShell
|
import com.sukisu.ultra.ui.util.createRootShell
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
class WebUIActivity : ComponentActivity() {
|
class WebUIActivity : ComponentActivity() {
|
||||||
private val rootShell by lazy { createRootShell(true) }
|
private val rootShell by lazy { createRootShell(true) }
|
||||||
|
private val superUserViewModel: SuperUserViewModel by viewModels()
|
||||||
private var webView = null as WebView?
|
private var webView = null as WebView?
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -35,6 +40,10 @@ class WebUIActivity : ComponentActivity() {
|
|||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
superUserViewModel.fetchAppList()
|
||||||
|
}
|
||||||
|
|
||||||
val moduleId = intent.getStringExtra("id") ?: finishAndRemoveTask().let { return }
|
val moduleId = intent.getStringExtra("id") ?: finishAndRemoveTask().let { return }
|
||||||
val name = intent.getStringExtra("name") ?: finishAndRemoveTask().let { return }
|
val name = intent.getStringExtra("name") ?: finishAndRemoveTask().let { return }
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||||
@@ -64,7 +73,21 @@ class WebUIActivity : ComponentActivity() {
|
|||||||
view: WebView,
|
view: WebView,
|
||||||
request: WebResourceRequest
|
request: WebResourceRequest
|
||||||
): WebResourceResponse? {
|
): WebResourceResponse? {
|
||||||
return webViewAssetLoader.shouldInterceptRequest(request.url)
|
val url = request.url
|
||||||
|
// Handle ksu://icon/[packageName] to serve app icon via WebView
|
||||||
|
if (url.scheme.equals("ksu", ignoreCase = true) && url.host.equals("icon", ignoreCase = true)) {
|
||||||
|
val packageName = url.path?.substring(1)
|
||||||
|
if (!packageName.isNullOrEmpty()) {
|
||||||
|
val icon = AppIconUtil.loadAppIconSync(this@WebUIActivity, packageName, 512)
|
||||||
|
if (icon != null) {
|
||||||
|
val stream = java.io.ByteArrayOutputStream()
|
||||||
|
icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, stream)
|
||||||
|
val inputStream = java.io.ByteArrayInputStream(stream.toByteArray())
|
||||||
|
return WebResourceResponse("image/png", null, inputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return webViewAssetLoader.shouldInterceptRequest(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
package com.sukisu.ultra.ui.webui
|
package com.sukisu.ultra.ui.webui
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.pm.ApplicationInfo
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import com.dergoogler.mmrl.webui.interfaces.WXInterface
|
import com.dergoogler.mmrl.webui.interfaces.WXInterface
|
||||||
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
||||||
import com.dergoogler.mmrl.webui.model.JavaScriptInterface
|
import com.dergoogler.mmrl.webui.model.JavaScriptInterface
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
import com.sukisu.ultra.ui.util.*
|
import com.sukisu.ultra.ui.util.*
|
||||||
import com.topjohnwu.superuser.CallbackList
|
import com.topjohnwu.superuser.CallbackList
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
@@ -138,7 +141,7 @@ class WebViewInterface(
|
|||||||
|
|
||||||
completableFuture.thenAccept { result ->
|
completableFuture.thenAccept { result ->
|
||||||
val emitExitCode =
|
val emitExitCode =
|
||||||
"(function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \${e}`); } })();"
|
$$"(function() { try { $${callbackFunc}.emit('exit', $${result.code}); } catch(e) { console.error(`emitExit error: ${e}`); } })();"
|
||||||
webView.post {
|
webView.post {
|
||||||
webView.evaluateJavascript(emitExitCode, null)
|
webView.evaluateJavascript(emitExitCode, null)
|
||||||
}
|
}
|
||||||
@@ -203,6 +206,56 @@ class WebViewInterface(
|
|||||||
return currentModuleInfo.toString()
|
return currentModuleInfo.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun listPackages(type: String): String {
|
||||||
|
val packageNames = SuperUserViewModel.apps
|
||||||
|
.filter { appInfo ->
|
||||||
|
val flags = appInfo.packageInfo.applicationInfo?.flags ?: 0
|
||||||
|
when (type.lowercase()) {
|
||||||
|
"system" -> (flags and ApplicationInfo.FLAG_SYSTEM) != 0
|
||||||
|
"user" -> (flags and ApplicationInfo.FLAG_SYSTEM) == 0
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { it.packageName }
|
||||||
|
.sorted()
|
||||||
|
|
||||||
|
val jsonArray = JSONArray()
|
||||||
|
for (pkgName in packageNames) {
|
||||||
|
jsonArray.put(pkgName)
|
||||||
|
}
|
||||||
|
return jsonArray.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun getPackagesInfo(packageNamesJson: String): String {
|
||||||
|
val packageNames = JSONArray(packageNamesJson)
|
||||||
|
val jsonArray = JSONArray()
|
||||||
|
val appMap = SuperUserViewModel.apps.associateBy { it.packageName }
|
||||||
|
for (i in 0 until packageNames.length()) {
|
||||||
|
val pkgName = packageNames.getString(i)
|
||||||
|
val appInfo = appMap[pkgName]
|
||||||
|
if (appInfo != null) {
|
||||||
|
val pkg = appInfo.packageInfo
|
||||||
|
val app = pkg.applicationInfo
|
||||||
|
val obj = JSONObject()
|
||||||
|
obj.put("packageName", pkg.packageName)
|
||||||
|
obj.put("versionName", pkg.versionName ?: "")
|
||||||
|
obj.put("versionCode", PackageInfoCompat.getLongVersionCode(pkg))
|
||||||
|
obj.put("appLabel", appInfo.label)
|
||||||
|
obj.put("isSystem", if (app != null) ((app.flags and ApplicationInfo.FLAG_SYSTEM) != 0) else JSONObject.NULL)
|
||||||
|
obj.put("uid", app?.uid ?: JSONObject.NULL)
|
||||||
|
jsonArray.put(obj)
|
||||||
|
} else {
|
||||||
|
val obj = JSONObject()
|
||||||
|
obj.put("packageName", pkgName)
|
||||||
|
obj.put("error", "Package not found or inaccessible")
|
||||||
|
jsonArray.put(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonArray.toString()
|
||||||
|
}
|
||||||
|
|
||||||
// =================== KPM支持 =============================
|
// =================== KPM支持 =============================
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
|
|||||||
Reference in New Issue
Block a user