Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 116 additions & 3 deletions app/src/main/java/org/randomcoder/udroid/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ import org.randomcoder.udroid.runtime.ProotMountResolver
import org.randomcoder.udroid.runtime.RuntimePhase
import org.randomcoder.udroid.runtime.RuntimeSnapshot
import org.randomcoder.udroid.runtime.RuntimeSupervisorService
import org.randomcoder.udroid.runtime.RootfsShortcutContract
import org.randomcoder.udroid.runtime.RootfsShortcutPublisher
import org.randomcoder.udroid.runtime.RootfsShortcutTarget
import org.randomcoder.udroid.runtime.RootfsStorageUsage
import org.randomcoder.udroid.runtime.VirglServerMode
import org.randomcoder.udroid.ui.UdroidApp
Expand Down Expand Up @@ -95,6 +98,10 @@ private data class PendingDesktopStart(
val configuration: DesktopConfiguration,
)

private data class PendingTerminalShortcut(
val tabId: String,
val rootfsName: String,
)
class MainActivity : ComponentActivity() {
private val app: UdroidApplication
get() = application as UdroidApplication
Expand Down Expand Up @@ -132,11 +139,13 @@ class MainActivity : ComponentActivity() {
private var pendingLinuxApplication: PendingLinuxApplication? = null
private var pendingLinuxApplicationId: String? = null
private var pendingDesktopStart: PendingDesktopStart? = null
private var pendingDesktopShortcutRootfsName: String? = null
private var pendingShortcutRootfsName: String? = null
private var pendingTerminalRootfsName: String? = null
private var pendingDebugDesktop: DebugCommand? = null
private var pendingDebugStopDesktop = false
private var pendingMicrophoneRootfsName: String? = null
private var pendingTerminalShortcut: PendingTerminalShortcut? = null
private var linuxApplicationsLoadGeneration = 0L
private var desktopScanGeneration = 0L
private var ociTagsLoadGeneration = 0L
Expand Down Expand Up @@ -324,7 +333,9 @@ class MainActivity : ComponentActivity() {
loadCatalogue()
handleUpdateIntent(intent)
handleInstallerIntent(intent)
if (!handleShortcutIntent(intent)) loadLinuxApplications()
if (!handleRootfsShortcutIntent(intent) && !handleShortcutIntent(intent)) {
loadLinuxApplications()
}
handleDebugCommand()
}

Expand All @@ -333,7 +344,7 @@ class MainActivity : ComponentActivity() {
setIntent(intent)
handleUpdateIntent(intent)
handleInstallerIntent(intent)
handleShortcutIntent(intent)
if (!handleRootfsShortcutIntent(intent)) handleShortcutIntent(intent)
handleDebugCommand()
}

Expand Down Expand Up @@ -378,6 +389,12 @@ class MainActivity : ComponentActivity() {
).destination
selectedDestination = resolvedDestination
applySystemBars(resolvedDestination)
if (resolvedDestination != UdroidDestination.SYSTEM) {
pendingDesktopShortcutRootfsName = null
}
if (resolvedDestination != UdroidDestination.TERMINAL) {
pendingTerminalShortcut = null
}
if (resolvedDestination == UdroidDestination.APPS) loadLinuxApplications()
if (resolvedDestination == UdroidDestination.SYSTEM) {
selectedSystemRootfsName?.let(::loadDesktopEnvironments)
Expand Down Expand Up @@ -481,6 +498,7 @@ class MainActivity : ComponentActivity() {
}
launchPendingLinuxApplication()
launchPendingDesktop()
selectPendingTerminalShortcut()
}

private fun loadCatalogue() {
Expand Down Expand Up @@ -866,6 +884,10 @@ class MainActivity : ComponentActivity() {
.disableForRootfs(rootfsName)
}.exceptionOrNull()
?.let { cleanupWarnings += it.message ?: "launcher shortcuts" }
runCatching {
RootfsShortcutPublisher(this@MainActivity).disableForRootfs(rootfsName)
}.exceptionOrNull()
?.let { cleanupWarnings += it.message ?: "terminal and display shortcuts" }

if (resetWork == null) {
runCatching { ProotEnvironmentProfileStore(this@MainActivity).remove(rootfsName) }
Expand Down Expand Up @@ -907,7 +929,11 @@ class MainActivity : ComponentActivity() {

pendingLinuxApplication = null
pendingDesktopStart = null
pendingDesktopShortcutRootfsName = null
pendingTerminalRootfsName = null
if (pendingTerminalShortcut?.rootfsName == rootfsName) {
pendingTerminalShortcut = null
}
linuxApplicationsLoadGeneration++
desktopScanGeneration++
rootfsMaintenanceName = null
Expand Down Expand Up @@ -988,6 +1014,7 @@ class MainActivity : ComponentActivity() {
if (environments.size == 1) " detected" else "s detected"
}
launchPendingDebugDesktop(rootfsName)
launchPendingDesktopShortcut(rootfsName)
},
onFailure = {
desktopEnvironments = emptyList()
Expand All @@ -1002,6 +1029,9 @@ class MainActivity : ComponentActivity() {
debugCommandError(desktopScanMessage ?: "Desktop detection failed")
}
pendingDebugDesktop = null
if (pendingDesktopShortcutRootfsName == rootfsName) {
pendingDesktopShortcutRootfsName = null
}
},
)
}
Expand Down Expand Up @@ -1060,6 +1090,14 @@ class MainActivity : ComponentActivity() {
selectDestination(UdroidDestination.DESKTOP)
}

private fun launchPendingDesktopShortcut(rootfsName: String) {
if (pendingDesktopShortcutRootfsName != rootfsName) return
pendingDesktopShortcutRootfsName = null
if (desktopEnvironments.isEmpty()) return
startSelectedDesktop()
selectDestination(UdroidDestination.DESKTOP)
}

private fun debugCommandError(message: String) {
app.journal.append(
component = "debug",
Expand Down Expand Up @@ -1354,6 +1392,46 @@ class MainActivity : ComponentActivity() {
return true
}

private fun handleRootfsShortcutIntent(intent: Intent?): Boolean {
if (intent?.action != RootfsShortcutContract.ACTION_OPEN) return false
val rootfsName =
intent
.getStringExtra(RootfsShortcutContract.EXTRA_ROOTFS_NAME)
?.takeIf(String::isNotBlank)
val target =
intent
.getStringExtra(RootfsShortcutContract.EXTRA_TARGET)
?.let { runCatching { RootfsShortcutTarget.valueOf(it) }.getOrNull() }
val terminalTabId =
intent
.getStringExtra(RootfsShortcutContract.EXTRA_TERMINAL_TAB_ID)
?.takeIf(String::isNotBlank)
intent.removeExtra(RootfsShortcutContract.EXTRA_ROOTFS_NAME)
intent.removeExtra(RootfsShortcutContract.EXTRA_TARGET)
intent.removeExtra(RootfsShortcutContract.EXTRA_TERMINAL_TAB_ID)
if (rootfsName == null || target == null) return true
if (installedRootfses.none { it.name == rootfsName }) {
rootfsMaintenanceMessage = "The Linux system for this shortcut is no longer installed"
selectDestination(UdroidDestination.DISTROS)
return true
}
when (target) {
RootfsShortcutTarget.TERMINAL -> openRootfsTerminal(rootfsName, terminalTabId)
RootfsShortcutTarget.DISPLAY -> {
if (
snapshot.desktop.rootfsName == rootfsName &&
snapshot.desktop.phase == DesktopSessionPhase.RUNNING
) {
selectDestination(UdroidDestination.DESKTOP)
} else {
pendingDesktopShortcutRootfsName = rootfsName
openSystemDetails(rootfsName)
}
}
}
return true
}

private fun resolveShortcutApplication(state: LinuxApplicationsState) {
val applicationId = pendingLinuxApplicationId ?: return
when (state) {
Expand Down Expand Up @@ -1440,9 +1518,27 @@ class MainActivity : ComponentActivity() {
selectDestination(UdroidDestination.APPS)
}

private fun openRootfsTerminal(rootfsName: String) {
private fun openRootfsTerminal(
rootfsName: String,
terminalTabId: String? = null,
) {
setActiveRootfs(rootfsName)
ensureNotificationPermission()
pendingTerminalShortcut =
terminalTabId?.let { PendingTerminalShortcut(it, rootfsName) }
val requestedTab =
runtimeService
?.terminalTabSnapshots()
?.firstOrNull {
it.rootfsName == rootfsName &&
(terminalTabId == null || it.id == terminalTabId)
}
if (requestedTab != null && runtimeService?.selectTerminalTab(requestedTab.id) == true) {
pendingTerminalShortcut = null
pendingTerminalRootfsName = null
selectDestination(UdroidDestination.TERMINAL)
return
}
val runningSession = runtimeService?.currentTerminalSession()
if (runningSession?.isRunning == true && runningSession.mSessionName != rootfsName) {
pendingTerminalRootfsName = rootfsName
Expand All @@ -1454,6 +1550,23 @@ class MainActivity : ComponentActivity() {
selectDestination(UdroidDestination.TERMINAL)
}

private fun selectPendingTerminalShortcut() {
val pending = pendingTerminalShortcut ?: return
val service = runtimeService ?: return
val requestedTab =
service
.terminalTabSnapshots()
.firstOrNull { it.id == pending.tabId && it.rootfsName == pending.rootfsName }
if (requestedTab != null) {
service.selectTerminalTab(requestedTab.id)
pendingTerminalShortcut = null
} else if (
snapshot.phase == RuntimePhase.RUNNING &&
service.currentTerminalSession()?.isRunning == true
) {
pendingTerminalShortcut = null
}
}
private fun ensureNotificationPermission() {
if (Build.VERSION.SDK_INT >= 33 &&
checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) !=
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.randomcoder.udroid.runtime

import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutManager
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import org.randomcoder.udroid.MainActivity
import org.randomcoder.udroid.R

enum class RootfsShortcutTarget {
TERMINAL,
DISPLAY,
}

class RootfsShortcutPublisher(
private val context: Context,
) {
fun publishAndRequestPin(
rootfsName: String,
label: String,
target: RootfsShortcutTarget,
terminalTabId: String? = null,
): Result<Unit> =
runCatching {
require(label.isNotBlank()) { "Shortcut name must not be blank" }
check(ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
"This launcher does not support adding shortcuts to the home screen"
}
val shortcutIntent =
Intent(context, MainActivity::class.java)
.setAction(RootfsShortcutContract.ACTION_OPEN)
.putExtra(RootfsShortcutContract.EXTRA_ROOTFS_NAME, rootfsName)
.putExtra(RootfsShortcutContract.EXTRA_TARGET, target.name)
.addFlags(
Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_SINGLE_TOP,
).apply {
terminalTabId?.let {
putExtra(RootfsShortcutContract.EXTRA_TERMINAL_TAB_ID, it)
}
}
val shortcut =
ShortcutInfoCompat
.Builder(
context,
RootfsShortcutContract.shortcutId(rootfsName, target, terminalTabId),
).setShortLabel(label.trim())
.setLongLabel("Open ${label.trim()} in uDroid")
.setIcon(IconCompat.createWithResource(context, R.drawable.udroid_logo))
.setIntent(shortcutIntent)
.build()
check(ShortcutManagerCompat.requestPinShortcut(context, shortcut, null)) {
"The launcher did not accept the shortcut request"
}
}

fun disableForRootfs(rootfsName: String) {
val manager = context.getSystemService(ShortcutManager::class.java) ?: return
val shortcutIds =
manager.pinnedShortcuts
.asSequence()
.map { it.id }
.filter { RootfsShortcutContract.belongsToRootfs(it, rootfsName) }
.toList()
if (shortcutIds.isEmpty()) return
manager.disableShortcuts(
shortcutIds,
"The Linux system for this shortcut was removed",
)
}
}

object RootfsShortcutContract {
const val ACTION_OPEN = "org.randomcoder.udroid.action.OPEN_ROOTFS_SHORTCUT"
const val EXTRA_ROOTFS_NAME = "org.randomcoder.udroid.extra.ROOTFS_NAME"
const val EXTRA_TARGET = "org.randomcoder.udroid.extra.ROOTFS_SHORTCUT_TARGET"
const val EXTRA_TERMINAL_TAB_ID = "org.randomcoder.udroid.extra.TERMINAL_TAB_ID"

private const val SHORTCUT_ID_PREFIX = "rootfs:"

fun belongsToRootfs(
shortcutId: String,
rootfsName: String,
): Boolean = shortcutId.startsWith("$SHORTCUT_ID_PREFIX$rootfsName:")

fun shortcutId(
rootfsName: String,
target: RootfsShortcutTarget,
terminalTabId: String? = null,
): String {
require(rootfsName.isNotBlank()) { "Linux system name must not be blank" }
require(terminalTabId == null || terminalTabId.isNotBlank()) {
"Terminal tab ID must not be blank"
}
require(target == RootfsShortcutTarget.TERMINAL || terminalTabId == null) {
"Only terminal shortcuts can target a terminal tab"
}
return listOfNotNull(SHORTCUT_ID_PREFIX + rootfsName, target.name.lowercase(), terminalTabId)
.joinToString(":")
}
}
Loading
Loading