diff --git a/app/src/main/java/io/github/a13e300/ksuwebui/App.kt b/app/src/main/java/io/github/a13e300/ksuwebui/App.kt index 7af2f73..e1404da 100644 --- a/app/src/main/java/io/github/a13e300/ksuwebui/App.kt +++ b/app/src/main/java/io/github/a13e300/ksuwebui/App.kt @@ -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)) } } diff --git a/app/src/main/java/io/github/a13e300/ksuwebui/FileSystemService.kt b/app/src/main/java/io/github/a13e300/ksuwebui/FileSystemService.kt index 1aec5db..28b7d0b 100644 --- a/app/src/main/java/io/github/a13e300/ksuwebui/FileSystemService.kt +++ b/app/src/main/java/io/github/a13e300/ksuwebui/FileSystemService.kt @@ -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() + + @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) + } + } } diff --git a/app/src/main/java/io/github/a13e300/ksuwebui/MainActivity.kt b/app/src/main/java/io/github/a13e300/ksuwebui/MainActivity.kt index e643e23..4ccf330 100644 --- a/app/src/main/java/io/github/a13e300/ksuwebui/MainActivity.kt +++ b/app/src/main/java/io/github/a13e300/ksuwebui/MainActivity.kt @@ -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() 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() 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) } } diff --git a/app/src/main/java/io/github/a13e300/ksuwebui/SuFilePathHandler.java b/app/src/main/java/io/github/a13e300/ksuwebui/RemoteFsPathHandler.java similarity index 91% rename from app/src/main/java/io/github/a13e300/ksuwebui/SuFilePathHandler.java rename to app/src/main/java/io/github/a13e300/ksuwebui/RemoteFsPathHandler.java index 6e7ff8e..af4ce8c 100644 --- a/app/src/main/java/io/github/a13e300/ksuwebui/SuFilePathHandler.java +++ b/app/src/main/java/io/github/a13e300/ksuwebui/RemoteFsPathHandler.java @@ -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(); * */ -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()); } /** diff --git a/app/src/main/java/io/github/a13e300/ksuwebui/WebUIActivity.kt b/app/src/main/java/io/github/a13e300/ksuwebui/WebUIActivity.kt index 810c3f4..1d97ea1 100644 --- a/app/src/main/java/io/github/a13e300/ksuwebui/WebUIActivity.kt +++ b/app/src/main/java/io/github/a13e300/ksuwebui/WebUIActivity.kt @@ -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 { @@ -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) } }