diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2b3e271..fe91e5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,16 @@ Versions after 2.17 are `MAJOR.MINOR.PATCH` (2.17 is followed by 2.17.1 or 2.18.
`docs/testing.md`, "Versions and releases"); the headings below keep the numbers as they
were released.
+## 2.19.0 — 2026-10-31
+
+- **Start on boot** in Settings, off by default: the app opens by itself after the device
+ starts, for a box that only shows cameras. Android 10+ needs the permission to draw over
+ other apps for it; turning the setting on opens that system screen, or shows the adb command
+ on devices without one. A firmware autorun, where the device has one, needs nothing from the
+ app. `/stats.json` adds `start_on_boot` (#255).
+- Opening the app again while it plays (the launcher, a second autostart) no longer restarts
+ the streams; only a shared link does (#255).
+
## 2.18.4 — 2026-10-22
- `udp://` slots and the U cell: the receive thread runs at the audio priority, so a push no
diff --git a/README.md b/README.md
index 7551eeb..b9b0587 100644
--- a/README.md
+++ b/README.md
@@ -87,6 +87,10 @@ A beta channel is available on [Google Play](https://play.google.com/apps/intern
Every item, the remote, sharing a URL from another app and **Reset**: [Usage](docs/usage.md).
+## Start on boot
+
+For a box that only shows cameras, the app can open by itself after the device starts. Use the device's own autorun first, where its app list has one (the Menu key on the Decoder tile → **AUTORUN**). Otherwise turn on **Start on boot** in **Settings**; Android 10 and newer also need the permission to draw over other apps, and the app shows how to give it. Details: [Start on boot](docs/usage.md#start-on-boot).
+
## Known limitations
> [!IMPORTANT]
diff --git a/README.ru.md b/README.ru.md
index 7741cf2..96d73d0 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -87,6 +87,10 @@
Все пункты меню, пульт, передача адреса из другого приложения и **Reset** — в [Usage](docs/usage.md) (на английском).
+## Запуск при включении
+
+Приставка, которая только показывает камеры, может открывать приложение сама после включения. Сначала используйте автозапуск самого устройства, если он есть в его списке приложений (кнопка Menu на плитке Decoder → **AUTORUN**). Если его нет — включите **Start on boot** в **Settings**; на Android 10 и новее нужно ещё разрешение показывать окна поверх других приложений, приложение подскажет, как его дать. Подробно: [Start on boot](docs/usage.md#start-on-boot) (на английском).
+
## Известные ограничения
> [!IMPORTANT]
diff --git a/README.zh-CN.md b/README.zh-CN.md
index f23354a..3b8ff2e 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -87,6 +87,10 @@
菜单各项、遥控器、从其他应用分享地址以及 **Reset** 的说明见 [Usage](docs/usage.md)(英文)。
+## 开机自启
+
+只用来显示摄像机画面的盒子可以在开机后自动打开应用。如果设备自带的应用列表提供自启功能(在 Decoder 图标上按 Menu 键 → **AUTORUN**),请优先使用。否则在 **Settings** 中勾选 **Start on boot**;Android 10 及以上还需要"在其他应用上层显示"的权限,应用会提示如何授予。详见 [Start on boot](docs/usage.md#start-on-boot)(英文)。
+
## 已知限制
> [!IMPORTANT]
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 728f651..539f50d 100755
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -23,8 +23,8 @@ android {
applicationId "com.openipc.decoder"
minSdk 21
targetSdk 36
- versionCode 47
- versionName "2.18.4"
+ versionCode 48
+ versionName "2.19.0"
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index e224db1..a354971 100755
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -6,6 +6,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/java/com/openipc/decoder/BootReceiver.java b/android/app/src/main/java/com/openipc/decoder/BootReceiver.java
new file mode 100644
index 0000000..34193a3
--- /dev/null
+++ b/android/app/src/main/java/com/openipc/decoder/BootReceiver.java
@@ -0,0 +1,126 @@
+/*
+ *
+ * Copyright (c) OpenIPC https://openipc.org Prosperity Public License 3.0.0
+ *
+ * BootReceiver.java — opens the app after the device boots, when Start on boot is on (#255)
+ *
+ */
+
+package com.openipc.decoder;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.PixelFormat;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class BootReceiver extends BroadcastReceiver {
+ private static final String TAG = "OpenIPCDecoder";
+ /** In the "settings" preferences, so Reset (#171) turns it off too. */
+ static final String PREF = "start_on_boot";
+
+ static boolean enabled(Context context) {
+ return context.getSharedPreferences("settings", Context.MODE_PRIVATE)
+ .getBoolean(PREF, false);
+ }
+
+ static void setEnabled(Context context, boolean on) {
+ context.getSharedPreferences("settings", Context.MODE_PRIVATE).edit()
+ .putBoolean(PREF, on).apply();
+ }
+
+ /**
+ * Android 10+ starts an activity from the background only for an app that may draw over
+ * other apps; many TV boxes have no screen to allow it, adb does (appops).
+ */
+ static boolean overlayNeeded() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
+ }
+
+ static boolean overlayAllowed(Context context) {
+ return !overlayNeeded() || Settings.canDrawOverlays(context);
+ }
+
+ /** For /stats.json, with or without an activity (Qodo on #256). */
+ static Map state(Context context) {
+ Map m = new LinkedHashMap<>();
+ m.put("on", enabled(context));
+ m.put("overlay", !overlayNeeded() ? "not needed"
+ : overlayAllowed(context) ? "allowed" : "missing");
+ return m;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) return;
+ if (!enabled(context)) return;
+ // A firmware autorun opened it already: another start would pause and resume it,
+ // which restarts every stream
+ if (Decoder.openOrOpening()) {
+ Log.i(TAG, "Start on boot: the app is open already");
+ return;
+ }
+ if (!overlayAllowed(context)) {
+ Log.w(TAG, "Start on boot: no permission to draw over other apps, Android may block the start");
+ }
+ Log.i(TAG, "Start on boot: opening the app");
+ Intent start = new Intent(context, Decoder.class);
+ start.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Android 15+ lets an app that targets 35+ start from the background with this
+ // permission only while one of its overlay windows is visible (Qodo on #256)
+ View overlay = Build.VERSION.SDK_INT >= 35 && overlayAllowed(context)
+ ? showOverlay(context) : null;
+ try {
+ context.startActivity(start);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Start on boot failed", e);
+ }
+ if (overlay != null) {
+ PendingResult pending = goAsync();
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ removeOverlay(context, overlay);
+ pending.finish();
+ }, OVERLAY_MS);
+ }
+ }
+
+ /** Long enough for the start to pass the check, short enough to stay well under 10 s. */
+ private static final long OVERLAY_MS = 2000;
+
+ /** A 1×1 transparent window that takes no touch or focus; null if it cannot be shown. */
+ @TargetApi(35)
+ private static View showOverlay(Context context) {
+ WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ View v = new View(context.getApplicationContext());
+ WindowManager.LayoutParams lp = new WindowManager.LayoutParams(1, 1,
+ WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
+ PixelFormat.TRANSPARENT);
+ try {
+ wm.addView(v, lp);
+ return v;
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Start on boot: no overlay window", e);
+ return null;
+ }
+ }
+
+ private static void removeOverlay(Context context, View v) {
+ try {
+ ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).removeView(v);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Start on boot: overlay window not removed", e);
+ }
+ }
+}
diff --git a/android/app/src/main/java/com/openipc/decoder/DebugServer.java b/android/app/src/main/java/com/openipc/decoder/DebugServer.java
index 07daace..9423405 100644
--- a/android/app/src/main/java/com/openipc/decoder/DebugServer.java
+++ b/android/app/src/main/java/com/openipc/decoder/DebugServer.java
@@ -304,6 +304,7 @@ private Map snapshot() {
m.put("app", "OpenIPC Decoder");
m.put("server", state);
m.put("mode", "no activity");
+ if (App.context != null) m.put("start_on_boot", BootReceiver.state(App.context));
}
return m;
}
diff --git a/android/app/src/main/java/com/openipc/decoder/Decoder.java b/android/app/src/main/java/com/openipc/decoder/Decoder.java
index c785280..efecde1 100755
--- a/android/app/src/main/java/com/openipc/decoder/Decoder.java
+++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java
@@ -190,6 +190,7 @@ public class Decoder extends Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ sOpening = true;
setContentView(R.layout.decoder);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
@@ -592,7 +593,8 @@ private void restartClean() {
@Override
public void onShowSettings() {
- menuManager.showUrlEditor(mActive, mHosts, mTransports, mRotates, mMirrors);
+ menuManager.showUrlEditor(mActive, mHosts, mTransports, mRotates, mMirrors,
+ BootReceiver.enabled(this));
}
@Override
@@ -601,6 +603,21 @@ public void onShowSettings() {
@Override
public void onSaveSettings() { saveSettings(); }
+ @Override
+ public void onStartOnBoot(boolean on) {
+ BootReceiver.setEnabled(this, on);
+ Log.i(TAG, "Start on boot " + (on ? "on" : "off"));
+ if (!on || BootReceiver.overlayAllowed(this)) return;
+ // Android 10+ (#255): the system screen to allow drawing over other apps, if there is one
+ Intent allow = new Intent(android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
+ Uri.parse("package:" + getPackageName()));
+ try {
+ startActivity(allow);
+ } catch (ActivityNotFoundException e) {
+ menuManager.showBootPermissionHint(getPackageName());
+ }
+ }
+
// ====================== Video lifecycle ======================
/** The single view's client for the active slot, with the slot's turn (#201). */
@@ -1588,6 +1605,8 @@ private Map debugDevice() {
// The video decoders and what they can do: instances, adaptive playback (#88)
List