From 37336a7899dd73e6fb2d51247922026a078eec77 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Wed, 14 Oct 2026 19:01:00 +0000 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20Reset=20in=20the=20menu=20=E2=80=94?= =?UTF-8?q?=20factory=20settings=20after=20a=20confirmation=20(#171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reset (above Exit) asks first; Cancel has the focus, so a stray OK on a remote changes nothing. Reset deletes the stored settings, the Keystore key of the encrypted camera URLs and WebUI's cookies, then starts the app in a new process. --- README.md | 1 + README.ru.md | 1 + README.zh-CN.md | 1 + .../java/com/openipc/decoder/Decoder.java | 35 +++++++++++++++++++ .../com/openipc/decoder/KeystoreCipher.java | 7 ++++ .../java/com/openipc/decoder/MenuManager.java | 32 +++++++++++++++++ 6 files changed, 77 insertions(+) diff --git a/README.md b/README.md index 4af1eba8..dfcdf460 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ A beta channel is available on [Google Play](https://play.google.com/apps/intern | **S** | The playback mode: every frame, or key frames only (the picture updates once per key frame interval, MJPEG once a second), the lowest decoder load. Works in multi-view and in the single view and never changes the view | | **Settings** | Edit the URL and the transport of the active slot (single-camera mode only) | | **WebUI** | Open the camera's web interface; credentials come from the RTSP URL (single-camera mode only) | +| **Reset** | Reset to factory settings after a confirmation (**Cancel** has the focus): all camera slots, transports and settings are deleted and the app starts again as after installing | | **Exit** | Close the app. The small text next to it is the app version and build hash. | Items that are on — D, Q, S, U and the active slot — have an orange fill; the item the remote is on has a light blue frame. diff --git a/README.ru.md b/README.ru.md index 16fb009f..c6dab42e 100644 --- a/README.ru.md +++ b/README.ru.md @@ -68,6 +68,7 @@ | **S** | Режим воспроизведения: все кадры или только ключевые (картинка обновляется раз в интервал ключевых кадров, MJPEG — раз в секунду), минимальная нагрузка на декодер. Работает и в мультивиде, и в одиночном режиме, вид не меняет | | **Settings** | Изменить адрес и транспорт активного слота (только в режиме одной камеры) | | **WebUI** | Открыть веб-интерфейс камеры; логин и пароль берутся из RTSP-адреса (только в режиме одной камеры) | +| **Reset** | Сброс к заводским настройкам после подтверждения (фокус на **Cancel**): удаляются все слоты камер, транспорты и настройки, приложение запускается заново, как после установки | | **Exit** | Закрыть приложение. Мелкий текст рядом — версия приложения и хеш сборки. | Включённые пункты — D, Q, S, U и активный слот — залиты оранжевым; пункт, на котором стоит пульт, обведён голубой рамкой. diff --git a/README.zh-CN.md b/README.zh-CN.md index f1f01458..637de928 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -68,6 +68,7 @@ | **S** | 播放模式:全部帧或仅关键帧(画面按关键帧间隔更新,MJPEG 每秒一帧),解码器负载最低。多画面和单画面均适用,不切换视图 | | **Settings** | 编辑当前槽位的地址和传输方式(仅单路模式) | | **WebUI** | 打开摄像机网页界面,用户名和密码取自 RTSP 地址(仅单路模式) | +| **Reset** | 确认后恢复出厂设置(默认焦点在 **Cancel**):删除所有摄像机槽位、传输方式和设置,应用像刚安装后一样重新启动 | | **Exit** | 退出应用。旁边的小字是应用版本号和构建哈希。 | 已开启的项目(D、Q、S、U 和当前槽位)以橙色填充;遥控器焦点所在的项目带浅蓝色边框。 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 96f5897d..078b167b 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -478,6 +478,41 @@ private boolean leaveUdpView() { @Override public void onExit() { finishAndRemoveTask(); } + /** + * Factory reset (#171): stops every stream, deletes the stored settings, the key of the + * encrypted camera URLs and WebUI's cookies, then starts the app in a new process, so + * nothing kept in memory survives either. + */ + @Override + public void onFactoryReset() { + Log.i(TAG, "Factory reset"); + if (quadEnabled) stopQuad(); + if (rtspClient != null) rtspClient.stop(); + closeDecoder(); + closeAudio(); + // commit(), not apply(): the process ends right after this + getSharedPreferences("settings", MODE_PRIVATE).edit().clear().commit(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + try { + KeystoreCipher.deleteKey(); + } catch (Exception e) { + Log.w(TAG, "Factory reset: cannot delete the URL key: " + e.getMessage()); + } + } + try { + android.webkit.CookieManager.getInstance().removeAllCookies(null); + android.webkit.CookieManager.getInstance().flush(); + } catch (Exception e) { + // No WebView on this device (#15): nothing stored there either + } + Intent restart = getPackageManager().getLaunchIntentForPackage(getPackageName()); + if (restart == null) restart = new Intent(this, Decoder.class); + restart.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(restart); + finishAndRemoveTask(); + Runtime.getRuntime().exit(0); + } + @Override public void onShowSettings() { menuManager.showUrlEditor(mActive, mHosts, mTransports); } diff --git a/android/app/src/main/java/com/openipc/decoder/KeystoreCipher.java b/android/app/src/main/java/com/openipc/decoder/KeystoreCipher.java index 1e15b573..5b2ec444 100644 --- a/android/app/src/main/java/com/openipc/decoder/KeystoreCipher.java +++ b/android/app/src/main/java/com/openipc/decoder/KeystoreCipher.java @@ -53,6 +53,13 @@ class KeystoreCipher implements CredentialStore.Cipherer { if (key == null) throw new IllegalStateException("No key in the Android Keystore"); } + /** Deletes the key, so stored URLs can never be read again (factory reset, #171). */ + static void deleteKey() throws Exception { + KeyStore ks = KeyStore.getInstance(KEYSTORE); + ks.load(null); + if (ks.containsAlias(ALIAS)) ks.deleteEntry(ALIAS); + } + @Override public byte[] encrypt(byte[] plain) throws Exception { Cipher c = Cipher.getInstance(TRANSFORMATION); diff --git a/android/app/src/main/java/com/openipc/decoder/MenuManager.java b/android/app/src/main/java/com/openipc/decoder/MenuManager.java index 6b9f13f0..cec17df5 100644 --- a/android/app/src/main/java/com/openipc/decoder/MenuManager.java +++ b/android/app/src/main/java/com/openipc/decoder/MenuManager.java @@ -81,6 +81,8 @@ interface Listener { /** D: debug text (resolution, codec, fps) in every multi-view cell on or off. */ void onToggleStats(); void onExit(); + /** Reset: every stored setting deleted, the app restarted as on first start (#171). */ + void onFactoryReset(); void onShowSettings(); void onSaveSettings(); void onWebUI(); @@ -295,6 +297,15 @@ public boolean dispatchKeyEvent(KeyEvent event) { layout.addView(divider, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, dp(1))); + // Two deliberate presses: Reset, then Reset again in the confirmation (#171) + TextView reset = createItem("Reset"); + layout.addView(reset); + reset.setOnClickListener(v -> { + PopupWindow p = mMenuPopup; + if (p != null) p.dismiss(); + confirmFactoryReset(); + }); + TextView exit = createItem("Exit"); layout.addView(exit); exit.setText(s); @@ -510,6 +521,27 @@ void showUrlEditor(int mActive, String[] mHosts, Transport.Mode[] mTransports) { input.requestFocus(); } + /** + * Asks before the factory reset (#171). Cancel has the focus, so a stray OK on a remote + * closes the dialog without deleting anything. + */ + private void confirmFactoryReset() { + AlertDialog dialog = new AlertDialog.Builder(activity) + .setTitle("Reset to factory settings?") + .setMessage("All camera slots, transports and settings are deleted, " + + "and the app starts again as after installing.") + .setNegativeButton("Cancel", (d, which) -> d.cancel()) + .setPositiveButton("Reset", (d, which) -> { + if (listener != null) listener.onFactoryReset(); + }) + .create(); + dialog.setOnShowListener(d -> { + View cancel = dialog.getButton(AlertDialog.BUTTON_NEGATIVE); + if (cancel != null) cancel.requestFocus(); + }); + dialog.show(); + } + private void focusChange(View item) { paintItem(item, false); item.setOnFocusChangeListener((v, hasFocus) -> paintItem(v, hasFocus)); From 0b1f3f397cc5049ddf9285b7781e52c8412258b4 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Wed, 14 Oct 2026 19:07:47 +0000 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20friendlier=20wording=20=E2=80=94=20?= =?UTF-8?q?Start=20over,=20then=20Clear=20settings=20with=20what=20it=20do?= =?UTF-8?q?es=20(#171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- README.ru.md | 2 +- README.zh-CN.md | 2 +- .../main/java/com/openipc/decoder/Decoder.java | 6 +++--- .../com/openipc/decoder/KeystoreCipher.java | 2 +- .../java/com/openipc/decoder/MenuManager.java | 18 ++++++++++-------- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index dfcdf460..7bf03c32 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ A beta channel is available on [Google Play](https://play.google.com/apps/intern | **S** | The playback mode: every frame, or key frames only (the picture updates once per key frame interval, MJPEG once a second), the lowest decoder load. Works in multi-view and in the single view and never changes the view | | **Settings** | Edit the URL and the transport of the active slot (single-camera mode only) | | **WebUI** | Open the camera's web interface; credentials come from the RTSP URL (single-camera mode only) | -| **Reset** | Reset to factory settings after a confirmation (**Cancel** has the focus): all camera slots, transports and settings are deleted and the app starts again as after installing | +| **Start over** | Clear all settings after a confirmation (**Cancel** has the focus) to set the app up again from scratch: camera slots, transports and the other settings are removed, and the app opens as right after installing. The cameras are not changed | | **Exit** | Close the app. The small text next to it is the app version and build hash. | Items that are on — D, Q, S, U and the active slot — have an orange fill; the item the remote is on has a light blue frame. diff --git a/README.ru.md b/README.ru.md index c6dab42e..c8783485 100644 --- a/README.ru.md +++ b/README.ru.md @@ -68,7 +68,7 @@ | **S** | Режим воспроизведения: все кадры или только ключевые (картинка обновляется раз в интервал ключевых кадров, MJPEG — раз в секунду), минимальная нагрузка на декодер. Работает и в мультивиде, и в одиночном режиме, вид не меняет | | **Settings** | Изменить адрес и транспорт активного слота (только в режиме одной камеры) | | **WebUI** | Открыть веб-интерфейс камеры; логин и пароль берутся из RTSP-адреса (только в режиме одной камеры) | -| **Reset** | Сброс к заводским настройкам после подтверждения (фокус на **Cancel**): удаляются все слоты камер, транспорты и настройки, приложение запускается заново, как после установки | +| **Start over** | Очистить все настройки после подтверждения (фокус на **Cancel**), чтобы настроить приложение с чистого листа: удаляются слоты камер, транспорты и остальные настройки, приложение открывается как сразу после установки. Сами камеры не меняются | | **Exit** | Закрыть приложение. Мелкий текст рядом — версия приложения и хеш сборки. | Включённые пункты — D, Q, S, U и активный слот — залиты оранжевым; пункт, на котором стоит пульт, обведён голубой рамкой. diff --git a/README.zh-CN.md b/README.zh-CN.md index 637de928..939b19fb 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -68,7 +68,7 @@ | **S** | 播放模式:全部帧或仅关键帧(画面按关键帧间隔更新,MJPEG 每秒一帧),解码器负载最低。多画面和单画面均适用,不切换视图 | | **Settings** | 编辑当前槽位的地址和传输方式(仅单路模式) | | **WebUI** | 打开摄像机网页界面,用户名和密码取自 RTSP 地址(仅单路模式) | -| **Reset** | 确认后恢复出厂设置(默认焦点在 **Cancel**):删除所有摄像机槽位、传输方式和设置,应用像刚安装后一样重新启动 | +| **Start over** | 确认后清除所有设置(默认焦点在 **Cancel**),以便从头重新配置:删除摄像机槽位、传输方式和其他设置,应用像刚安装后一样打开。摄像机本身不受影响 | | **Exit** | 退出应用。旁边的小字是应用版本号和构建哈希。 | 已开启的项目(D、Q、S、U 和当前槽位)以橙色填充;遥控器焦点所在的项目带浅蓝色边框。 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 078b167b..10acf362 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -479,13 +479,13 @@ private boolean leaveUdpView() { public void onExit() { finishAndRemoveTask(); } /** - * Factory reset (#171): stops every stream, deletes the stored settings, the key of the + * Start over (#171): stops every stream, deletes the stored settings, the key of the * encrypted camera URLs and WebUI's cookies, then starts the app in a new process, so * nothing kept in memory survives either. */ @Override public void onFactoryReset() { - Log.i(TAG, "Factory reset"); + Log.i(TAG, "Start over: clearing all settings"); if (quadEnabled) stopQuad(); if (rtspClient != null) rtspClient.stop(); closeDecoder(); @@ -496,7 +496,7 @@ public void onFactoryReset() { try { KeystoreCipher.deleteKey(); } catch (Exception e) { - Log.w(TAG, "Factory reset: cannot delete the URL key: " + e.getMessage()); + Log.w(TAG, "Start over: cannot delete the URL key: " + e.getMessage()); } } try { diff --git a/android/app/src/main/java/com/openipc/decoder/KeystoreCipher.java b/android/app/src/main/java/com/openipc/decoder/KeystoreCipher.java index 5b2ec444..e6e4f486 100644 --- a/android/app/src/main/java/com/openipc/decoder/KeystoreCipher.java +++ b/android/app/src/main/java/com/openipc/decoder/KeystoreCipher.java @@ -53,7 +53,7 @@ class KeystoreCipher implements CredentialStore.Cipherer { if (key == null) throw new IllegalStateException("No key in the Android Keystore"); } - /** Deletes the key, so stored URLs can never be read again (factory reset, #171). */ + /** Deletes the key, so stored URLs can never be read again (Start over, #171). */ static void deleteKey() throws Exception { KeyStore ks = KeyStore.getInstance(KEYSTORE); ks.load(null); diff --git a/android/app/src/main/java/com/openipc/decoder/MenuManager.java b/android/app/src/main/java/com/openipc/decoder/MenuManager.java index cec17df5..87fbec2d 100644 --- a/android/app/src/main/java/com/openipc/decoder/MenuManager.java +++ b/android/app/src/main/java/com/openipc/decoder/MenuManager.java @@ -81,7 +81,7 @@ interface Listener { /** D: debug text (resolution, codec, fps) in every multi-view cell on or off. */ void onToggleStats(); void onExit(); - /** Reset: every stored setting deleted, the app restarted as on first start (#171). */ + /** Start over: every stored setting cleared, the app restarted as on first start (#171). */ void onFactoryReset(); void onShowSettings(); void onSaveSettings(); @@ -297,8 +297,8 @@ public boolean dispatchKeyEvent(KeyEvent event) { layout.addView(divider, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, dp(1))); - // Two deliberate presses: Reset, then Reset again in the confirmation (#171) - TextView reset = createItem("Reset"); + // Two deliberate presses: Start over, then Clear settings in the confirmation (#171) + TextView reset = createItem("Start over"); layout.addView(reset); reset.setOnClickListener(v -> { PopupWindow p = mMenuPopup; @@ -522,16 +522,18 @@ void showUrlEditor(int mActive, String[] mHosts, Transport.Mode[] mTransports) { } /** - * Asks before the factory reset (#171). Cancel has the focus, so a stray OK on a remote + * Asks before clearing the settings (#171). Cancel has the focus, so a stray OK on a remote * closes the dialog without deleting anything. */ private void confirmFactoryReset() { AlertDialog dialog = new AlertDialog.Builder(activity) - .setTitle("Reset to factory settings?") - .setMessage("All camera slots, transports and settings are deleted, " - + "and the app starts again as after installing.") + .setTitle("Start with a clean slate?") + .setMessage("This clears all camera slots, their transports and the other " + + "settings, so you can set the app up again from scratch. The app " + + "restarts and opens as right after installing. Your cameras are not " + + "changed.") .setNegativeButton("Cancel", (d, which) -> d.cancel()) - .setPositiveButton("Reset", (d, which) -> { + .setPositiveButton("Clear settings", (d, which) -> { if (listener != null) listener.onFactoryReset(); }) .create(); From 26d42b7b6e540cc438125cfd4a347990e6ccc20f Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Wed, 14 Oct 2026 19:07:59 +0000 Subject: [PATCH 3/6] refactor: Start over names in the code (#171) --- .../app/src/main/java/com/openipc/decoder/Decoder.java | 2 +- .../src/main/java/com/openipc/decoder/MenuManager.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) 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 10acf362..334f04cb 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -484,7 +484,7 @@ private boolean leaveUdpView() { * nothing kept in memory survives either. */ @Override - public void onFactoryReset() { + public void onStartOver() { Log.i(TAG, "Start over: clearing all settings"); if (quadEnabled) stopQuad(); if (rtspClient != null) rtspClient.stop(); diff --git a/android/app/src/main/java/com/openipc/decoder/MenuManager.java b/android/app/src/main/java/com/openipc/decoder/MenuManager.java index 87fbec2d..9715e524 100644 --- a/android/app/src/main/java/com/openipc/decoder/MenuManager.java +++ b/android/app/src/main/java/com/openipc/decoder/MenuManager.java @@ -82,7 +82,7 @@ interface Listener { void onToggleStats(); void onExit(); /** Start over: every stored setting cleared, the app restarted as on first start (#171). */ - void onFactoryReset(); + void onStartOver(); void onShowSettings(); void onSaveSettings(); void onWebUI(); @@ -303,7 +303,7 @@ public boolean dispatchKeyEvent(KeyEvent event) { reset.setOnClickListener(v -> { PopupWindow p = mMenuPopup; if (p != null) p.dismiss(); - confirmFactoryReset(); + confirmStartOver(); }); TextView exit = createItem("Exit"); @@ -525,7 +525,7 @@ void showUrlEditor(int mActive, String[] mHosts, Transport.Mode[] mTransports) { * Asks before clearing the settings (#171). Cancel has the focus, so a stray OK on a remote * closes the dialog without deleting anything. */ - private void confirmFactoryReset() { + private void confirmStartOver() { AlertDialog dialog = new AlertDialog.Builder(activity) .setTitle("Start with a clean slate?") .setMessage("This clears all camera slots, their transports and the other " @@ -534,7 +534,7 @@ private void confirmFactoryReset() { + "changed.") .setNegativeButton("Cancel", (d, which) -> d.cancel()) .setPositiveButton("Clear settings", (d, which) -> { - if (listener != null) listener.onFactoryReset(); + if (listener != null) listener.onStartOver(); }) .create(); dialog.setOnShowListener(d -> { From 8c8b54081dc32f7a5ce90abd799ca664e33c77bc Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Wed, 14 Oct 2026 19:15:27 +0000 Subject: [PATCH 4/6] feat: menu in two columns, [ Settings | WebUI ] and [ Exit | Reset ]; Reset checks the settings write, waits for the cookies and clears WebUI's HTTP logins (#171, #173, Qodo on #172) --- README.md | 2 +- README.ru.md | 2 +- README.zh-CN.md | 2 +- .../java/com/openipc/decoder/Decoder.java | 42 +++++++++++---- .../com/openipc/decoder/KeystoreCipher.java | 2 +- .../java/com/openipc/decoder/MenuManager.java | 54 ++++++++++++------- 6 files changed, 71 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 7bf03c32..26d60abb 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ A beta channel is available on [Google Play](https://play.google.com/apps/intern | **S** | The playback mode: every frame, or key frames only (the picture updates once per key frame interval, MJPEG once a second), the lowest decoder load. Works in multi-view and in the single view and never changes the view | | **Settings** | Edit the URL and the transport of the active slot (single-camera mode only) | | **WebUI** | Open the camera's web interface; credentials come from the RTSP URL (single-camera mode only) | -| **Start over** | Clear all settings after a confirmation (**Cancel** has the focus) to set the app up again from scratch: camera slots, transports and the other settings are removed, and the app opens as right after installing. The cameras are not changed | +| **Reset** | Clear all settings after a confirmation (**Cancel** has the focus) to set the app up again from scratch: camera slots, transports and the other settings are removed, and the app opens as right after installing. The cameras are not changed | | **Exit** | Close the app. The small text next to it is the app version and build hash. | Items that are on — D, Q, S, U and the active slot — have an orange fill; the item the remote is on has a light blue frame. diff --git a/README.ru.md b/README.ru.md index c8783485..8d62df0a 100644 --- a/README.ru.md +++ b/README.ru.md @@ -68,7 +68,7 @@ | **S** | Режим воспроизведения: все кадры или только ключевые (картинка обновляется раз в интервал ключевых кадров, MJPEG — раз в секунду), минимальная нагрузка на декодер. Работает и в мультивиде, и в одиночном режиме, вид не меняет | | **Settings** | Изменить адрес и транспорт активного слота (только в режиме одной камеры) | | **WebUI** | Открыть веб-интерфейс камеры; логин и пароль берутся из RTSP-адреса (только в режиме одной камеры) | -| **Start over** | Очистить все настройки после подтверждения (фокус на **Cancel**), чтобы настроить приложение с чистого листа: удаляются слоты камер, транспорты и остальные настройки, приложение открывается как сразу после установки. Сами камеры не меняются | +| **Reset** | Очистить все настройки после подтверждения (фокус на **Cancel**), чтобы настроить приложение с чистого листа: удаляются слоты камер, транспорты и остальные настройки, приложение открывается как сразу после установки. Сами камеры не меняются | | **Exit** | Закрыть приложение. Мелкий текст рядом — версия приложения и хеш сборки. | Включённые пункты — D, Q, S, U и активный слот — залиты оранжевым; пункт, на котором стоит пульт, обведён голубой рамкой. diff --git a/README.zh-CN.md b/README.zh-CN.md index 939b19fb..82700195 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -68,7 +68,7 @@ | **S** | 播放模式:全部帧或仅关键帧(画面按关键帧间隔更新,MJPEG 每秒一帧),解码器负载最低。多画面和单画面均适用,不切换视图 | | **Settings** | 编辑当前槽位的地址和传输方式(仅单路模式) | | **WebUI** | 打开摄像机网页界面,用户名和密码取自 RTSP 地址(仅单路模式) | -| **Start over** | 确认后清除所有设置(默认焦点在 **Cancel**),以便从头重新配置:删除摄像机槽位、传输方式和其他设置,应用像刚安装后一样打开。摄像机本身不受影响 | +| **Reset** | 确认后清除所有设置(默认焦点在 **Cancel**),以便从头重新配置:删除摄像机槽位、传输方式和其他设置,应用像刚安装后一样打开。摄像机本身不受影响 | | **Exit** | 退出应用。旁边的小字是应用版本号和构建哈希。 | 已开启的项目(D、Q、S、U 和当前槽位)以橙色填充;遥控器焦点所在的项目带浅蓝色边框。 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 334f04cb..5fd4e9e0 100755 --- a/android/app/src/main/java/com/openipc/decoder/Decoder.java +++ b/android/app/src/main/java/com/openipc/decoder/Decoder.java @@ -479,38 +479,58 @@ private boolean leaveUdpView() { public void onExit() { finishAndRemoveTask(); } /** - * Start over (#171): stops every stream, deletes the stored settings, the key of the - * encrypted camera URLs and WebUI's cookies, then starts the app in a new process, so - * nothing kept in memory survives either. + * Reset (#171): stops every stream, deletes the stored settings, the key of the encrypted + * camera URLs and what WebUI kept (cookies, HTTP logins), then starts the app in a new + * process, so nothing kept in memory survives either. */ @Override - public void onStartOver() { - Log.i(TAG, "Start over: clearing all settings"); + public void onReset() { + Log.i(TAG, "Reset: clearing all settings"); + // commit(), not apply(): the process ends soon after. If the write fails, nothing + // else is touched and the app stays as it was, to try again (Qodo on #172) + if (!getSharedPreferences("settings", MODE_PRIVATE).edit().clear().commit()) { + Log.w(TAG, "Reset: the settings could not be cleared"); + Toast.makeText(this, "Could not clear the settings, please try again", + Toast.LENGTH_LONG).show(); + return; + } if (quadEnabled) stopQuad(); if (rtspClient != null) rtspClient.stop(); closeDecoder(); closeAudio(); - // commit(), not apply(): the process ends right after this - getSharedPreferences("settings", MODE_PRIVATE).edit().clear().commit(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { try { KeystoreCipher.deleteKey(); } catch (Exception e) { - Log.w(TAG, "Start over: cannot delete the URL key: " + e.getMessage()); + // The URLs it encrypted are gone already; the next save reuses the key + Log.w(TAG, "Reset: cannot delete the URL key: " + e.getMessage()); } } try { - android.webkit.CookieManager.getInstance().removeAllCookies(null); - android.webkit.CookieManager.getInstance().flush(); + android.webkit.WebViewDatabase.getInstance(this).clearHttpAuthUsernamePassword(); + android.webkit.CookieManager cookies = android.webkit.CookieManager.getInstance(); + // Restarted only once the cookies are really gone (Qodo on #172) + cookies.removeAllCookies(removed -> { + cookies.flush(); + restartClean(); + }); } catch (Exception e) { // No WebView on this device (#15): nothing stored there either + restartClean(); } + } + + /** Starts the app again in a new process after a reset (#171). */ + private void restartClean() { Intent restart = getPackageManager().getLaunchIntentForPackage(getPackageName()); if (restart == null) restart = new Intent(this, Decoder.class); restart.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(restart); finishAndRemoveTask(); - Runtime.getRuntime().exit(0); + // The system has the new task once startActivity() returns; it starts a process for + // it after this one ends. A short delay lets the launch settle first + new Handler(android.os.Looper.getMainLooper()).postDelayed( + () -> Runtime.getRuntime().exit(0), 300); } @Override diff --git a/android/app/src/main/java/com/openipc/decoder/KeystoreCipher.java b/android/app/src/main/java/com/openipc/decoder/KeystoreCipher.java index e6e4f486..e8080186 100644 --- a/android/app/src/main/java/com/openipc/decoder/KeystoreCipher.java +++ b/android/app/src/main/java/com/openipc/decoder/KeystoreCipher.java @@ -53,7 +53,7 @@ class KeystoreCipher implements CredentialStore.Cipherer { if (key == null) throw new IllegalStateException("No key in the Android Keystore"); } - /** Deletes the key, so stored URLs can never be read again (Start over, #171). */ + /** Deletes the key, so stored URLs can never be read again (Reset, #171). */ static void deleteKey() throws Exception { KeyStore ks = KeyStore.getInstance(KEYSTORE); ks.load(null); diff --git a/android/app/src/main/java/com/openipc/decoder/MenuManager.java b/android/app/src/main/java/com/openipc/decoder/MenuManager.java index 9715e524..daf48390 100644 --- a/android/app/src/main/java/com/openipc/decoder/MenuManager.java +++ b/android/app/src/main/java/com/openipc/decoder/MenuManager.java @@ -66,6 +66,8 @@ class MenuManager { /** The item a remote starts from: the active slot, Q or U (#91). */ private TextView mFocusItem; private TextView mSettingsBtn; + /** Settings | WebUI, shown in the single view only (#173). */ + private LinearLayout mSettingsRow; private TextView mWebUiBtn; TextView[] camButtons; private Dialog mBrowserDialog; @@ -81,8 +83,8 @@ interface Listener { /** D: debug text (resolution, codec, fps) in every multi-view cell on or off. */ void onToggleStats(); void onExit(); - /** Start over: every stored setting cleared, the app restarted as on first start (#171). */ - void onStartOver(); + /** Reset: every stored setting cleared, the app restarted as on first start (#171). */ + void onReset(); void onShowSettings(); void onSaveSettings(); void onWebUI(); @@ -193,16 +195,17 @@ public boolean dispatchKeyEvent(KeyEvent event) { LinearLayout.LayoutParams.WRAP_CONTENT)); mSettingsBtn = createItem("Settings"); - layout.addView(mSettingsBtn); - mSettingsBtn.setVisibility(single ? View.VISIBLE : View.GONE); final TextView quadBtn = createItem("Q"); quadBtn.setGravity(Gravity.CENTER); quadBtn.setPadding(dp(12), dp(8), dp(12), dp(8)); mWebUiBtn = createItem("WebUI"); - layout.addView(mWebUiBtn); - mWebUiBtn.setVisibility(single ? View.VISIBLE : View.GONE); + // Two items per row keep the menu short (#173); the row is for the single view only + mSettingsRow = pairRow(mSettingsBtn, mWebUiBtn); + layout.addView(mSettingsRow, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + mSettingsRow.setVisibility(single ? View.VISIBLE : View.GONE); camButtons = new TextView[CAM_COUNT]; mCachedCamButtons = new TextView[CAM_COUNT]; @@ -297,18 +300,18 @@ public boolean dispatchKeyEvent(KeyEvent event) { layout.addView(divider, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, dp(1))); - // Two deliberate presses: Start over, then Clear settings in the confirmation (#171) - TextView reset = createItem("Start over"); - layout.addView(reset); + // Two deliberate presses: Reset, then Clear settings in the confirmation (#171) + TextView reset = createItem("Reset"); reset.setOnClickListener(v -> { PopupWindow p = mMenuPopup; if (p != null) p.dismiss(); - confirmStartOver(); + confirmReset(); }); TextView exit = createItem("Exit"); - layout.addView(exit); exit.setText(s); + layout.addView(pairRow(exit, reset), new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); exit.setOnClickListener(v -> { if (listener != null) listener.onExit(); }); @@ -333,11 +336,8 @@ public boolean dispatchKeyEvent(KeyEvent event) { private void updateCachedMenu(boolean quadEnabled, int mActive, String[] mHosts) { boolean single = !quadEnabled && !udpShown; - if (mSettingsBtn != null) { - mSettingsBtn.setVisibility(single ? View.VISIBLE : View.GONE); - } - if (mWebUiBtn != null) { - mWebUiBtn.setVisibility(single ? View.VISIBLE : View.GONE); + if (mSettingsRow != null) { + mSettingsRow.setVisibility(single ? View.VISIBLE : View.GONE); } if (mCachedQuadBtn != null) { if (quadEnabled) highlightItem(mCachedQuadBtn); @@ -404,6 +404,24 @@ static boolean isNavigationKey(int keyCode) { } } + /** + * Two menu items side by side, split by a thin line (#173): the left one takes the free + * width, the right one its own. Left / Right move between them with a remote. + */ + private LinearLayout pairRow(TextView left, TextView right) { + LinearLayout row = new LinearLayout(activity); + row.setOrientation(LinearLayout.HORIZONTAL); + row.addView(left, new LinearLayout.LayoutParams(0, + LinearLayout.LayoutParams.WRAP_CONTENT, 1)); + View line = new View(activity); + line.setBackgroundColor(Color.DKGRAY); + row.addView(line, new LinearLayout.LayoutParams(dp(1), + LinearLayout.LayoutParams.MATCH_PARENT)); + row.addView(right, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + return row; + } + private TextView createItem(String title) { TextView text = new TextView(activity); text.setText(title); @@ -525,7 +543,7 @@ void showUrlEditor(int mActive, String[] mHosts, Transport.Mode[] mTransports) { * Asks before clearing the settings (#171). Cancel has the focus, so a stray OK on a remote * closes the dialog without deleting anything. */ - private void confirmStartOver() { + private void confirmReset() { AlertDialog dialog = new AlertDialog.Builder(activity) .setTitle("Start with a clean slate?") .setMessage("This clears all camera slots, their transports and the other " @@ -534,7 +552,7 @@ private void confirmStartOver() { + "changed.") .setNegativeButton("Cancel", (d, which) -> d.cancel()) .setPositiveButton("Clear settings", (d, which) -> { - if (listener != null) listener.onStartOver(); + if (listener != null) listener.onReset(); }) .create(); dialog.setOnShowListener(d -> { From 5b0e994662409fa01447f97c1ee1f2437bab8579 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Wed, 14 Oct 2026 19:21:06 +0000 Subject: [PATCH 5/6] fix: menu rows split in the middle (#173); Reset restarts the app from a helper process, so the new launch is not ended with the old process (Qodo on #172) --- android/app/src/main/AndroidManifest.xml | 4 ++ .../java/com/openipc/decoder/Decoder.java | 13 ++----- .../java/com/openipc/decoder/MenuManager.java | 10 +++-- .../java/com/openipc/decoder/Restart.java | 37 +++++++++++++++++++ 4 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 android/app/src/main/java/com/openipc/decoder/Restart.java diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 33e1180b..922a4fe1 100755 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -27,6 +27,10 @@ tools:targetApi="33"> + + Runtime.getRuntime().exit(0), 300); } @Override diff --git a/android/app/src/main/java/com/openipc/decoder/MenuManager.java b/android/app/src/main/java/com/openipc/decoder/MenuManager.java index daf48390..830bb8d9 100644 --- a/android/app/src/main/java/com/openipc/decoder/MenuManager.java +++ b/android/app/src/main/java/com/openipc/decoder/MenuManager.java @@ -405,8 +405,8 @@ static boolean isNavigationKey(int keyCode) { } /** - * Two menu items side by side, split by a thin line (#173): the left one takes the free - * width, the right one its own. Left / Right move between them with a remote. + * Two menu items side by side, split by a thin line exactly in the middle (#173): both get + * half the width. Left / Right move between them with a remote. */ private LinearLayout pairRow(TextView left, TextView right) { LinearLayout row = new LinearLayout(activity); @@ -417,8 +417,10 @@ private LinearLayout pairRow(TextView left, TextView right) { line.setBackgroundColor(Color.DKGRAY); row.addView(line, new LinearLayout.LayoutParams(dp(1), LinearLayout.LayoutParams.MATCH_PARENT)); - row.addView(right, new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + row.addView(right, new LinearLayout.LayoutParams(0, + LinearLayout.LayoutParams.WRAP_CONTENT, 1)); + left.setSingleLine(true); + right.setSingleLine(true); return row; } diff --git a/android/app/src/main/java/com/openipc/decoder/Restart.java b/android/app/src/main/java/com/openipc/decoder/Restart.java new file mode 100644 index 00000000..3ff28b9b --- /dev/null +++ b/android/app/src/main/java/com/openipc/decoder/Restart.java @@ -0,0 +1,37 @@ +/* + * + * Copyright (c) OpenIPC https://openipc.org Prosperity Public License 3.0.0 + * + * Restart.java — starts the app again in a new process after Reset (#171) + * + */ + +package com.openipc.decoder; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Process; + +/** + * Runs in a process of its own (":restart"), like {@link Crash}: it ends the main process, + * then opens the app, which therefore starts in a fresh process with nothing kept in memory. + * Ending the main process from inside it right after startActivity() could take the new + * activity down with it (Qodo on #172, seen on Android 15). + */ +public class Restart extends Activity { + static final String EXTRA_PID = "pid"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + int pid = getIntent() != null ? getIntent().getIntExtra(EXTRA_PID, -1) : -1; + if (pid > 0 && pid != Process.myPid()) Process.killProcess(pid); + Intent open = new Intent(this, Decoder.class); + open.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(open); + finish(); + // This helper process is not needed any more + Runtime.getRuntime().exit(0); + } +} From 2d96037027cd543a99fc7e2462962666022eed81 Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Wed, 14 Oct 2026 19:21:06 +0000 Subject: [PATCH 6/6] chore: version 2.05 --- android/app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index e3fc69f7..00cccadb 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 29 - versionName "2.04" + versionCode 30 + versionName "2.05" buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\"" }