Finished build for the TVs and steadier lists
Port of release 0.108.61. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,14 @@ android {
|
||||
versionName = "0.111.0-advent-premieres"
|
||||
}
|
||||
buildFeatures { compose = true }
|
||||
// The finished build for the TVs (Paul, 6 November 2026: the test build stuttered on the box). Not debuggable, so
|
||||
// Android compiles it fully; signed with the same key as the test builds, so it installs over them keeping everything.
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
// Android logging and the like do nothing in unit tests, so whole flows (a channel read from a fake server) can run.
|
||||
testOptions { unitTests.isReturnDefaultValues = true }
|
||||
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
<activity android:name=".MainActivity" android:exported="true" android:screenOrientation="landscape">
|
||||
<intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LEANBACK_LAUNCHER" /></intent-filter>
|
||||
</activity>
|
||||
<!-- The install check (InstallSnapshot.kt): only adb can send it, as it needs the DUMP permission apps can't hold. -->
|
||||
<receiver android:name=".InstallSnapshotReceiver" android:exported="true" android:permission="android.permission.DUMP">
|
||||
<intent-filter><action android:name="tv.orbit.classic.INSTALL_SNAPSHOT" /></intent-filter>
|
||||
</receiver>
|
||||
<!-- The hand-run playback test, opened only by MainActivity's orbit_play_test extra. -->
|
||||
<activity android:name=".PlaybackTestActivity" android:exported="false" />
|
||||
</application>
|
||||
|
||||
@@ -80,7 +80,7 @@ internal fun HomeScreen(channels: List<SampleChannel>, modifier: Modifier = Modi
|
||||
val glide = rememberGlide()
|
||||
CompositionLocalProvider(LocalGlide provides glide) { Column(Modifier.width(400.dp).glideArea(glide, Yellow, 6.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
homeCategories(channels.map { it.number }).forEachIndexed { index, category ->
|
||||
HomeCard(Modifier.fillMaxWidth().height(31.dp).riseIn(index).focusProperties { if (index == 0 && nav != null) up = nav }, if (index == 0) firstFocus else null, onClick = { onCategory(category) }) { focused ->
|
||||
HomeCard(Modifier.fillMaxWidth().height(31.dp).focusProperties { if (index == 0 && nav != null) up = nav }, if (index == 0) firstFocus else null, onClick = { onCategory(category) }) { focused ->
|
||||
Row(Modifier.fillMaxSize().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Image(painterResource(category.icon), null, Modifier.size(18.dp), colorFilter = ColorFilter.tint(if (focused) OnAccent else Ink))
|
||||
Text(category.label.uppercase(java.util.Locale.UK), color = if (focused) OnAccent else Ink, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, letterSpacing = .5.sp, maxLines = 1)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package tv.orbit.classic
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* The install check for a finished (non-debuggable) build (Paul, 6 November 2026), where adb's run-as can no longer read
|
||||
* Orbit's files. Sent only from adb (the receiver needs the DUMP permission, which apps can't hold):
|
||||
*
|
||||
* - `snapshot`: copies the schedules and settings into Orbit's own folder on shared storage, with the library's and
|
||||
* sign-in's checksums, for adb to pull and compare. Nothing is read out of them here.
|
||||
* - `rollback <version>`: copies the schedules aside as schedule-rollback-<version>, as every install does.
|
||||
* - `clear`: removes the copy once it's been pulled.
|
||||
*
|
||||
* It only ever copies; it never changes the schedules, the library or the sign-in.
|
||||
*/
|
||||
class InstallSnapshotReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val result = runCatching { InstallSnapshot.handle(context, intent.getStringExtra("do").orEmpty(), intent.getStringExtra("version")) }
|
||||
resultData = result.fold({ it }, { "failed" })
|
||||
android.util.Log.i("OrbitInstall", "${intent.getStringExtra("do")}: $resultData")
|
||||
}
|
||||
}
|
||||
|
||||
internal object InstallSnapshot {
|
||||
private val versionShape = Regex("""\d+\.\d+\.\d+""")
|
||||
|
||||
fun handle(context: Context, action: String, version: String?): String {
|
||||
val data = context.filesDir.parentFile ?: return "no data"
|
||||
val out = File(context.getExternalFilesDir(null), "install-snapshot")
|
||||
return when (action) {
|
||||
"snapshot" -> {
|
||||
out.deleteRecursively()
|
||||
val schedule = File(data, "no_backup/schedule")
|
||||
schedule.copyRecursively(File(out, "no_backup/schedule"), overwrite = true)
|
||||
File(data, "shared_prefs/orbit_settings.xml").takeIf { it.exists() }?.copyTo(File(out, "shared_prefs/orbit_settings.xml"), overwrite = true)
|
||||
File(out, "hashes.txt").writeText(listOf("no_backup/catalogue.db", "no_backup/orbit-connection.v1")
|
||||
.joinToString("\n") { "${sha256(File(data, it))} $it" } + "\n")
|
||||
"copied ${schedule.walk().count { it.isFile }}"
|
||||
}
|
||||
"rollback" -> {
|
||||
if (version == null || !versionShape.matches(version)) return "bad version"
|
||||
val schedule = File(data, "no_backup/schedule")
|
||||
val aside = File(data, "no_backup/schedule-rollback-$version")
|
||||
aside.deleteRecursively(); schedule.copyRecursively(aside, overwrite = true)
|
||||
"aside ${aside.walk().count { it.isFile }}"
|
||||
}
|
||||
"clear" -> { out.deleteRecursively(); "cleared" }
|
||||
else -> "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
private fun sha256(file: File): String = if (!file.exists()) "missing" else file.inputStream().use { input ->
|
||||
val digest = MessageDigest.getInstance("SHA-256"); val buffer = ByteArray(64 * 1024)
|
||||
while (true) { val n = input.read(buffer); if (n < 0) break; digest.update(buffer, 0, n) }
|
||||
digest.digest().joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
}
|
||||
@@ -493,7 +493,7 @@ internal fun OrbitArcade(modifier: Modifier, onPlaying: (Boolean) -> Unit) {
|
||||
// Tall enough for the name and its line in full (Paul, 5 November 2026: the line was clipped at 48).
|
||||
Row(Modifier.fillMaxWidth().height(56.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
pair.forEachIndexed { i, game ->
|
||||
GameCard(game, Modifier.weight(1f).fillMaxHeight().riseIn(row * 2 + i), if (row == 0 && i == 0) first else null, if (row == 0) nav else null, { chosen = game }, { play(game) })
|
||||
GameCard(game, Modifier.weight(1f).fillMaxHeight(), if (row == 0 && i == 0) first else null, if (row == 0) nav else null, { chosen = game }, { play(game) })
|
||||
}
|
||||
if (pair.size == 1) Spacer(Modifier.weight(1f))
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ internal fun WhosWatching(onClose: () -> Unit) {
|
||||
// Kids first, so a child's OK goes straight to their own.
|
||||
Profiles.all.sortedBy { if (it.kind == ProfileKind.Kids) 0 else 1 }.forEachIndexed { i, p ->
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Column(Modifier.riseIn(i).then(if (i == 0) Modifier.focusRequester(first) else Modifier).onFocusChanged { focused = it.isFocused }
|
||||
Column(Modifier.then(if (i == 0) Modifier.focusRequester(first) else Modifier).onFocusChanged { focused = it.isFocused }
|
||||
// No grey focus box behind the picture (Paul, 6 November 2026): the gold ring and name say it's chosen.
|
||||
.clickable(remember { androidx.compose.foundation.interaction.MutableInteractionSource() }, indication = null) { choose(p) },
|
||||
horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
|
||||
Reference in New Issue
Block a user