diff --git a/VERSION b/VERSION
index a412349..8f8b3f7 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.7.10
+1.7.11
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index c66ac14..ab9c1ee 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -11,8 +11,8 @@ android {
applicationId = "com.remotedisplay.player"
minSdk = 26
targetSdk = 34
- versionCode = 13
- versionName = "1.7.10"
+ versionCode = 14
+ versionName = "1.7.11"
}
signingConfigs {
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 16cf27c..3767a67 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -14,6 +14,8 @@
+
+
= Build.VERSION_CODES.O) {
- val channel = NotificationChannel(
- CHANNEL_ID,
- CHANNEL_NAME,
- NotificationManager.IMPORTANCE_LOW
- ).apply {
- description = "ScreenTinker background service"
- setShowBadge(false)
- }
val manager = getSystemService(NotificationManager::class.java)
- manager.createNotificationChannel(channel)
+ manager.createNotificationChannel(
+ NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW).apply {
+ description = "ScreenTinker background service"
+ setShowBadge(false)
+ }
+ )
+ manager.createNotificationChannel(
+ NotificationChannel(BOOT_CHANNEL_ID, "ScreenTinker Startup", NotificationManager.IMPORTANCE_HIGH).apply {
+ description = "Launches the display on boot"
+ setShowBadge(false)
+ }
+ )
}
}
}
diff --git a/android/app/src/main/java/com/remotedisplay/player/SetupActivity.kt b/android/app/src/main/java/com/remotedisplay/player/SetupActivity.kt
index fde4451..d696f63 100644
--- a/android/app/src/main/java/com/remotedisplay/player/SetupActivity.kt
+++ b/android/app/src/main/java/com/remotedisplay/player/SetupActivity.kt
@@ -2,11 +2,15 @@ package com.remotedisplay.player
import android.Manifest
import android.accessibilityservice.AccessibilityServiceInfo
+import android.annotation.SuppressLint
+import android.app.NotificationManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.net.Uri
import android.os.Build
+import android.os.PowerManager
import android.os.Bundle
import android.provider.Settings
import android.view.View
@@ -26,8 +30,15 @@ class SetupActivity : AppCompatActivity() {
private lateinit var notificationStatus: TextView
private lateinit var enableAccessibilityBtn: Button
private lateinit var enableInstallBtn: Button
+ private lateinit var fullscreenStatus: TextView
+ private lateinit var enableFullscreenBtn: Button
+ private lateinit var batteryStatus: TextView
+ private lateinit var enableBatteryBtn: Button
+ private lateinit var overlayStatus: TextView
+ private lateinit var enableOverlayBtn: Button
private lateinit var continueBtn: Button
+ @SuppressLint("BatteryLife")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -40,6 +51,9 @@ class SetupActivity : AppCompatActivity() {
setContentView(R.layout.activity_setup)
+ // App's UI is up — clear the boot "Starting display…" notification.
+ getSystemService(NotificationManager::class.java)?.cancel(999)
+
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
@@ -75,11 +89,60 @@ class SetupActivity : AppCompatActivity() {
enableInstallBtn.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startActivity(Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
- data = android.net.Uri.parse("package:$packageName")
+ data = Uri.parse("package:$packageName")
})
}
}
+ fullscreenStatus = findViewById(R.id.fullscreenStatus)
+ enableFullscreenBtn = findViewById(R.id.enableFullscreenBtn)
+ batteryStatus = findViewById(R.id.batteryStatus)
+ enableBatteryBtn = findViewById(R.id.enableBatteryBtn)
+ overlayStatus = findViewById(R.id.overlayStatus)
+ enableOverlayBtn = findViewById(R.id.enableOverlayBtn)
+
+ // Display-over-other-apps: alternate boot-launch path. With this granted the
+ // boot receiver can directly start the activity from the background, which
+ // works where you can't set a launcher (e.g. Android TV).
+ enableOverlayBtn.setOnClickListener {
+ startActivity(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
+ data = Uri.parse("package:$packageName")
+ })
+ }
+
+ // Launch-on-boot needs USE_FULL_SCREEN_INTENT, which Android 14+ auto-revokes
+ // for non-calling apps — so the boot full-screen launcher silently fails until
+ // the user grants it. Older versions auto-grant it, so only show the row where
+ // it can actually be off.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ // USE_FULL_SCREEN_INTENT is auto-granted before Android 14 — hide the row.
+ findViewById(R.id.fullscreenRow).visibility = View.GONE
+ } else {
+ enableFullscreenBtn.setOnClickListener {
+ try {
+ startActivity(Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT).apply {
+ data = Uri.parse("package:$packageName")
+ })
+ } catch (e: Exception) {
+ startActivity(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
+ putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
+ })
+ }
+ }
+ }
+
+ // Battery-optimization exemption keeps the boot receiver from being deferred
+ // and the app from being killed in standby (esp. on OEM / TV boxes).
+ enableBatteryBtn.setOnClickListener {
+ try {
+ startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
+ data = Uri.parse("package:$packageName")
+ })
+ } catch (e: Exception) {
+ startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))
+ }
+ }
+
continueBtn.setOnClickListener {
prefs.edit().putBoolean("setup_complete", true).apply()
proceedToNext()
@@ -130,6 +193,27 @@ class SetupActivity : AppCompatActivity() {
if (hasNotif) View.GONE else View.VISIBLE
}
+ // Launch on boot (full-screen intent — only restrictable on Android 14+)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ val canFsi = getSystemService(NotificationManager::class.java).canUseFullScreenIntent()
+ fullscreenStatus.text = if (canFsi) "ON" else "OFF"
+ fullscreenStatus.setTextColor(if (canFsi) 0xFF22C55E.toInt() else 0xFFEF4444.toInt())
+ enableFullscreenBtn.visibility = if (canFsi) View.GONE else View.VISIBLE
+ }
+
+ // Battery optimization exemption
+ val ignoringBattery = (getSystemService(Context.POWER_SERVICE) as PowerManager)
+ .isIgnoringBatteryOptimizations(packageName)
+ batteryStatus.text = if (ignoringBattery) "ON" else "OFF"
+ batteryStatus.setTextColor(if (ignoringBattery) 0xFF22C55E.toInt() else 0xFFEF4444.toInt())
+ enableBatteryBtn.visibility = if (ignoringBattery) View.GONE else View.VISIBLE
+
+ // Display over other apps
+ val canOverlay = Settings.canDrawOverlays(this)
+ overlayStatus.text = if (canOverlay) "ON" else "OFF"
+ overlayStatus.setTextColor(if (canOverlay) 0xFF22C55E.toInt() else 0xFFEF4444.toInt())
+ enableOverlayBtn.visibility = if (canOverlay) View.GONE else View.VISIBLE
+
// Update continue button text
val allGood = accessibilityEnabled && canInstall
continueBtn.text = if (allGood) "Continue to Setup" else "Continue Anyway"
diff --git a/android/app/src/main/java/com/remotedisplay/player/service/BootReceiver.kt b/android/app/src/main/java/com/remotedisplay/player/service/BootReceiver.kt
index e1711c2..90027fe 100644
--- a/android/app/src/main/java/com/remotedisplay/player/service/BootReceiver.kt
+++ b/android/app/src/main/java/com/remotedisplay/player/service/BootReceiver.kt
@@ -5,6 +5,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
+import android.provider.Settings
import android.util.Log
import androidx.core.app.NotificationCompat
import android.app.NotificationManager
@@ -33,18 +34,31 @@ class BootReceiver : BroadcastReceiver() {
Log.e("BootReceiver", "Failed to start service: ${e.message}")
}
- // Use a full-screen intent to launch the activity (bypasses Android 12+ restrictions)
- try {
- val launchIntent = Intent(context, MainActivity::class.java).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
- }
+ val launchIntent = Intent(context, MainActivity::class.java).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ }
+ // Primary: with "display over other apps" granted, a direct background
+ // startActivity is permitted — the most reliable launch, and the one that
+ // works on Android TV where you can't set a home launcher.
+ if (Settings.canDrawOverlays(context)) {
+ try {
+ context.startActivity(launchIntent)
+ Log.i("BootReceiver", "Direct launch (overlay permission)")
+ } catch (e: Exception) {
+ Log.e("BootReceiver", "Direct launch failed: ${e.message}")
+ }
+ }
+
+ // Fallback: full-screen-intent notification (covers a locked screen / when
+ // the overlay permission isn't granted).
+ try {
val pendingIntent = PendingIntent.getActivity(
context, 0, launchIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
- val notification = NotificationCompat.Builder(context, RemoteDisplayApp.CHANNEL_ID)
+ val notification = NotificationCompat.Builder(context, RemoteDisplayApp.BOOT_CHANNEL_ID)
.setContentTitle("ScreenTinker")
.setContentText("Starting display...")
.setSmallIcon(android.R.drawable.ic_media_play)
diff --git a/android/app/src/main/res/layout/activity_setup.xml b/android/app/src/main/res/layout/activity_setup.xml
index b845e7a..7f86a7d 100644
--- a/android/app/src/main/res/layout/activity_setup.xml
+++ b/android/app/src/main/res/layout/activity_setup.xml
@@ -6,31 +6,42 @@
android:gravity="center"
android:orientation="vertical"
android:background="#111827"
- android:padding="48dp"
+ android:padding="12dp"
android:keepScreenOn="true">
+
+
+
+
+ android:layout_marginBottom="3dp" />
+ android:textSize="9sp"
+ android:layout_marginBottom="8dp" />
+ android:layout_marginBottom="8dp">
+ android:layout_marginBottom="5dp">
+ android:textSize="8sp" />
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp" />
@@ -90,7 +105,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
- android:layout_marginBottom="16dp">
+ android:layout_marginBottom="5dp">
+ android:textSize="8sp" />
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp" />
@@ -143,7 +162,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
- android:layout_marginBottom="16dp"
+ android:layout_marginBottom="5dp"
android:visibility="gone">
+ android:textSize="8sp" />
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:background="@drawable/button_primary"
+ android:paddingTop="10dp"
+ android:paddingBottom="10dp" />
+
+
+