feat: system media controls (MediaSession) for the active server (#1)
* feat: add system media controls for the active server Add a media3 MediaSessionService that mirrors playback on the active RelayTV server as a system media session (lock screen / quick settings): - MediaControlService polls GET /status and publishes state, metadata, artwork, and remote volume; forwards play/pause, next/previous, seek, stop, and volume to the server HTTP API - RelayRemotePlayer implements SimpleBasePlayer over the remote state - RemoteStatus parses /status defensively across server versions - New Settings screen (toolbar menu) with a media-controls toggle and a Manage servers shortcut; fix open_servers intent handling via onNewIntent when MainActivity is already running Release prep: - Bump to versionCode 6 / versionName 1.3.0 - Wire optional release signing from MYAPP_RELEASE_* gradle properties - Add FOREGROUND_SERVICE(_MEDIA_PLAYBACK) permissions and lint opt-ins - Update privacy policy, release checklist (Play Console foreground service declaration), README, and Gradle daemon heap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: sign release bundle in GitHub Actions Decode the upload keystore from the RELEASE_KEYSTORE_BASE64 secret and pass the MYAPP_RELEASE_* signing properties to Gradle via ORG_GRADLE_PROJECT_* env vars, so tagged releases produce a signed .aab. Fail fast if the keystore secret is missing, and verify the bundle signature after the build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,10 @@ class MainActivity : AppCompatActivity() {
|
||||
loadActiveServer(forcePickerOnFailure = false, manualRefresh = true)
|
||||
true
|
||||
}
|
||||
R.id.action_settings -> {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
true
|
||||
}
|
||||
R.id.action_privacy -> {
|
||||
openPrivacyPolicy()
|
||||
true
|
||||
@@ -184,6 +188,14 @@ class MainActivity : AppCompatActivity() {
|
||||
loadServerBase(base.trimEnd('/'), forcePickerOnFailure = true, manualRefresh = false)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
if (intent.getBooleanExtra("open_servers", false)) {
|
||||
showServerPicker()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildFileChooserIntent(fileChooserParams: WebChromeClient.FileChooserParams): Intent {
|
||||
val acceptTypes = fileChooserParams.acceptTypes
|
||||
.orEmpty()
|
||||
@@ -263,11 +275,19 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onResume()
|
||||
isInForeground = true
|
||||
registerNetworkCallback()
|
||||
ensureMediaControlService()
|
||||
if (!activeBaseUrl.isNullOrBlank()) {
|
||||
scheduleHeartbeat(2_000)
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
private fun ensureMediaControlService() {
|
||||
if (!AppSettings.isMediaControlsEnabled(this)) return
|
||||
if (HostStore.getActiveBaseUrl(this).isNullOrBlank()) return
|
||||
runCatching { startService(Intent(this, MediaControlService::class.java)) }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
isInForeground = false
|
||||
@@ -341,6 +361,8 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
val recovered = consecutiveHealthFailures > 0
|
||||
consecutiveHealthFailures = 0
|
||||
// Keep the media-controls service alive while we can see the server.
|
||||
ensureMediaControlService()
|
||||
if (recovered && !manualRefresh) {
|
||||
Toast.makeText(this@MainActivity, "Reconnected to RelayTV.", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user