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
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))
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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());
}
/**

View File

@@ -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)
}
}