diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3238f10..7e9be09 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,11 +17,13 @@ android { buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = true + isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + signingConfig = signingConfigs["debug"] } } compileOptions { @@ -33,6 +35,7 @@ android { } buildFeatures { buildConfig = true + viewBinding = true } } @@ -46,4 +49,5 @@ dependencies { implementation(libs.com.github.topjohnwu.libsu.core) implementation(libs.com.github.topjohnwu.libsu.service) implementation(libs.com.github.topjohnwu.libsu.io) + implementation(libs.androidx.swiperefreshlayout) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ad549e8..3dd94e0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,11 +15,19 @@ android:theme="@style/Theme.KsuWebUI" android:networkSecurityConfig="@xml/network_security_config" tools:targetApi="31" > + + + + + + + + android:documentLaunchMode="intoExisting" /> \ No newline at end of file diff --git a/app/src/main/java/io/github/a13e300/ksuwebui/FileSystemService.kt b/app/src/main/java/io/github/a13e300/ksuwebui/FileSystemService.kt new file mode 100644 index 0000000..2753f6b --- /dev/null +++ b/app/src/main/java/io/github/a13e300/ksuwebui/FileSystemService.kt @@ -0,0 +1,12 @@ +package io.github.a13e300.ksuwebui + +import android.content.Intent +import android.os.IBinder +import com.topjohnwu.superuser.ipc.RootService +import com.topjohnwu.superuser.nio.FileSystemManager + +class FileSystemService : RootService() { + override fun onBind(intent: Intent): IBinder { + return FileSystemManager.getService() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/a13e300/ksuwebui/MainActivity.kt b/app/src/main/java/io/github/a13e300/ksuwebui/MainActivity.kt new file mode 100644 index 0000000..b295c7b --- /dev/null +++ b/app/src/main/java/io/github/a13e300/ksuwebui/MainActivity.kt @@ -0,0 +1,187 @@ +package io.github.a13e300.ksuwebui + +import android.content.ComponentName +import android.content.Intent +import android.content.ServiceConnection +import android.net.Uri +import android.os.Bundle +import android.os.IBinder +import android.view.LayoutInflater +import android.view.Menu +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.internal.MainShell +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 + +class MainActivity : AppCompatActivity() { + private var connection: ServiceConnection? = null + private var rootFilesystem: FileSystemManager? = null + private lateinit var binding: ActivityMainBinding + private var moduleList = emptyList() + private lateinit var adapter: Adapter + private val prefs by lazy { getSharedPreferences("settings", MODE_PRIVATE) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + adapter = Adapter() + binding.list.adapter = adapter + setContentView(binding.root) + setSupportActionBar(binding.toolbar) + binding.swipeRefresh.setOnRefreshListener { + refresh() + } + binding.swipeRefresh.isRefreshing = true + refresh() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menuInflater.inflate(R.menu.main_menu, menu) + menu!!.findItem(R.id.enable_webview_debugging).apply { + isChecked = prefs.getBoolean("enable_web_debugging", BuildConfig.DEBUG) + setOnMenuItemClickListener { + val newValue = !it.isChecked + prefs.edit().putBoolean("enable_web_debugging", newValue).apply() + it.isChecked = newValue + true + } + } + return true + } + + private fun refresh() { + moduleList = emptyList() + adapter.notifyDataSetChanged() + binding.info.setText(R.string.loading) + binding.info.isVisible = true + fetchModuleList() + } + + private fun fetchModuleList() { + thread { + if (!maybeStartRootService()) return@thread + val fs = rootFilesystem!! + val mods = mutableListOf() + fs.getFile("/data/adb/modules").listFiles()!!.forEach { f -> + if (!f.isDirectory) return@forEach + if (!fs.getFile(f, "webroot").isDirectory) return@forEach + if (fs.getFile(f, "disable").exists()) return@forEach + var name = f.name + val id = f.name + var author = "?" + var version = "?" + var desc = "" + fs.getFile(f, "module.prop").newInputStream().bufferedReader().use { + it.lines().forEach { l -> + val ls = l.split("=", limit = 2) + if (ls.size == 2) { + if (ls[0] == "name") name = ls[1] + else if (ls[0] == "description") desc = ls[1] + else if (ls[0] == "author") author = ls[1] + else if (ls[0] == "version") version = ls[1] + } + + } + } + mods.add(Module(name, id, desc, author, version)) + } + runOnUiThread { + moduleList = mods + adapter.notifyDataSetChanged() + binding.swipeRefresh.isRefreshing = false + if (mods.isEmpty()) { + binding.info.setText(R.string.no_modules) + binding.info.isVisible = true + } else { + binding.info.isVisible = 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) + + inner class Adapter : RecyclerView.Adapter() { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + ItemModuleBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + } + + override fun getItemCount(): Int = moduleList.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = moduleList[position] + val id = item.id + val name = item.name + holder.binding.name.text = name + holder.binding.author.text = "Author: ${item.author}" + holder.binding.version.text = "Version: ${item.version}" + holder.binding.desc.text = item.desc + holder.binding.root.setOnClickListener { + startActivity( + Intent(this@MainActivity, WebUIActivity::class.java) + .setData(Uri.parse("ksuwebui://webui/$id")) + .putExtra("id", id) + .putExtra("name", name) + ) + } + } + + } + + 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 + } + + } + Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER)) + runOnUiThread { + RootService.bind(Intent(this, FileSystemService::class.java), connection!!) + } + return false + } + return true + } + + override fun onDestroy() { + super.onDestroy() + connection?.let { RootService.unbind(it) } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/github/a13e300/ksuwebui/Util.kt b/app/src/main/java/io/github/a13e300/ksuwebui/Util.kt index 7e9ea08..9b8e2d0 100644 --- a/app/src/main/java/io/github/a13e300/ksuwebui/Util.kt +++ b/app/src/main/java/io/github/a13e300/ksuwebui/Util.kt @@ -16,23 +16,9 @@ inline fun withNewRootShell( fun createRootShell(globalMnt: Boolean = false): Shell { Shell.enableVerboseLogging = BuildConfig.DEBUG val builder = Shell.Builder.create() - return try { - if (globalMnt) { - builder.build("su", "-g") + return if (globalMnt) { + builder.build("su", "-mm") } else { builder.build("su") } - } catch (e: Throwable) { - Log.w(TAG, "ksu failed: ", e) - try { - if (globalMnt) { - builder.build("su") - } else { - builder.build("su", "-mm") - } - } catch (e: Throwable) { - Log.e(TAG, "su failed: ", e) - builder.build("sh") - } - } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..56a9345 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_module.xml b/app/src/main/res/layout/item_module.xml new file mode 100644 index 0000000..8a56667 --- /dev/null +++ b/app/src/main/res/layout/item_module.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml new file mode 100644 index 0000000..b6240ed --- /dev/null +++ b/app/src/main/res/menu/main_menu.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 44ea19d..99ede9b 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,6 +1,6 @@ -