chore: prepare Android app for release
This commit is contained in:
@@ -1,207 +1,90 @@
|
|||||||
# 📺 RelayTV Android Remote
|
# RelayTV Android Remote
|
||||||
|
|
||||||
**RelayTV Android** is the official companion remote app for
|
RelayTV Android is the Android companion app for the self-hosted RelayTV server. It gives you a lightweight remote, embedded `/ui` access, multi-server management, and Android share targets for queueing or immediately playing links on a RelayTV box.
|
||||||
**RelayTV**, a powerful local-first media relay and playback system.
|
|
||||||
|
|
||||||
Control, launch, and share content to your RelayTV server instantly from
|
RelayTV server:
|
||||||
Android --- with multi-server support and seamless share integration.
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## 🚀 What is RelayTV?
|
|
||||||
|
|
||||||
RelayTV is a local media relay and playback server designed for:
|
|
||||||
|
|
||||||
- YouTube & streaming links
|
|
||||||
- Direct media URLs
|
|
||||||
- Queue management
|
|
||||||
- HDMI playback (mpv-based)
|
|
||||||
- LAN-first operation
|
|
||||||
- Smart `/smart` auto-routing endpoint
|
|
||||||
|
|
||||||
🔗 RelayTV Server:\
|
|
||||||
https://github.com/mcgeezy/relaytv
|
https://github.com/mcgeezy/relaytv
|
||||||
|
|
||||||
🔗 Home Assistant Integration:\
|
Home Assistant integration:
|
||||||
https://github.com/mcgeezy/relaytv-ha
|
https://github.com/mcgeezy/relaytv-ha
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
## Features
|
||||||
|
|
||||||
# ✨ Features
|
- Multi-server support with health verification before save
|
||||||
|
- LAN auto-discovery of `_relaytv._tcp` servers with Android NSD / mDNS
|
||||||
|
- Embedded RelayTV `/ui` WebView with reconnect and heartbeat recovery
|
||||||
|
- Dedicated Android share targets:
|
||||||
|
- `RelayTV Queue` -> `POST /smart`
|
||||||
|
- `RelayTV Play` -> `POST /play_now`
|
||||||
|
- HTTP and HTTPS support for self-hosted RelayTV servers
|
||||||
|
|
||||||
## 📡 Multi-Server Support
|
## Requirements
|
||||||
|
|
||||||
- Add unlimited RelayTV servers
|
- Android 8.0+ (`minSdk 26`)
|
||||||
- Name each server (Living Room, Office, Bedroom, etc.)
|
- RelayTV server reachable on your LAN or via a trusted remote URL
|
||||||
- Instantly switch active server
|
|
||||||
- Remove or edit servers anytime
|
|
||||||
- Built-in `/health` verification before saving
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
## Build And Test
|
||||||
|
|
||||||
## 🔗 Share to RelayTV
|
|
||||||
|
|
||||||
- Share any link from Android to RelayTV
|
|
||||||
|
|
||||||
- Automatically sends to:
|
|
||||||
|
|
||||||
POST /smart
|
|
||||||
|
|
||||||
- Smart endpoint handles play vs queue logic
|
|
||||||
|
|
||||||
- Instant feedback toast:
|
|
||||||
|
|
||||||
Sent to "Living Room"
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## 🎨 Clean RelayTV UI
|
|
||||||
|
|
||||||
- Material 3 design
|
|
||||||
- Dark-mode optimized
|
|
||||||
- RelayTV branding (electric blue + charcoal theme)
|
|
||||||
- Quick server switch from toolbar
|
|
||||||
- Pull-to-refresh WebView
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## 🧠 Smart URL Handling
|
|
||||||
|
|
||||||
- Auto-normalizes URLs:
|
|
||||||
- 10.0.55.2:8787 → http://10.0.55.2:8787
|
|
||||||
- Validates server before saving
|
|
||||||
- Prevents crash loops from invalid input
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## 📱 WebView Relay Mode
|
|
||||||
|
|
||||||
Loads:
|
|
||||||
|
|
||||||
/ui
|
|
||||||
|
|
||||||
from the active server for full RelayTV interface access.
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# 🏗 Requirements
|
|
||||||
|
|
||||||
- Android 8.0+
|
|
||||||
- RelayTV server running locally or remotely
|
|
||||||
- HTTP or HTTPS supported
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# 🔐 HTTPS Notes
|
|
||||||
|
|
||||||
RelayTV supports both:
|
|
||||||
|
|
||||||
http://
|
|
||||||
https://
|
|
||||||
|
|
||||||
For best compatibility:
|
|
||||||
|
|
||||||
- Prefer HTTPS
|
|
||||||
- Use a valid certificate (reverse proxy, Nginx Proxy Manager, etc.)
|
|
||||||
- Or use HTTP on trusted LAN environments
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# 🛠 Build Instructions
|
|
||||||
|
|
||||||
Clone:
|
|
||||||
|
|
||||||
git clone https://github.com/mcgeezy/relaytv-android.git
|
|
||||||
cd relaytv-android
|
|
||||||
|
|
||||||
Debug build:
|
|
||||||
|
|
||||||
./gradlew assembleDebug
|
|
||||||
|
|
||||||
Release bundle (for Play):
|
|
||||||
|
|
||||||
./gradlew bundleRelease
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
## 🔧 Local Build/Test Scripts
|
|
||||||
|
|
||||||
Validate host environment:
|
Validate host environment:
|
||||||
|
|
||||||
./scripts/check-android-env.sh all
|
```bash
|
||||||
|
./scripts/check-android-env.sh all
|
||||||
|
```
|
||||||
|
|
||||||
Install SDK components (requires sdkmanager):
|
Install or refresh Android SDK components:
|
||||||
|
|
||||||
./scripts/bootstrap-android.sh
|
```bash
|
||||||
|
./scripts/bootstrap-android.sh
|
||||||
|
```
|
||||||
|
|
||||||
Build + lint + unit tests:
|
Debug build, lint, and unit tests:
|
||||||
|
|
||||||
./scripts/build-debug.sh
|
```bash
|
||||||
|
./scripts/build-debug.sh
|
||||||
|
```
|
||||||
|
|
||||||
Connected/device tests:
|
Release bundle and release lint:
|
||||||
|
|
||||||
./scripts/test-connected.sh
|
```bash
|
||||||
|
./scripts/build-release.sh
|
||||||
|
```
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
Connected tests on a physical device or emulator:
|
||||||
|
|
||||||
# 📦 Play Store Ready
|
```bash
|
||||||
|
./scripts/test-connected.sh
|
||||||
|
```
|
||||||
|
|
||||||
Release build includes:
|
## Release Notes
|
||||||
|
|
||||||
- Resource shrinking
|
- Current Play target: Android 15 / API 35
|
||||||
- Code minification
|
- Release bundle output: `app/build/outputs/bundle/release/app-release.aab`
|
||||||
- Material 3 theming
|
- Release checklist: [docs/RELEASE_CHECKLIST.md](/opt/dev/relaytv-android/docs/RELEASE_CHECKLIST.md)
|
||||||
- Clean package namespace
|
- Privacy policy draft: [docs/PRIVACY_POLICY.md](/opt/dev/relaytv-android/docs/PRIVACY_POLICY.md)
|
||||||
|
|
||||||
Before Play upload:
|
## Network Notes
|
||||||
|
|
||||||
- Configure signing keystore
|
RelayTV Android intentionally supports both:
|
||||||
- Increment versionCode
|
|
||||||
- Generate .aab
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
- `http://`
|
||||||
|
- `https://`
|
||||||
|
|
||||||
# 🏠 Home Assistant Integration
|
HTTP support is retained because RelayTV is commonly deployed as a trusted LAN-first service on local IPs and hostnames. If your server is reachable outside your private network, use HTTPS.
|
||||||
|
|
||||||
RelayTV integrates directly with Home Assistant via:
|
## API Surface Used By The App
|
||||||
|
|
||||||
https://github.com/mcgeezy/relaytv-ha
|
- `GET /health`
|
||||||
|
- `GET /ui`
|
||||||
|
- `POST /smart`
|
||||||
|
- `POST /play_now`
|
||||||
|
|
||||||
Features include:
|
## Contributing
|
||||||
|
|
||||||
- Media player entity
|
- Keep Android UX aligned with RelayTV’s self-hosted, local-first model
|
||||||
- Queue control
|
- Preserve the queue vs play-now share behavior
|
||||||
- Playback control
|
- Avoid regressions in reconnect and server discovery flows
|
||||||
- Side panel iframe UI
|
|
||||||
- Multi-server support
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
## License
|
||||||
|
|
||||||
# 🔄 API Reference
|
|
||||||
|
|
||||||
RelayTV Android primarily uses:
|
|
||||||
|
|
||||||
POST /smart
|
|
||||||
GET /health
|
|
||||||
GET /ui
|
|
||||||
|
|
||||||
Full API reference available in:
|
|
||||||
|
|
||||||
|
Same license as the RelayTV core project:
|
||||||
https://github.com/mcgeezy/relaytv
|
https://github.com/mcgeezy/relaytv
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# 🤝 Contributing
|
|
||||||
|
|
||||||
Contributions welcome!
|
|
||||||
|
|
||||||
- Keep UI consistent with RelayTV branding
|
|
||||||
- Maintain `/smart` routing behavior
|
|
||||||
- Preserve multi-server architecture
|
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# 📄 License
|
|
||||||
|
|
||||||
Same license as RelayTV core project.\
|
|
||||||
See: https://github.com/mcgeezy/relaytv
|
|
||||||
|
|||||||
+19
-15
@@ -4,33 +4,38 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "pro.relaytv"
|
namespace = "pro.relaytv"
|
||||||
compileSdk 34
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "pro.relaytv"
|
applicationId = "pro.relaytv"
|
||||||
minSdk 26
|
minSdk = 26
|
||||||
targetSdk 34
|
targetSdk = 35
|
||||||
versionCode 2
|
versionCode = 3
|
||||||
versionName "1.1.0"
|
versionName = "1.2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled = true
|
||||||
shrinkResources true
|
shrinkResources = true
|
||||||
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled false
|
minifyEnabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
kotlinOptions { jvmTarget = "17" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -38,7 +43,6 @@ dependencies {
|
|||||||
implementation "androidx.appcompat:appcompat:1.7.0"
|
implementation "androidx.appcompat:appcompat:1.7.0"
|
||||||
implementation "androidx.activity:activity-ktx:1.9.2"
|
implementation "androidx.activity:activity-ktx:1.9.2"
|
||||||
implementation "com.google.android.material:material:1.12.0"
|
implementation "com.google.android.material:material:1.12.0"
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
|
||||||
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||||
implementation "androidx.work:work-runtime-ktx:2.9.1"
|
implementation "androidx.work:work-runtime-ktx:2.9.1"
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="false"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
@@ -26,18 +28,60 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ShareActivity"
|
android:name=".ShareActivity"
|
||||||
android:exported="true"
|
android:exported="false"
|
||||||
android:excludeFromRecents="true"
|
android:excludeFromRecents="true"
|
||||||
android:finishOnTaskLaunch="true"
|
android:finishOnTaskLaunch="true"
|
||||||
android:taskAffinity=""
|
android:taskAffinity=""
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay"/>
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name=".ShareQueueAlias"
|
||||||
|
android:targetActivity=".ShareActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:enabled="true"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:finishOnTaskLaunch="true"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:label="@string/share_queue_label"
|
||||||
android:theme="@android:style/Theme.NoDisplay">
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
<meta-data
|
||||||
|
android:name="pro.relaytv.SHARE_ENDPOINT_PATH"
|
||||||
|
android:value="/smart" />
|
||||||
|
<meta-data
|
||||||
|
android:name="pro.relaytv.SHARE_SUCCESS_TEMPLATE"
|
||||||
|
android:value="@string/share_queue_toast" />
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND"/>
|
<action android:name="android.intent.action.SEND"/>
|
||||||
<category android:name="android.intent.category.DEFAULT"/>
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity-alias>
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name=".SharePlayNowAlias"
|
||||||
|
android:targetActivity=".ShareActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:enabled="true"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:finishOnTaskLaunch="true"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:label="@string/share_play_now_label"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
<meta-data
|
||||||
|
android:name="pro.relaytv.SHARE_ENDPOINT_PATH"
|
||||||
|
android:value="/play_now" />
|
||||||
|
<meta-data
|
||||||
|
android:name="pro.relaytv.SHARE_SUCCESS_TEMPLATE"
|
||||||
|
android:value="@string/share_play_now_toast" />
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".SettingsActivity"
|
android:name=".SettingsActivity"
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package pro.relaytv
|
package pro.relaytv
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.Network
|
import android.net.Network
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
@@ -25,7 +27,6 @@ import androidx.activity.OnBackPressedCallback
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
@@ -44,7 +45,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private lateinit var web: WebView
|
private lateinit var web: WebView
|
||||||
private lateinit var toolbar: MaterialToolbar
|
private lateinit var toolbar: MaterialToolbar
|
||||||
private lateinit var swipeRefresh: SwipeRefreshLayout
|
|
||||||
|
|
||||||
private var activeBaseUrl: String? = null
|
private var activeBaseUrl: String? = null
|
||||||
private var consecutiveHealthFailures = 0
|
private var consecutiveHealthFailures = 0
|
||||||
@@ -83,7 +83,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val openServers = intent.getBooleanExtra("open_servers", false)
|
val openServers = intent.getBooleanExtra("open_servers", false)
|
||||||
toolbar = findViewById(R.id.toolbar)
|
toolbar = findViewById(R.id.toolbar)
|
||||||
web = findViewById(R.id.web)
|
web = findViewById(R.id.web)
|
||||||
swipeRefresh = findViewById(R.id.swipeRefresh)
|
|
||||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
if (this@MainActivity::web.isInitialized && web.canGoBack()) {
|
if (this@MainActivity::web.isInitialized && web.canGoBack()) {
|
||||||
@@ -106,29 +105,25 @@ class MainActivity : AppCompatActivity() {
|
|||||||
loadActiveServer(forcePickerOnFailure = false, manualRefresh = true)
|
loadActiveServer(forcePickerOnFailure = false, manualRefresh = true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.action_privacy -> {
|
||||||
|
openPrivacyPolicy()
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
web.settings.javaScriptEnabled = true
|
configureWebView()
|
||||||
web.settings.domStorageEnabled = true
|
|
||||||
web.settings.mediaPlaybackRequiresUserGesture = false
|
|
||||||
web.settings.userAgentString = web.settings.userAgentString + " RelayTV/1.1.0"
|
|
||||||
swipeRefresh.setOnRefreshListener {
|
|
||||||
loadActiveServer(forcePickerOnFailure = false, manualRefresh = true)
|
|
||||||
}
|
|
||||||
|
|
||||||
web.webChromeClient = WebChromeClient()
|
web.webChromeClient = WebChromeClient()
|
||||||
web.webViewClient = object : WebViewClient() {
|
web.webViewClient = object : WebViewClient() {
|
||||||
override fun onPageFinished(view: WebView, url: String) {
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
mainFrameFailed = false
|
mainFrameFailed = false
|
||||||
swipeRefresh.isRefreshing = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
|
override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) {
|
||||||
if (request.isForMainFrame) {
|
if (request.isForMainFrame) {
|
||||||
mainFrameFailed = true
|
mainFrameFailed = true
|
||||||
swipeRefresh.isRefreshing = false
|
|
||||||
Toast.makeText(this@MainActivity, "Connection lost. Retrying…", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this@MainActivity, "Connection lost. Retrying…", Toast.LENGTH_SHORT).show()
|
||||||
scheduleHeartbeat(1_500)
|
scheduleHeartbeat(1_500)
|
||||||
}
|
}
|
||||||
@@ -147,6 +142,26 @@ class MainActivity : AppCompatActivity() {
|
|||||||
loadServerBase(base.trimEnd('/'), forcePickerOnFailure = true, manualRefresh = false)
|
loadServerBase(base.trimEnd('/'), forcePickerOnFailure = true, manualRefresh = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
private fun configureWebView() {
|
||||||
|
val versionName = runCatching {
|
||||||
|
packageManager.getPackageInfo(packageName, 0).versionName
|
||||||
|
}.getOrNull() ?: "dev"
|
||||||
|
web.settings.javaScriptEnabled = true
|
||||||
|
web.settings.domStorageEnabled = true
|
||||||
|
web.settings.mediaPlaybackRequiresUserGesture = false
|
||||||
|
web.settings.userAgentString = web.settings.userAgentString + " RelayTV/$versionName"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openPrivacyPolicy() {
|
||||||
|
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.privacy_policy_url)))
|
||||||
|
runCatching {
|
||||||
|
startActivity(browserIntent)
|
||||||
|
}.onFailure {
|
||||||
|
Toast.makeText(this, getString(R.string.privacy_open_failed), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
isInForeground = true
|
isInForeground = true
|
||||||
@@ -173,13 +188,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private fun loadServerBase(base: String, forcePickerOnFailure: Boolean, manualRefresh: Boolean) {
|
private fun loadServerBase(base: String, forcePickerOnFailure: Boolean, manualRefresh: Boolean) {
|
||||||
val normalized = base.trimEnd('/')
|
val normalized = base.trimEnd('/')
|
||||||
if (normalized.isBlank()) {
|
if (normalized.isBlank()) {
|
||||||
swipeRefresh.isRefreshing = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
activeBaseUrl = normalized
|
activeBaseUrl = normalized
|
||||||
if (manualRefresh || !initialLoadComplete) {
|
|
||||||
swipeRefresh.isRefreshing = true
|
|
||||||
}
|
|
||||||
probeServerHealth(
|
probeServerHealth(
|
||||||
base = normalized,
|
base = normalized,
|
||||||
forcePickerOnFailure = forcePickerOnFailure,
|
forcePickerOnFailure = forcePickerOnFailure,
|
||||||
@@ -191,7 +202,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
private fun loadActiveServer(forcePickerOnFailure: Boolean, manualRefresh: Boolean) {
|
private fun loadActiveServer(forcePickerOnFailure: Boolean, manualRefresh: Boolean) {
|
||||||
val base = activeBaseUrl ?: HostStore.getActiveBaseUrl(this)?.trimEnd('/')
|
val base = activeBaseUrl ?: HostStore.getActiveBaseUrl(this)?.trimEnd('/')
|
||||||
if (base.isNullOrBlank()) {
|
if (base.isNullOrBlank()) {
|
||||||
swipeRefresh.isRefreshing = false
|
|
||||||
showServerPicker(force = true)
|
showServerPicker(force = true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -248,7 +258,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
mainFrameFailed = false
|
mainFrameFailed = false
|
||||||
initialLoadComplete = true
|
initialLoadComplete = true
|
||||||
swipeRefresh.isRefreshing = false
|
|
||||||
scheduleHeartbeat(HEARTBEAT_OK_MS)
|
scheduleHeartbeat(HEARTBEAT_OK_MS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,7 +266,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun onServerHealthFailed(forcePickerOnFailure: Boolean, manualRefresh: Boolean) {
|
private fun onServerHealthFailed(forcePickerOnFailure: Boolean, manualRefresh: Boolean) {
|
||||||
consecutiveHealthFailures += 1
|
consecutiveHealthFailures += 1
|
||||||
swipeRefresh.isRefreshing = false
|
|
||||||
|
|
||||||
if (forcePickerOnFailure && !initialLoadComplete) {
|
if (forcePickerOnFailure && !initialLoadComplete) {
|
||||||
Toast.makeText(this, "Server not reachable. Switch servers.", Toast.LENGTH_LONG).show()
|
Toast.makeText(this, "Server not reachable. Switch servers.", Toast.LENGTH_LONG).show()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package pro.relaytv
|
package pro.relaytv
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
@@ -10,6 +11,11 @@ import androidx.work.WorkManager
|
|||||||
|
|
||||||
class ShareActivity : AppCompatActivity() {
|
class ShareActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val META_ENDPOINT_PATH = "pro.relaytv.SHARE_ENDPOINT_PATH"
|
||||||
|
private const val META_SUCCESS_TEMPLATE = "pro.relaytv.SHARE_SUCCESS_TEMPLATE"
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@@ -26,10 +32,13 @@ class ShareActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val shared = intent.getStringExtra(Intent.EXTRA_TEXT) ?: ""
|
val shared = intent.getStringExtra(Intent.EXTRA_TEXT) ?: ""
|
||||||
val url = extractUrl(shared) ?: run { finish(); return }
|
val url = extractUrl(shared) ?: run { finish(); return }
|
||||||
|
val endpointPath = resolveMetaString(META_ENDPOINT_PATH).ifBlank { "/smart" }
|
||||||
|
val successTemplate = resolveMetaString(META_SUCCESS_TEMPLATE).ifBlank { "Sent to \"%s\"" }
|
||||||
|
|
||||||
val input = Data.Builder()
|
val input = Data.Builder()
|
||||||
.putString(ShareWorker.KEY_BASE, base)
|
.putString(ShareWorker.KEY_BASE, base)
|
||||||
.putString(ShareWorker.KEY_URL, url)
|
.putString(ShareWorker.KEY_URL, url)
|
||||||
|
.putString(ShareWorker.KEY_ENDPOINT_PATH, endpointPath)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val req = OneTimeWorkRequestBuilder<ShareWorker>()
|
val req = OneTimeWorkRequestBuilder<ShareWorker>()
|
||||||
@@ -38,10 +47,20 @@ class ShareActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
WorkManager.getInstance(this).enqueue(req)
|
WorkManager.getInstance(this).enqueue(req)
|
||||||
val serverName = HostStore.getActiveHost(this)?.name?.ifBlank { "Server" } ?: "Server"
|
val serverName = HostStore.getActiveHost(this)?.name?.ifBlank { "Server" } ?: "Server"
|
||||||
Toast.makeText(this, "Sent to \"$serverName\"", Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, successTemplate.format(serverName), Toast.LENGTH_SHORT).show()
|
||||||
finishAndRemoveTask()
|
finishAndRemoveTask()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun resolveMetaString(key: String): String {
|
||||||
|
return try {
|
||||||
|
val flags = PackageManager.GET_META_DATA
|
||||||
|
val info = packageManager.getActivityInfo(componentName, flags)
|
||||||
|
info.metaData?.getString(key).orEmpty()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun extractUrl(text: String): String? {
|
private fun extractUrl(text: String): String? {
|
||||||
val m = Regex("""https?://\S+""").find(text)
|
val m = Regex("""https?://\S+""").find(text)
|
||||||
return m?.value?.trim()?.trimEnd(')', ']', '>', '"', '\'')
|
return m?.value?.trim()?.trimEnd(')', ']', '>', '"', '\'')
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.app.NotificationManager
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
@@ -16,6 +15,7 @@ class ShareWorker(appContext: Context, params: WorkerParameters) : Worker(appCon
|
|||||||
companion object {
|
companion object {
|
||||||
const val KEY_BASE = "base"
|
const val KEY_BASE = "base"
|
||||||
const val KEY_URL = "url"
|
const val KEY_URL = "url"
|
||||||
|
const val KEY_ENDPOINT_PATH = "endpoint_path"
|
||||||
private const val CHANNEL_ID = "relaytv_silent"
|
private const val CHANNEL_ID = "relaytv_silent"
|
||||||
private const val NOTIF_ID = 4242
|
private const val NOTIF_ID = 4242
|
||||||
}
|
}
|
||||||
@@ -23,9 +23,10 @@ class ShareWorker(appContext: Context, params: WorkerParameters) : Worker(appCon
|
|||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val base = inputData.getString(KEY_BASE)?.trim()?.trimEnd('/') ?: return Result.failure()
|
val base = inputData.getString(KEY_BASE)?.trim()?.trimEnd('/') ?: return Result.failure()
|
||||||
val url = inputData.getString(KEY_URL) ?: return Result.failure()
|
val url = inputData.getString(KEY_URL) ?: return Result.failure()
|
||||||
|
val endpointPath = inputData.getString(KEY_ENDPOINT_PATH)?.trim()?.ifBlank { "/smart" } ?: "/smart"
|
||||||
return try {
|
return try {
|
||||||
val payload = JSONObject().put("url", url).toString()
|
val payload = JSONObject().put("url", url).toString()
|
||||||
val req = Net.postJson(base + "/smart", payload)
|
val req = Net.postJson(base + endpointPath, payload)
|
||||||
|
|
||||||
Net.client.newCall(req).execute().use { resp ->
|
Net.client.newCall(req).execute().use { resp ->
|
||||||
val body = resp.body?.string().orEmpty()
|
val body = resp.body?.string().orEmpty()
|
||||||
@@ -57,10 +58,8 @@ class ShareWorker(appContext: Context, params: WorkerParameters) : Worker(appCon
|
|||||||
private fun postNotification(title: String, text: String, tapToOpen: Boolean) {
|
private fun postNotification(title: String, text: String, tapToOpen: Boolean) {
|
||||||
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
val ch = NotificationChannel(CHANNEL_ID, "RelayTV", NotificationManager.IMPORTANCE_LOW)
|
||||||
val ch = NotificationChannel(CHANNEL_ID, "RelayTV", NotificationManager.IMPORTANCE_LOW)
|
nm.createNotificationChannel(ch)
|
||||||
nm.createNotificationChannel(ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||||
.setSmallIcon(android.R.drawable.ic_media_play)
|
.setSmallIcon(android.R.drawable.ic_media_play)
|
||||||
@@ -78,7 +77,7 @@ class ShareWorker(appContext: Context, params: WorkerParameters) : Worker(appCon
|
|||||||
applicationContext,
|
applicationContext,
|
||||||
0,
|
0,
|
||||||
openIntent,
|
openIntent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0)
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
builder.setContentIntent(pending)
|
builder.setContentIntent(pending)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="120"
|
||||||
|
android:viewportHeight="120">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M12.98,22.42v74.54c0,5.48 2.19,9.87 6.58,12.06 4.38,2.19 8.77,2.19 13.15,0l70.15,-37.27c4.38,-2.19 6.58,-6.58 6.58,-12.06 0,-5.48 -2.19,-9.87 -6.58,-12.06L32.71,10.36C23.47,5.74 12.98,12.99 12.98,22.42z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#1B2439"
|
||||||
|
android:pathData="M16.98,25.69v68c0,5 2,9 6,11 4,2 8,2 12,0l64,-34c4,-2 6,-6 6,-11 0,-5 -2,-9 -6,-11l-64,-34c-8.43,-4.22 -18,2.41 -18,11z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M64.89,39.13C61.32,36.63 38.94,24.09 34.01,21.83c-5.76,-2.64 -10.27,1.85 -9.88,5.88 0.17,1.77 0.18,57.55 0.21,63.09 0.04,8.03 5.25,8.1 8.46,7.16 5.14,-1.41 28.58,-15.18 29.63,-15.78"
|
||||||
|
android:strokeColor="#36D9F4"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="5" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M66.28,72.15C65.1,71.98 52.37,69.37 50.4,69.24M67.69,50.59C65.1,51.84 49.2,61.51 49.2,61.51"
|
||||||
|
android:strokeColor="#2D89FF"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeWidth="4" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#2D89FF"
|
||||||
|
android:pathData="M73.01,66.99a6.97,6.97 0 1,1 0,13.93a6.97,6.97 0 1,1 0,-13.93z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#36D9F4"
|
||||||
|
android:pathData="M44.86,55.91a9.96,9.96 0 1,1 0,19.92a9.96,9.96 0 1,1 0,-19.92z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#2D89FF"
|
||||||
|
android:pathData="M74.24,41.12a7,7 0 1,1 0,14a7,7 0 1,1 0,-14z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="120"
|
||||||
|
android:viewportHeight="120">
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M16.98,25.69v68c0,5 2,9 6,11 4,2 8,2 12,0l64,-34c4,-2 6,-6 6,-11 0,-5 -2,-9 -6,-11l-64,-34c-8.43,-4.22 -18,2.41 -18,11z"
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="6" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M64.89,39.13C61.32,36.63 38.94,24.09 34.01,21.83c-5.76,-2.64 -10.27,1.85 -9.88,5.88 0.17,1.77 0.18,57.55 0.21,63.09 0.04,8.03 5.25,8.1 8.46,7.16 5.14,-1.41 28.58,-15.18 29.63,-15.78"
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="5" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M66.28,72.15C65.1,71.98 52.37,69.37 50.4,69.24M67.69,50.59C65.1,51.84 49.2,61.51 49.2,61.51"
|
||||||
|
android:strokeColor="#FFFFFF"
|
||||||
|
android:strokeLineCap="round"
|
||||||
|
android:strokeWidth="4" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M73.01,66.99a6.97,6.97 0 1,1 0,13.93a6.97,6.97 0 1,1 0,-13.93z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M44.86,55.91a9.96,9.96 0 1,1 0,19.92a9.96,9.96 0 1,1 0,-19.92z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M74.24,41.12a7,7 0 1,1 0,14a7,7 0 1,1 0,-14z" />
|
||||||
|
</vector>
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent">
|
||||||
android:background="@color/relaytv_surface">
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -18,16 +17,10 @@
|
|||||||
app:menu="@menu/main_menu" />
|
app:menu="@menu/main_menu" />
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<WebView
|
||||||
android:id="@+id/swipeRefresh"
|
android:id="@+id/web"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
<WebView
|
|
||||||
android:id="@+id/web"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:fillViewport="true">
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="20dp">
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="RelayTV Server"
|
|
||||||
android:textSize="22sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:paddingBottom="10dp"/>
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Base URL (HTTP or HTTPS). Example: http://nuc.lan:8787"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:paddingBottom="8dp"/>
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/editBaseUrl"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="http://nuc.lan:8787"
|
|
||||||
android:inputType="textUri"
|
|
||||||
android:singleLine="true"/>
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:paddingTop="14dp">
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnSave"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="Save"/>
|
|
||||||
<Space
|
|
||||||
android:layout_width="12dp"
|
|
||||||
android:layout_height="wrap_content"/>
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnDetect"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="Auto-detect"/>
|
|
||||||
</LinearLayout>
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/txtStatus"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text=""
|
|
||||||
android:textSize="13sp"
|
|
||||||
android:paddingTop="10dp"/>
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Tip: Share a link from BravePipe → RelayTV. It sends to /smart in the background and shows a notification (tap it to open UI)."
|
|
||||||
android:textSize="13sp"
|
|
||||||
android:paddingTop="16dp"/>
|
|
||||||
</LinearLayout>
|
|
||||||
</ScrollView>
|
|
||||||
@@ -11,4 +11,8 @@
|
|||||||
android:title="@string/reload"
|
android:title="@string/reload"
|
||||||
android:icon="@android:drawable/ic_popup_sync"
|
android:icon="@android:drawable/ic_popup_sync"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_privacy"
|
||||||
|
android:title="@string/privacy_policy"
|
||||||
|
app:showAsAction="never" />
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/relaytv_surface" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@color/relaytv_surface" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
|
||||||
|
</adaptive-icon>
|
||||||
@@ -21,4 +21,11 @@
|
|||||||
<string name="status_offline">Offline</string>
|
<string name="status_offline">Offline</string>
|
||||||
<string name="status_checking">Checking…</string>
|
<string name="status_checking">Checking…</string>
|
||||||
<string name="status_active_prefix">Active</string>
|
<string name="status_active_prefix">Active</string>
|
||||||
|
<string name="share_queue_label">RelayTV Queue</string>
|
||||||
|
<string name="share_play_now_label">RelayTV Play</string>
|
||||||
|
<string name="share_queue_toast">Queued on "%s"</string>
|
||||||
|
<string name="share_play_now_toast">Playing now on "%s"</string>
|
||||||
|
<string name="privacy_policy">Privacy policy</string>
|
||||||
|
<string name="privacy_policy_url">https://github.com/mcgeezy/relaytv-android/blob/main/docs/PRIVACY_POLICY.md</string>
|
||||||
|
<string name="privacy_open_failed">No browser available to open the privacy policy.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -10,12 +10,4 @@
|
|||||||
|
|
||||||
</style>
|
</style>
|
||||||
<style name="Theme.RelayTV.NoActionBar" parent="Theme.Material3.DayNight.NoActionBar" />
|
<style name="Theme.RelayTV.NoActionBar" parent="Theme.Material3.DayNight.NoActionBar" />
|
||||||
|
|
||||||
<style name="Theme.RelayTV.Share" parent="Theme.Material3.DayNight.NoActionBar">
|
|
||||||
<item name="android:windowIsTranslucent">true</item>
|
|
||||||
<item name="android:windowBackground">@android:color/transparent</item>
|
|
||||||
<item name="android:colorBackground">@android:color/transparent</item>
|
|
||||||
<item name="android:backgroundDimEnabled">false</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<full-backup-content>
|
||||||
|
<exclude domain="root" path="." />
|
||||||
|
</full-backup-content>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup>
|
||||||
|
<exclude domain="root" path="." />
|
||||||
|
</cloud-backup>
|
||||||
|
<device-transfer>
|
||||||
|
<exclude domain="root" path="." />
|
||||||
|
</device-transfer>
|
||||||
|
</data-extraction-rules>
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<network-security-config>
|
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||||
<base-config cleartextTrafficPermitted="true" />
|
<base-config
|
||||||
|
cleartextTrafficPermitted="true"
|
||||||
|
tools:ignore="InsecureBaseConfiguration" />
|
||||||
</network-security-config>
|
</network-security-config>
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# RelayTV Android Privacy Policy
|
||||||
|
|
||||||
|
Last updated: April 3, 2026
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
RelayTV Android is a companion remote for self-hosted RelayTV servers. The app does not require an account, does not include advertising, and does not use third-party analytics SDKs.
|
||||||
|
|
||||||
|
## Information The App Processes
|
||||||
|
|
||||||
|
RelayTV Android may process the following categories of data in order to operate:
|
||||||
|
|
||||||
|
1. RelayTV server information
|
||||||
|
- Server names and base URLs you add in the app
|
||||||
|
- The currently selected active server
|
||||||
|
2. Shared content
|
||||||
|
- Links you share into the app using Android share targets such as `RelayTV Queue` and `RelayTV Play`
|
||||||
|
3. Local network discovery data
|
||||||
|
- RelayTV service advertisements discovered on your local network via mDNS / NSD
|
||||||
|
4. Device-local notification data
|
||||||
|
- Notification text used to confirm a queue or play action
|
||||||
|
|
||||||
|
## How The App Uses Data
|
||||||
|
|
||||||
|
RelayTV Android uses the data above only to:
|
||||||
|
|
||||||
|
1. Connect to the RelayTV server you selected
|
||||||
|
2. Load the RelayTV web UI in an embedded WebView
|
||||||
|
3. Send shared links to RelayTV endpoints such as `/smart` or `/play_now`
|
||||||
|
4. Discover compatible RelayTV servers on your local network
|
||||||
|
5. Show local notifications about share results and reconnect state
|
||||||
|
|
||||||
|
## Data Storage
|
||||||
|
|
||||||
|
The app stores configured server names and base URLs locally on your device using Android shared preferences.
|
||||||
|
|
||||||
|
The app does not operate its own cloud backend and does not sync your app data to a RelayTV-operated online service.
|
||||||
|
|
||||||
|
## Data Sharing
|
||||||
|
|
||||||
|
RelayTV Android sends requests only to the RelayTV server you configured or discovered and chose to use.
|
||||||
|
|
||||||
|
The app does not sell personal information and does not share data with advertising networks or analytics providers.
|
||||||
|
|
||||||
|
## Network Security
|
||||||
|
|
||||||
|
RelayTV is designed for local-first and self-hosted use. Because many self-hosted RelayTV installations run on trusted LAN HTTP endpoints, the app allows cleartext HTTP connections in addition to HTTPS. If your RelayTV server is exposed outside a trusted private network, you should use HTTPS.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
RelayTV Android may request or use these permissions:
|
||||||
|
|
||||||
|
1. `INTERNET`
|
||||||
|
- Required to contact your RelayTV server
|
||||||
|
2. `ACCESS_NETWORK_STATE`
|
||||||
|
- Required to detect connectivity changes and recover the UI after network interruptions
|
||||||
|
3. `CHANGE_WIFI_MULTICAST_STATE`
|
||||||
|
- Required for local network RelayTV discovery on some devices
|
||||||
|
4. `POST_NOTIFICATIONS`
|
||||||
|
- Required on newer Android versions to show queue/play result notifications
|
||||||
|
|
||||||
|
## Children
|
||||||
|
|
||||||
|
RelayTV Android is not directed to children and does not knowingly collect personal information from children.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
This privacy policy may be updated when app behavior changes. Update the "Last updated" date whenever this document changes materially.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
For project updates or issue reporting, use the project repository:
|
||||||
|
|
||||||
|
https://github.com/mcgeezy/relaytv-android
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# RelayTV Android Release Checklist
|
||||||
|
|
||||||
|
Use this checklist before cutting a public release or uploading a new Play Console build.
|
||||||
|
|
||||||
|
## Build Readiness
|
||||||
|
|
||||||
|
1. Verify Android toolchain prerequisites:
|
||||||
|
- `./scripts/check-android-env.sh all`
|
||||||
|
2. Install or refresh required SDK components:
|
||||||
|
- `./scripts/bootstrap-android.sh`
|
||||||
|
3. Validate debug build, lint, and unit tests:
|
||||||
|
- `./scripts/build-debug.sh`
|
||||||
|
4. Validate release bundle and release lint:
|
||||||
|
- `./scripts/build-release.sh`
|
||||||
|
5. If you have a device connected, run instrumentation tests:
|
||||||
|
- `./scripts/test-connected.sh`
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
1. Increment `versionCode` for every public upload.
|
||||||
|
2. Set `versionName` to the user-facing release number.
|
||||||
|
3. Verify the user agent string and release notes match the version being shipped.
|
||||||
|
|
||||||
|
## Play Console Requirements
|
||||||
|
|
||||||
|
1. Keep `targetSdk` at the current Google Play minimum.
|
||||||
|
- As of April 3, 2026, Play requires new apps and updates to target Android 15 / API 35.
|
||||||
|
2. Upload an Android App Bundle:
|
||||||
|
- `app/build/outputs/bundle/release/app-release.aab`
|
||||||
|
3. Use Play App Signing in Play Console.
|
||||||
|
4. Complete the Data safety form for this release.
|
||||||
|
5. Provide a valid support email in Play Console.
|
||||||
|
6. Publish a public privacy policy URL.
|
||||||
|
- This repo includes a draft policy in `docs/PRIVACY_POLICY.md`.
|
||||||
|
- The in-app privacy menu points to the GitHub-hosted copy on `main`.
|
||||||
|
|
||||||
|
## App-Specific Verification
|
||||||
|
|
||||||
|
1. Confirm manual server entry still works for both HTTP and HTTPS RelayTV servers.
|
||||||
|
2. Confirm LAN discovery finds `_relaytv._tcp` services on the local network.
|
||||||
|
3. Confirm both Android share targets work:
|
||||||
|
- `RelayTV Queue` -> `/smart`
|
||||||
|
- `RelayTV Play` -> `/play_now`
|
||||||
|
4. Confirm the WebView reconnects after the RelayTV server restarts or the network drops.
|
||||||
|
5. Confirm the privacy menu opens the published policy URL.
|
||||||
|
|
||||||
|
## Signing And Distribution
|
||||||
|
|
||||||
|
1. Configure your release signing key or Play App Signing flow.
|
||||||
|
2. Archive the signed `.aab`, release notes, and the exact commit SHA used for the build.
|
||||||
|
3. Tag the release in git after the Play upload is verified.
|
||||||
|
|
||||||
|
## Official References
|
||||||
|
|
||||||
|
1. Target API requirement:
|
||||||
|
- https://developer.android.com/google/play/requirements/target-sdk
|
||||||
|
2. App setup, versioning, and Play App Signing:
|
||||||
|
- https://support.google.com/googleplay/android-developer/answer/9859152
|
||||||
|
3. Developer support contact requirements:
|
||||||
|
- https://support.google.com/googleplay/android-developer/answer/113477
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
android.suppressUnsupportedCompileSdk=35
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<lint>
|
||||||
|
<issue id="GradleDependency" severity="ignore" />
|
||||||
|
<issue id="IconDuplicates" severity="ignore" />
|
||||||
|
<issue id="IconLauncherShape" severity="ignore" />
|
||||||
|
</lint>
|
||||||
@@ -5,9 +5,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
INSTALL_EMULATOR="${INSTALL_EMULATOR:-0}"
|
INSTALL_EMULATOR="${INSTALL_EMULATOR:-0}"
|
||||||
ANDROID_PLATFORM="${ANDROID_PLATFORM:-android-34}"
|
ANDROID_PLATFORM="${ANDROID_PLATFORM:-android-35}"
|
||||||
ANDROID_BUILD_TOOLS="${ANDROID_BUILD_TOOLS:-34.0.0}"
|
ANDROID_BUILD_TOOLS="${ANDROID_BUILD_TOOLS:-35.0.0}"
|
||||||
ANDROID_SYSTEM_IMAGE="${ANDROID_SYSTEM_IMAGE:-system-images;android-34;google_apis;x86_64}"
|
ANDROID_SYSTEM_IMAGE="${ANDROID_SYSTEM_IMAGE:-system-images;android-35;google_apis;x86_64}"
|
||||||
|
|
||||||
say() { printf '%s\n' "$*"; }
|
say() { printf '%s\n' "$*"; }
|
||||||
|
|
||||||
|
|||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
cd "${REPO_ROOT}"
|
||||||
|
|
||||||
|
if [[ ! -x "./scripts/check-android-env.sh" ]]; then
|
||||||
|
chmod +x "./scripts/check-android-env.sh"
|
||||||
|
fi
|
||||||
|
./scripts/check-android-env.sh build
|
||||||
|
|
||||||
|
if [[ ! -x "./gradlew" ]]; then
|
||||||
|
chmod +x ./gradlew
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$#" -gt 0 ]]; then
|
||||||
|
TASKS=("$@")
|
||||||
|
else
|
||||||
|
TASKS=("clean" "bundleRelease" "lintRelease")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Running Gradle tasks: ${TASKS[*]}"
|
||||||
|
./gradlew --no-daemon "${TASKS[@]}"
|
||||||
@@ -7,6 +7,8 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|||||||
|
|
||||||
failures=0
|
failures=0
|
||||||
warnings=0
|
warnings=0
|
||||||
|
ANDROID_PLATFORM="${ANDROID_PLATFORM:-android-35}"
|
||||||
|
ANDROID_BUILD_TOOLS="${ANDROID_BUILD_TOOLS:-35.0.0}"
|
||||||
|
|
||||||
say() { printf '%s\n' "$*"; }
|
say() { printf '%s\n' "$*"; }
|
||||||
ok() { say "[ok] $*"; }
|
ok() { say "[ok] $*"; }
|
||||||
@@ -90,6 +92,20 @@ check_sdk() {
|
|||||||
else
|
else
|
||||||
warn "sdkmanager not found in PATH or ${sdk_root}/cmdline-tools/latest/bin."
|
warn "sdkmanager not found in PATH or ${sdk_root}/cmdline-tools/latest/bin."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${sdk_root}" ]]; then
|
||||||
|
if [[ -d "${sdk_root}/platforms/${ANDROID_PLATFORM}" ]]; then
|
||||||
|
ok "Found Android platform: ${ANDROID_PLATFORM}"
|
||||||
|
else
|
||||||
|
fail "Missing Android platform: ${ANDROID_PLATFORM}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "${sdk_root}/build-tools/${ANDROID_BUILD_TOOLS}" ]]; then
|
||||||
|
ok "Found Android build-tools: ${ANDROID_BUILD_TOOLS}"
|
||||||
|
else
|
||||||
|
fail "Missing Android build-tools: ${ANDROID_BUILD_TOOLS}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
check_gradle_wrapper() {
|
check_gradle_wrapper() {
|
||||||
|
|||||||
Reference in New Issue
Block a user