use global FileSystemManager

This commit is contained in:
5ec1cff
2024-10-04 15:07:37 +08:00
parent a8f363c12e
commit 136e373ad1
5 changed files with 159 additions and 93 deletions

View File

@@ -1,11 +1,22 @@
package io.github.a13e300.ksuwebui package io.github.a13e300.ksuwebui
import android.app.Application import android.app.Application
import android.os.Handler
import android.os.Looper
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import java.util.concurrent.Executors
class App : Application() { 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() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this
Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER)) Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER))
} }
} }

View File

@@ -1,12 +1,88 @@
package io.github.a13e300.ksuwebui package io.github.a13e300.ksuwebui
import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder import android.os.IBinder
import androidx.annotation.MainThread
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ipc.RootService import com.topjohnwu.superuser.ipc.RootService
import com.topjohnwu.superuser.nio.FileSystemManager import com.topjohnwu.superuser.nio.FileSystemManager
import java.util.concurrent.CopyOnWriteArraySet
class FileSystemService : RootService() { class FileSystemService : RootService() {
override fun onBind(intent: Intent): IBinder { override fun onBind(intent: Intent): IBinder {
return FileSystemManager.getService() 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)
}
}
} }

View File

@@ -1,13 +1,10 @@
package io.github.a13e300.ksuwebui package io.github.a13e300.ksuwebui
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.ViewGroup import android.view.ViewGroup
@@ -18,17 +15,12 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ipc.RootService
import com.topjohnwu.superuser.nio.FileSystemManager import com.topjohnwu.superuser.nio.FileSystemManager
import io.github.a13e300.ksuwebui.databinding.ActivityMainBinding import io.github.a13e300.ksuwebui.databinding.ActivityMainBinding
import io.github.a13e300.ksuwebui.databinding.ItemModuleBinding import io.github.a13e300.ksuwebui.databinding.ItemModuleBinding
import kotlin.concurrent.thread
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity(), FileSystemService.Listener {
private var connection: ServiceConnection? = null
private var rootFilesystem: FileSystemManager? = null
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private var moduleList = emptyList<Module>() private var moduleList = emptyList<Module>()
private lateinit var adapter: Adapter private lateinit var adapter: Adapter
@@ -101,13 +93,11 @@ class MainActivity : AppCompatActivity() {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
binding.info.setText(R.string.loading) binding.info.setText(R.string.loading)
binding.info.isVisible = true binding.info.isVisible = true
fetchModuleList() FileSystemService.start(this)
} }
private fun fetchModuleList() { override fun onServiceAvailable(fs: FileSystemManager) {
thread { App.executor.submit {
if (!maybeStartRootService()) return@thread
val fs = rootFilesystem!!
val mods = mutableListOf<Module>() val mods = mutableListOf<Module>()
val showDisabled = prefs.getBoolean("show_disabled", false) val showDisabled = prefs.getBoolean("show_disabled", false)
fs.getFile("/data/adb/modules").listFiles()!!.forEach { f -> 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) 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) 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() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
connection?.let { RootService.unbind(it) } FileSystemService.removeListener(this)
} }
} }

View File

@@ -8,9 +8,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import androidx.webkit.WebViewAssetLoader; import androidx.webkit.WebViewAssetLoader;
import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.nio.FileSystemManager;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -37,8 +35,8 @@ import java.util.zip.GZIPInputStream;
* .build(); * .build();
* </pre> * </pre>
*/ */
public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler { public final class RemoteFsPathHandler implements WebViewAssetLoader.PathHandler {
private static final String TAG = "SuFilePathHandler"; private static final String TAG = "FsServicePathHandler";
/** /**
* Default value to be used as MIME type if guessing MIME type failed. * 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 @NonNull
private final File mDirectory; private final File mDirectory;
private final Shell mShell; private final FileSystemManager mFs;
/** /**
* Creates PathHandler for app's internal storage. * Creates PathHandler for app's internal storage.
@@ -81,14 +79,14 @@ public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler {
* which files can be loaded. * which files can be loaded.
* @throws IllegalArgumentException if the directory is not allowed. * @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 { try {
mDirectory = new File(getCanonicalDirPath(directory)); mDirectory = new File(getCanonicalDirPath(directory));
if (!isAllowedInternalStorageDir(context)) { if (!isAllowedInternalStorageDir(context)) {
throw new IllegalArgumentException("The given directory \"" + directory throw new IllegalArgumentException("The given directory \"" + directory
+ "\" doesn't exist under an allowed app internal storage directory"); + "\" doesn't exist under an allowed app internal storage directory");
} }
mShell = rootShell; mFs = fs;
} catch (IOException e) { } catch (IOException e) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Failed to resolve the canonical path for the given directory: " "Failed to resolve the canonical path for the given directory: "
@@ -133,7 +131,7 @@ public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler {
try { try {
File file = getCanonicalFileIfChild(mDirectory, path); File file = getCanonicalFileIfChild(mDirectory, path);
if (file != null) { if (file != null) {
InputStream is = openFile(file, mShell); InputStream is = openFile(file, mFs);
String mimeType = guessMimeType(path); String mimeType = guessMimeType(path);
return new WebResourceResponse(mimeType, null, is); return new WebResourceResponse(mimeType, null, is);
} else { } else {
@@ -169,11 +167,8 @@ public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler {
return path.endsWith(".svgz") ? new GZIPInputStream(stream) : stream; return path.endsWith(".svgz") ? new GZIPInputStream(stream) : stream;
} }
public static InputStream openFile(@NonNull File file, @NonNull Shell shell) throws IOException { public static InputStream openFile(@NonNull File file, @NonNull FileSystemManager fs) throws IOException {
SuFile suFile = new SuFile(file.getAbsolutePath()); return handleSvgzStream(file.getPath(), fs.getFile(file.getAbsolutePath()).newInputStream());
suFile.setShell(shell);
InputStream fis = SuFileInputStream.open(suFile);
return handleSvgzStream(file.getPath(), fis);
} }
/** /**

View File

@@ -9,20 +9,22 @@ import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse import android.webkit.WebResourceResponse
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
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.webkit.WebViewAssetLoader import androidx.webkit.WebViewAssetLoader
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.nio.FileSystemManager
import java.io.File import java.io.File
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
class WebUIActivity : ComponentActivity() { class WebUIActivity : ComponentActivity(), FileSystemService.Listener {
private lateinit var webviewInterface: WebViewInterface 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?) { override fun onCreate(savedInstanceState: Bundle?) {
// Enable edge to edge // Enable edge to edge
@@ -33,8 +35,12 @@ class WebUIActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val moduleId = intent.getStringExtra("id")!! val moduleId = intent.getStringExtra("id")
val name = intent.getStringExtra("name")!! if (moduleId == null) {
finish()
return
}
val name = intent.getStringExtra("name") ?: moduleId
if (name.isNotEmpty()) { if (name.isNotEmpty()) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@@ -48,27 +54,9 @@ class WebUIActivity : ComponentActivity() {
val prefs = getSharedPreferences("settings", MODE_PRIVATE) val prefs = getSharedPreferences("settings", MODE_PRIVATE)
WebView.setWebContentsDebuggingEnabled(prefs.getBoolean("enable_web_debugging", BuildConfig.DEBUG)) WebView.setWebContentsDebuggingEnabled(prefs.getBoolean("enable_web_debugging", BuildConfig.DEBUG))
val moduleDir = "/data/adb/modules/${moduleId}" 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()
val webViewClient = object : WebViewClient() { webView = WebView(this).apply {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
return webViewAssetLoader.shouldInterceptRequest(request.url)
}
}
val webView = WebView(this).apply {
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets -> ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
val inset = insets.getInsets(WindowInsetsCompat.Type.systemBars()) val inset = insets.getInsets(WindowInsetsCompat.Type.systemBars())
view.updateLayoutParams<MarginLayoutParams> { view.updateLayoutParams<MarginLayoutParams> {
@@ -83,16 +71,51 @@ class WebUIActivity : ComponentActivity() {
settings.domStorageEnabled = true settings.domStorageEnabled = true
settings.allowFileAccess = false settings.allowFileAccess = false
webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir) 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") addJavascriptInterface(webviewInterface, "ksu")
setWebViewClient(webViewClient) setWebViewClient(webViewClient)
loadUrl("https://mui.kernelsu.org/index.html") 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() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
runCatching { rootShell?.close() } FileSystemService.removeListener(this)
} }
} }