You've already forked KsuWebUIStandalone
mirror of
https://github.com/5ec1cff/KsuWebUIStandalone.git
synced 2025-09-06 06:37:11 +00:00
use global FileSystemManager
This commit is contained in:
@@ -1,11 +1,22 @@
|
||||
package io.github.a13e300.ksuwebui
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class App : Application() {
|
||||
companion object {
|
||||
lateinit var instance: App
|
||||
private set
|
||||
val executor by lazy { Executors.newCachedThreadPool() }
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
instance = this
|
||||
Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,88 @@
|
||||
package io.github.a13e300.ksuwebui
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import androidx.annotation.MainThread
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
|
||||
class FileSystemService : RootService() {
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return FileSystemManager.getService()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onServiceAvailable(fs: FileSystemManager)
|
||||
fun onLaunchFailed()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private sealed class Status {
|
||||
data object Uninitialized : Status()
|
||||
data object CheckRoot : Status()
|
||||
data class ServiceAvailable(val fs: FileSystemManager) : Status()
|
||||
}
|
||||
|
||||
private var status: Status = Status.Uninitialized
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(p0: ComponentName, p1: IBinder) {
|
||||
val fs = FileSystemManager.getRemote(p1)
|
||||
status = Status.ServiceAvailable(fs)
|
||||
pendingListeners.forEach { l ->
|
||||
l.onServiceAvailable(fs)
|
||||
pendingListeners.remove(l)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(p0: ComponentName) {
|
||||
status = Status.Uninitialized
|
||||
}
|
||||
|
||||
}
|
||||
private val pendingListeners = CopyOnWriteArraySet<Listener>()
|
||||
|
||||
@MainThread
|
||||
fun start(listener: Listener) {
|
||||
(status as? Status.ServiceAvailable)?.let {
|
||||
listener.onServiceAvailable(it.fs)
|
||||
return
|
||||
}
|
||||
pendingListeners.add(listener)
|
||||
if (status == Status.Uninitialized) {
|
||||
checkRoot()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkRoot() {
|
||||
status = Status.CheckRoot
|
||||
App.executor.submit {
|
||||
val isRoot = Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER).build().use {
|
||||
it.isRoot
|
||||
}
|
||||
App.handler.post {
|
||||
if (isRoot) {
|
||||
launchService()
|
||||
} else {
|
||||
status = Status.Uninitialized
|
||||
pendingListeners.forEach { l ->
|
||||
l.onLaunchFailed()
|
||||
pendingListeners.remove(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchService() {
|
||||
bind(Intent(App.instance, FileSystemService::class.java), connection)
|
||||
}
|
||||
|
||||
fun removeListener(listener: Listener) {
|
||||
pendingListeners.remove(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package io.github.a13e300.ksuwebui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.ViewGroup
|
||||
@@ -18,17 +15,12 @@ import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import io.github.a13e300.ksuwebui.databinding.ActivityMainBinding
|
||||
import io.github.a13e300.ksuwebui.databinding.ItemModuleBinding
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private var connection: ServiceConnection? = null
|
||||
private var rootFilesystem: FileSystemManager? = null
|
||||
class MainActivity : AppCompatActivity(), FileSystemService.Listener {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private var moduleList = emptyList<Module>()
|
||||
private lateinit var adapter: Adapter
|
||||
@@ -101,13 +93,11 @@ class MainActivity : AppCompatActivity() {
|
||||
adapter.notifyDataSetChanged()
|
||||
binding.info.setText(R.string.loading)
|
||||
binding.info.isVisible = true
|
||||
fetchModuleList()
|
||||
FileSystemService.start(this)
|
||||
}
|
||||
|
||||
private fun fetchModuleList() {
|
||||
thread {
|
||||
if (!maybeStartRootService()) return@thread
|
||||
val fs = rootFilesystem!!
|
||||
override fun onServiceAvailable(fs: FileSystemManager) {
|
||||
App.executor.submit {
|
||||
val mods = mutableListOf<Module>()
|
||||
val showDisabled = prefs.getBoolean("show_disabled", false)
|
||||
fs.getFile("/data/adb/modules").listFiles()!!.forEach { f ->
|
||||
@@ -147,6 +137,14 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLaunchFailed() {
|
||||
moduleList = emptyList()
|
||||
adapter.notifyDataSetChanged()
|
||||
binding.info.setText(R.string.please_grant_root)
|
||||
binding.info.isVisible = true
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
}
|
||||
|
||||
data class Module(val name: String, val id: String, val desc: String, val author: String, val version: String)
|
||||
|
||||
class ViewHolder(val binding: ItemModuleBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
@@ -183,45 +181,8 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
}
|
||||
|
||||
private fun maybeStartRootService(): Boolean {
|
||||
if (connection == null) {
|
||||
val isRoot = Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER).build().use {
|
||||
it.isRoot
|
||||
}
|
||||
|
||||
if (!isRoot) {
|
||||
runOnUiThread {
|
||||
moduleList = emptyList()
|
||||
adapter.notifyDataSetChanged()
|
||||
binding.info.setText(R.string.please_grant_root)
|
||||
binding.info.isVisible = true
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(p0: ComponentName, p1: IBinder) {
|
||||
rootFilesystem = FileSystemManager.getRemote(p1)
|
||||
fetchModuleList()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(p0: ComponentName) {
|
||||
rootFilesystem = null
|
||||
connection = null
|
||||
}
|
||||
|
||||
}
|
||||
runOnUiThread {
|
||||
RootService.bind(Intent(this, FileSystemService::class.java), connection!!)
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
connection?.let { RootService.unbind(it) }
|
||||
FileSystemService.removeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.webkit.WebViewAssetLoader;
|
||||
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -37,8 +35,8 @@ import java.util.zip.GZIPInputStream;
|
||||
* .build();
|
||||
* </pre>
|
||||
*/
|
||||
public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler {
|
||||
private static final String TAG = "SuFilePathHandler";
|
||||
public final class RemoteFsPathHandler implements WebViewAssetLoader.PathHandler {
|
||||
private static final String TAG = "FsServicePathHandler";
|
||||
|
||||
/**
|
||||
* Default value to be used as MIME type if guessing MIME type failed.
|
||||
@@ -57,7 +55,7 @@ public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler {
|
||||
@NonNull
|
||||
private final File mDirectory;
|
||||
|
||||
private final Shell mShell;
|
||||
private final FileSystemManager mFs;
|
||||
|
||||
/**
|
||||
* Creates PathHandler for app's internal storage.
|
||||
@@ -81,14 +79,14 @@ public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler {
|
||||
* which files can be loaded.
|
||||
* @throws IllegalArgumentException if the directory is not allowed.
|
||||
*/
|
||||
public SuFilePathHandler(@NonNull Context context, @NonNull File directory, Shell rootShell) {
|
||||
public RemoteFsPathHandler(@NonNull Context context, @NonNull File directory, FileSystemManager fs) {
|
||||
try {
|
||||
mDirectory = new File(getCanonicalDirPath(directory));
|
||||
if (!isAllowedInternalStorageDir(context)) {
|
||||
throw new IllegalArgumentException("The given directory \"" + directory
|
||||
+ "\" doesn't exist under an allowed app internal storage directory");
|
||||
}
|
||||
mShell = rootShell;
|
||||
mFs = fs;
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to resolve the canonical path for the given directory: "
|
||||
@@ -133,7 +131,7 @@ public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler {
|
||||
try {
|
||||
File file = getCanonicalFileIfChild(mDirectory, path);
|
||||
if (file != null) {
|
||||
InputStream is = openFile(file, mShell);
|
||||
InputStream is = openFile(file, mFs);
|
||||
String mimeType = guessMimeType(path);
|
||||
return new WebResourceResponse(mimeType, null, is);
|
||||
} else {
|
||||
@@ -169,11 +167,8 @@ public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler {
|
||||
return path.endsWith(".svgz") ? new GZIPInputStream(stream) : stream;
|
||||
}
|
||||
|
||||
public static InputStream openFile(@NonNull File file, @NonNull Shell shell) throws IOException {
|
||||
SuFile suFile = new SuFile(file.getAbsolutePath());
|
||||
suFile.setShell(shell);
|
||||
InputStream fis = SuFileInputStream.open(suFile);
|
||||
return handleSvgzStream(file.getPath(), fis);
|
||||
public static InputStream openFile(@NonNull File file, @NonNull FileSystemManager fs) throws IOException {
|
||||
return handleSvgzStream(file.getPath(), fs.getFile(file.getAbsolutePath()).newInputStream());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -9,20 +9,22 @@ import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.nio.FileSystemManager
|
||||
import java.io.File
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
class WebUIActivity : ComponentActivity() {
|
||||
class WebUIActivity : ComponentActivity(), FileSystemService.Listener {
|
||||
private lateinit var webviewInterface: WebViewInterface
|
||||
|
||||
private var rootShell: Shell? = null
|
||||
private lateinit var webView: WebView
|
||||
private lateinit var moduleDir: String
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Enable edge to edge
|
||||
@@ -33,8 +35,12 @@ class WebUIActivity : ComponentActivity() {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val moduleId = intent.getStringExtra("id")!!
|
||||
val name = intent.getStringExtra("name")!!
|
||||
val moduleId = intent.getStringExtra("id")
|
||||
if (moduleId == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
val name = intent.getStringExtra("name") ?: moduleId
|
||||
if (name.isNotEmpty()) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -48,27 +54,9 @@ class WebUIActivity : ComponentActivity() {
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
WebView.setWebContentsDebuggingEnabled(prefs.getBoolean("enable_web_debugging", BuildConfig.DEBUG))
|
||||
|
||||
val moduleDir = "/data/adb/modules/${moduleId}"
|
||||
val webRoot = File("${moduleDir}/webroot")
|
||||
val rootShell = createRootShell(true).also { this.rootShell = it }
|
||||
val webViewAssetLoader = WebViewAssetLoader.Builder()
|
||||
.setDomain("mui.kernelsu.org")
|
||||
.addPathHandler(
|
||||
"/",
|
||||
SuFilePathHandler(this, webRoot, rootShell)
|
||||
)
|
||||
.build()
|
||||
moduleDir = "/data/adb/modules/$moduleId"
|
||||
|
||||
val webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
return webViewAssetLoader.shouldInterceptRequest(request.url)
|
||||
}
|
||||
}
|
||||
|
||||
val webView = WebView(this).apply {
|
||||
webView = WebView(this).apply {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
|
||||
val inset = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
view.updateLayoutParams<MarginLayoutParams> {
|
||||
@@ -83,16 +71,51 @@ class WebUIActivity : ComponentActivity() {
|
||||
settings.domStorageEnabled = true
|
||||
settings.allowFileAccess = false
|
||||
webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir)
|
||||
}
|
||||
|
||||
setContentView(webView)
|
||||
FileSystemService.start(this)
|
||||
}
|
||||
|
||||
private fun setupWebview(fs: FileSystemManager) {
|
||||
val webRoot = File("$moduleDir/webroot")
|
||||
val webViewAssetLoader = WebViewAssetLoader.Builder()
|
||||
.setDomain("mui.kernelsu.org")
|
||||
.addPathHandler(
|
||||
"/",
|
||||
RemoteFsPathHandler(
|
||||
this,
|
||||
webRoot,
|
||||
fs
|
||||
)
|
||||
)
|
||||
.build()
|
||||
val webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
return webViewAssetLoader.shouldInterceptRequest(request.url)
|
||||
}
|
||||
}
|
||||
webView.apply {
|
||||
addJavascriptInterface(webviewInterface, "ksu")
|
||||
setWebViewClient(webViewClient)
|
||||
loadUrl("https://mui.kernelsu.org/index.html")
|
||||
}
|
||||
}
|
||||
|
||||
setContentView(webView)
|
||||
override fun onServiceAvailable(fs: FileSystemManager) {
|
||||
setupWebview(fs)
|
||||
}
|
||||
|
||||
override fun onLaunchFailed() {
|
||||
Toast.makeText(this, R.string.please_grant_root, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
runCatching { rootShell?.close() }
|
||||
FileSystemService.removeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user