From be1b398c41f628edd742030dad89d03ac0853174 Mon Sep 17 00:00:00 2001 From: RhysBoxGameStudios Date: Mon, 10 Aug 2026 18:36:00 +0800 Subject: [PATCH] Portable App: full tool panel listing host apps (v1.2.0) --- .gitignore | 1 + README.md | 6 +- apps/portable/build.gradle.kts | 4 +- apps/portable/src/main/AndroidManifest.xml | 1 + .../console/portable/MainActivity.kt | 20 +-- .../console/portable/PortableActivity.kt | 161 ++++++++++++++++++ .../src/main/res/layout/activity_portable.xml | 36 ++++ .../src/main/res/layout/item_tool.xml | 26 +++ apps/portable/src/main/res/values/strings.xml | 6 +- docs/PROTOCOL.md | 12 ++ .../com/gostudios/console/sdk/Protocol.kt | 6 + 11 files changed, 256 insertions(+), 23 deletions(-) create mode 100644 apps/portable/src/main/java/com/gostudios/console/portable/PortableActivity.kt create mode 100644 apps/portable/src/main/res/layout/activity_portable.xml create mode 100644 apps/portable/src/main/res/layout/item_tool.xml diff --git a/.gitignore b/.gitignore index 0b23846..8465e71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Gradle .gradle/ +.kotlin/ build/ **/build/ local.properties diff --git a/README.md b/README.md index fc6489a..b818d50 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ which bundles four companion services on one device: | Feature | What it does | |---|---| -| šŸ–„ļø **Portable USB** | Find your GoConsoleOS USB host on Wi-Fi, launch it, open the USB Installer | +| šŸ–„ļø **Portable USB** | Find your GoConsoleOS USB host on Wi-Fi and open the **Portable App** panel — a full launcher for host tools: GoUsbMaker, USB Health, Cast, GoAI, GoStore and Screenshot (`tools.list` / `tools.run`) | | šŸ“” **GoConsoleOS Link** | LAN transport to browse and launch the host's game library over the same Wi-Fi (Steam Link style) | | šŸ›”ļø **USB Health** | Local on-device volume & health check plus the host's deep SMART report for portable USB drives (Android 13/16) | | šŸ“ŗ **GoConsoleOS Cast** | Screen-cast the device to the host / TV via the same transport | @@ -32,6 +32,10 @@ UDP :39100 discovery GCS hello → hosts reply with a JSON beacon TCP :39101 streaming control lines + length-prefixed binary frames (JPEG, USB, cast) ``` +Control messages include `hello`, `games.list`, `games.launch`, `usb.list`, +`pair`, `cast.start`/`cast.stop`, and the Portable App panel's `tools.list` +(fetch the remote tool catalogue) and `tools.run` (run a tool by id). + See [docs/PROTOCOL.md](docs/PROTOCOL.md) for the wire format and [docs/HOST.md](docs/HOST.md) for the Windows host service. diff --git a/apps/portable/build.gradle.kts b/apps/portable/build.gradle.kts index 023317e..1782859 100644 --- a/apps/portable/build.gradle.kts +++ b/apps/portable/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "com.gostudios.console.portable" minSdk = 23 targetSdk = 35 - versionCode = 18 - versionName = "1.1.0" + versionCode = 19 + versionName = "1.2.0" vectorDrawables { useSupportLibrary = true } } diff --git a/apps/portable/src/main/AndroidManifest.xml b/apps/portable/src/main/AndroidManifest.xml index c79dd3c..32769f4 100644 --- a/apps/portable/src/main/AndroidManifest.xml +++ b/apps/portable/src/main/AndroidManifest.xml @@ -35,6 +35,7 @@ + diff --git a/apps/portable/src/main/java/com/gostudios/console/portable/MainActivity.kt b/apps/portable/src/main/java/com/gostudios/console/portable/MainActivity.kt index 44a8960..dbfe9db 100644 --- a/apps/portable/src/main/java/com/gostudios/console/portable/MainActivity.kt +++ b/apps/portable/src/main/java/com/gostudios/console/portable/MainActivity.kt @@ -8,9 +8,6 @@ import com.gostudios.console.portable.cast.MediaProjectionActivity import com.gostudios.console.sdk.ConsoleHost import com.gostudios.console.sdk.DeviceServer import com.gostudios.console.sdk.Discovery -import com.gostudios.console.sdk.LinkClient -import com.gostudios.console.sdk.Protocol -import org.json.JSONObject /** * Home screen of GoConsoleOS Portable. Discovers a GoConsoleOS host on the LAN @@ -40,7 +37,7 @@ class MainActivity : AppCompatActivity() { activeHost?.let { startActivity(MediaProjectionActivity.intent(this, it)) } } findViewById(R.id.btnBoot).setOnClickListener { - activeHost?.let { openUsbInstaller(it) } + activeHost?.let { startActivity(PortableActivity.intent(this, it)) } } scanner = Discovery.Scanner { host -> @@ -58,21 +55,6 @@ class MainActivity : AppCompatActivity() { scanner?.start() } - /** Ask the host to open its bundled GoConsoleOS USB Installer tool. */ - private fun openUsbInstaller(host: ConsoleHost) { - var client: LinkClient? = null - client = LinkClient(host, object : LinkClient.Listener { - override fun onConnected(h: ConsoleHost) { - client?.sendControl(Protocol.MSG_PAIR, JSONObject().put("action", "open-usb-installer")) - client?.close() - } - override fun onControl(type: String, payload: JSONObject) {} - override fun onFrame(type: Int, payload: ByteArray) {} - override fun onDisconnected(error: String?) {} - }) - client.connect() - } - override fun onDestroy() { DeviceServer.stop() scanner?.stop() diff --git a/apps/portable/src/main/java/com/gostudios/console/portable/PortableActivity.kt b/apps/portable/src/main/java/com/gostudios/console/portable/PortableActivity.kt new file mode 100644 index 0000000..f9d1413 --- /dev/null +++ b/apps/portable/src/main/java/com/gostudios/console/portable/PortableActivity.kt @@ -0,0 +1,161 @@ +package com.gostudios.console.portable + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.gostudios.console.sdk.ConsoleHost +import com.gostudios.console.sdk.LinkClient +import com.gostudios.console.sdk.Protocol +import org.json.JSONArray +import org.json.JSONObject + +/** + * GoConsoleOS Portable — full tool panel for the remote host. + * + * Connects to the GoConsoleOS console on the LAN, lists the host tools it can + * drive (GoUsbMaker / USB Health / Cast / GoAI / Store / Screenshot / ...) and + * triggers the selected one by sending a control message back to the host. + */ +class PortableActivity : AppCompatActivity() { + + private var client: LinkClient? = null + private lateinit var adapter: ToolAdapter + private val tools = mutableListOf() + + /** Default tool catalogue shown when the host does not support `tools.list`. */ + private val defaults = listOf( + ToolRow("usb-installer", "GoUsbMaker", "Build a Portable USB Gaming Console"), + ToolRow("usb-health", "USB Health", "SMART report for every USB console"), + ToolRow("cast", "GoConsoleOS Cast", "Mirror your console to a TV or device"), + ToolRow("goai", "GoAI", "Ask your assistant anything, locally"), + ToolRow("store", "GoStore", "Browse the curated app catalogue"), + ToolRow("screenshot", "Screenshot", "Capture the current screen on the host"), + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_portable) + + val hostJson = intent.getStringExtra(EXTRA_HOST) ?: return + val host = ConsoleHost.fromJson(JSONObject(hostJson)) + + val rv = findViewById(R.id.toolList) + rv.layoutManager = LinearLayoutManager(this) + adapter = ToolAdapter(tools) { tool -> runTool(tool) } + rv.adapter = adapter + + client = LinkClient(host, object : LinkClient.Listener { + override fun onConnected(h: ConsoleHost) { + runOnUiThread { + findViewById(R.id.txtConn).text = + getString(R.string.status_connected, h.name) + } + client?.sendControl(MSG_TOOLS_LIST) + } + + override fun onControl(type: String, payload: JSONObject) { + if (type == MSG_TOOLS_LIST) { + applyTools(payload.optJSONArray("tools")) + } else if (type == MSG_TOOLS_RUN) { + runOnUiThread { + findViewById(R.id.txtConn).text = + getString(R.string.tool_ok, payload.optString("tool", "")) + } + } + } + + override fun onFrame(type: Int, payload: ByteArray) {} + override fun onDisconnected(error: String?) { + runOnUiThread { + findViewById(R.id.txtConn).text = "Disconnected: $error" + } + } + }) + client?.connect() + } + + override fun onDestroy() { + client?.close() + super.onDestroy() + } + + private fun runTool(tool: ToolRow) { + val c = client ?: return + if (tool.id == "usb-installer") { + c.sendControl(Protocol.MSG_PAIR, JSONObject().put("action", "open-usb-installer")) + } else { + c.sendControl(MSG_TOOLS_RUN, JSONObject().put("tool", tool.id)) + } + findViewById(R.id.txtConn).text = + getString(R.string.tool_running, tool.name) + } + + private fun applyTools(arr: JSONArray?) { + val rows: List = if (arr == null || arr.length() == 0) { + defaults + } else { + buildList { + for (i in 0 until arr.length()) { + val item = arr.optJSONObject(i) ?: continue + add( + ToolRow( + item.optString("id", ""), + item.optString("name", ""), + item.optString("desc", ""), + ), + ) + } + }.ifEmpty { defaults } + } + runOnUiThread { + tools.clear() + tools.addAll(rows) + adapter.notifyDataSetChanged() + } + } + + private class ToolRow(val id: String, val name: String, val desc: String) + + private class ToolAdapter( + private val items: MutableList, + private val onClick: (ToolRow) -> Unit, + ) : RecyclerView.Adapter() { + + class Holder(view: View) : RecyclerView.ViewHolder(view) { + val name: TextView = view.findViewById(R.id.toolName) + val desc: TextView = view.findViewById(R.id.toolDesc) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { + val v = LayoutInflater.from(parent.context) + .inflate(R.layout.item_tool, parent, false) + return Holder(v) + } + + override fun onBindViewHolder(holder: Holder, position: Int) { + val tool = items[position] + holder.name.text = tool.name + holder.desc.text = tool.desc + holder.itemView.setOnClickListener { onClick(tool) } + } + + override fun getItemCount() = items.size + } + + companion object { + private const val EXTRA_HOST = "hostJson" + private const val MSG_TOOLS_LIST = "tools.list" + private const val MSG_TOOLS_RUN = "tools.run" + + fun intent(context: Context, host: ConsoleHost): Intent = + Intent(context, PortableActivity::class.java) + .putExtra(EXTRA_HOST, host.toJson().toString()) + } +} \ No newline at end of file diff --git a/apps/portable/src/main/res/layout/activity_portable.xml b/apps/portable/src/main/res/layout/activity_portable.xml new file mode 100644 index 0000000..c5677fe --- /dev/null +++ b/apps/portable/src/main/res/layout/activity_portable.xml @@ -0,0 +1,36 @@ + + + + + + + + + + \ No newline at end of file diff --git a/apps/portable/src/main/res/layout/item_tool.xml b/apps/portable/src/main/res/layout/item_tool.xml new file mode 100644 index 0000000..87522e2 --- /dev/null +++ b/apps/portable/src/main/res/layout/item_tool.xml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/apps/portable/src/main/res/values/strings.xml b/apps/portable/src/main/res/values/strings.xml index d94b170..34dc04f 100644 --- a/apps/portable/src/main/res/values/strings.xml +++ b/apps/portable/src/main/res/values/strings.xml @@ -9,6 +9,10 @@ GoConsoleOS Link USB Health Cast - Open USB Installer + Portable App + Starting %1$s… + %1$s sent to the console. + GoConsoleOS Portable App + Tools for your GoConsoleOS on USB %1$d%% \ No newline at end of file diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index a43f74f..90992fc 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -55,8 +55,20 @@ The stream mixes two kinds of records: {"type":"cast.start"}\n {"type":"cast.stop"}\n {"type":"pair","action":"open-usb-installer"}\n +{"type":"tools.list"}\n +{"type":"tools.run","tool":"goai"}\n ``` +`tools.list` replies with the Portable App tool catalogue the host supports: + +```json +{"type":"tools.list","tools":[{"id":"usb-installer","name":"GoUsbMaker","desc":"..."},{"id":"goai","name":"GoAI","desc":"..."}]} +``` + +`tools.run` triggers that tool on the host (`usb-installer`, `usb-health`, +`cast`, `goai`, `store`, `screenshot`) and replies +`{"type":"tools.run","ok":true,"tool":"..."}`. + ### 2. Binary frames 5-byte header: `1 byte type` + `4 byte big-endian length`, then payload. diff --git a/shared/src/main/java/com/gostudios/console/sdk/Protocol.kt b/shared/src/main/java/com/gostudios/console/sdk/Protocol.kt index 048fffa..f97d51f 100644 --- a/shared/src/main/java/com/gostudios/console/sdk/Protocol.kt +++ b/shared/src/main/java/com/gostudios/console/sdk/Protocol.kt @@ -36,6 +36,12 @@ object Protocol { const val MSG_CAST_START = "cast.start" const val MSG_CAST_STOP = "cast.stop" + /** List remote host tools (usb-installer, usb-health, cast, goai, ...). */ + const val MSG_TOOLS_LIST = "tools.list" + + /** Run a remote host tool by id. */ + const val MSG_TOOLS_RUN = "tools.run" + const val USB_HEALTH_OK = "ok" const val USB_HEALTH_FAIR = "fair" const val USB_HEALTH_POOR = "poor"