mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-17 07:34:15 -06:00
feat(android): hidden settings menu with multi-tap BACK/ESC detection
Add an in-app settings menu reachable via 2× BACK (or ESC) taps, with a 1.8s window — Android TV and touch devices. - 2 taps: settings dialog (change server, re-pair, permissions, exit) - 3 taps: exit dialog directly (skip menu) - Auto-banner after 10+ consecutive connection failures Settings options: - Change server URL (pre-fills ProvisioningActivity) - Reconfigure device (clear credentials → re-pair) - Permissions (Accessibility + Notifications status → system settings) - Device info (ID, APK version, connection status) - Exit app (finishAffinity) Also adds EXTRA_SERVER_URL to ProvisioningActivity and a consecutiveFailures counter to WebSocketService.
This commit is contained in:
parent
be01674d35
commit
69be6e804e
|
|
@ -1,12 +1,15 @@
|
||||||
package com.remotedisplay.player
|
package com.remotedisplay.player
|
||||||
|
|
||||||
import android.accessibilityservice.AccessibilityServiceInfo
|
import android.accessibilityservice.AccessibilityServiceInfo
|
||||||
|
import android.Manifest
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.EditText
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
|
@ -19,7 +22,9 @@ import android.view.WindowManager
|
||||||
import android.view.accessibility.AccessibilityManager
|
import android.view.accessibility.AccessibilityManager
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import com.remotedisplay.player.data.ContentCache
|
import com.remotedisplay.player.data.ContentCache
|
||||||
import com.remotedisplay.player.data.ServerConfig
|
import com.remotedisplay.player.data.ServerConfig
|
||||||
|
|
@ -66,6 +71,14 @@ class MainActivity : AppCompatActivity() {
|
||||||
private var screenshotStreamRunnable: Runnable? = null
|
private var screenshotStreamRunnable: Runnable? = null
|
||||||
private var playbackStarted = false
|
private var playbackStarted = false
|
||||||
|
|
||||||
|
// Multi-tap BACK/ESC for hidden settings menu.
|
||||||
|
// Collect taps in a 2-second window; on expiry: 2 taps → settings, 3+ taps → exit.
|
||||||
|
private val backTapTimes = mutableListOf<Long>()
|
||||||
|
private var backTapRunnable: Runnable? = null
|
||||||
|
private val TAP_WINDOW_MS = 1800L
|
||||||
|
// Connection-failure auto-prompt threshold.
|
||||||
|
private var failureBannerShown = false
|
||||||
|
|
||||||
private val connection = object : ServiceConnection {
|
private val connection = object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
val binder = service as WebSocketService.LocalBinder
|
val binder = service as WebSocketService.LocalBinder
|
||||||
|
|
@ -248,6 +261,16 @@ class MainActivity : AppCompatActivity() {
|
||||||
updateChecker.otaStatusReporter = { wsService?.sendOtaStatus() }
|
updateChecker.otaStatusReporter = { wsService?.sendOtaStatus() }
|
||||||
updateChecker.startPeriodicCheck()
|
updateChecker.startPeriodicCheck()
|
||||||
|
|
||||||
|
// Periodic connection-failure check so the "Stuck connecting?" banner appears
|
||||||
|
// without waiting for the next playlist update
|
||||||
|
val failureCheck = object : Runnable {
|
||||||
|
override fun run() {
|
||||||
|
checkConnectionFailureBanner()
|
||||||
|
handler.postDelayed(this, 30_000L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.postDelayed(failureCheck, 15_000L)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rotate the whole stage in software so portrait / flipped signage works even on
|
// Rotate the whole stage in software so portrait / flipped signage works even on
|
||||||
|
|
@ -783,10 +806,186 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
// Don't exit the app on back press - this is a kiosk/signage app
|
// Don't exit the app on back press - this is a kiosk/signage app.
|
||||||
|
// Multi-tap detection is handled in dispatchKeyEvent.
|
||||||
Log.i("MainActivity", "Back press intercepted (kiosk mode)")
|
Log.i("MainActivity", "Back press intercepted (kiosk mode)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multi-tap BACK/ESC detection — 2 taps → settings, 3+ taps → exit dialog.
|
||||||
|
// Catches hardware BACK, D-pad BACK (KEYCODE_BACK=4), and ESC (111).
|
||||||
|
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||||
|
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||||
|
when (event.keyCode) {
|
||||||
|
KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_ESCAPE -> {
|
||||||
|
handleBackTap()
|
||||||
|
return true // consume — never let the system handle it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.dispatchKeyEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleBackTap() {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
backTapTimes.add(now)
|
||||||
|
|
||||||
|
// Trim taps older than the window
|
||||||
|
while (backTapTimes.isNotEmpty() && now - backTapTimes.first() > TAP_WINDOW_MS) {
|
||||||
|
backTapTimes.removeAt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any pending evaluation and re-schedule
|
||||||
|
backTapRunnable?.let { handler.removeCallbacks(it) }
|
||||||
|
backTapRunnable = Runnable {
|
||||||
|
val count = backTapTimes.size
|
||||||
|
backTapTimes.clear()
|
||||||
|
when {
|
||||||
|
count >= 3 -> showExitDialog()
|
||||||
|
count == 2 -> showSettingsDialog()
|
||||||
|
// count == 1 → ignored (kiosk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.postDelayed(backTapRunnable!!, TAP_WINDOW_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSettingsDialog() {
|
||||||
|
val serverUrl = config.serverUrl
|
||||||
|
val connected = wsService?.isConnected() == true
|
||||||
|
val version = try {
|
||||||
|
packageManager.getPackageInfo(packageName, 0).versionName ?: "?"
|
||||||
|
} catch (_: Exception) { "?" }
|
||||||
|
|
||||||
|
val items = arrayOf(
|
||||||
|
"${getString(R.string.settings_change_server)}\n ${if (serverUrl.isEmpty()) "—" else serverUrl}",
|
||||||
|
getString(R.string.settings_reconfigure),
|
||||||
|
getString(R.string.settings_permissions),
|
||||||
|
"${getString(R.string.settings_device_info)}\n ${getString(R.string.settings_info_device)}: ${config.deviceId.take(8)}… | v$version | ${if (connected) "●" else "○"}",
|
||||||
|
getString(R.string.settings_exit)
|
||||||
|
)
|
||||||
|
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(getString(R.string.settings_title))
|
||||||
|
.setItems(items) { _, which ->
|
||||||
|
when (which) {
|
||||||
|
0 -> showChangeServerDialog(serverUrl)
|
||||||
|
1 -> {
|
||||||
|
config.clearDeviceCredentials()
|
||||||
|
navigateToProvisioning(serverUrl)
|
||||||
|
}
|
||||||
|
2 -> showPermissionsDialog()
|
||||||
|
4 -> showExitDialog()
|
||||||
|
// 3 = info (read-only, dismiss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setOnCancelListener { /* dismissed, back to kiosk */ }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showChangeServerDialog(currentUrl: String) {
|
||||||
|
val input = EditText(this).apply {
|
||||||
|
setText(currentUrl)
|
||||||
|
inputType = android.text.InputType.TYPE_TEXT_VARIATION_URI
|
||||||
|
hint = "https://screentinker.com"
|
||||||
|
setSingleLine()
|
||||||
|
}
|
||||||
|
val container = FrameLayout(this).apply {
|
||||||
|
val pad = (16 * resources.displayMetrics.density).toInt()
|
||||||
|
setPadding(pad, pad, pad, 0)
|
||||||
|
addView(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(getString(R.string.settings_change_server))
|
||||||
|
.setView(container)
|
||||||
|
.setPositiveButton(getString(R.string.settings_save)) { _, _ ->
|
||||||
|
val url = input.text.toString().trim().trimEnd('/')
|
||||||
|
if (url.isNotEmpty() && url != currentUrl) {
|
||||||
|
config.clearDeviceCredentials()
|
||||||
|
navigateToProvisioning(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showPermissionsDialog() {
|
||||||
|
val accEnabled = isAccessibilityEnabled()
|
||||||
|
val notifyGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
|
||||||
|
} else true
|
||||||
|
|
||||||
|
val lines = buildString {
|
||||||
|
appendLine("${getString(R.string.settings_perm_accessibility)}: ${if (accEnabled) "✓" else "✗"}")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
appendLine("${getString(R.string.settings_perm_notifications)}: ${if (notifyGranted) "✓" else "✗"}")
|
||||||
|
}
|
||||||
|
appendLine("")
|
||||||
|
appendLine(getString(R.string.settings_perm_hint))
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(getString(R.string.settings_permissions))
|
||||||
|
.setMessage(lines)
|
||||||
|
.setPositiveButton(getString(R.string.settings_perm_open)) { _, _ ->
|
||||||
|
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = android.net.Uri.parse("package:$packageName")
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showExitDialog() {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(getString(R.string.settings_exit_title))
|
||||||
|
.setMessage(getString(R.string.settings_exit_confirm))
|
||||||
|
.setPositiveButton(getString(R.string.settings_exit_yes)) { _, _ ->
|
||||||
|
try {
|
||||||
|
wsService?.disconnect()
|
||||||
|
if (bound) { unbindService(connection); bound = false }
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
finishAffinity()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(getString(R.string.settings_exit_title))
|
||||||
|
.setMessage(getString(R.string.settings_exit_confirm))
|
||||||
|
.setPositiveButton(getString(R.string.settings_exit_yes)) { _, _ ->
|
||||||
|
try {
|
||||||
|
wsService?.disconnect()
|
||||||
|
if (bound) { unbindService(connection); bound = false }
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
finishAffinity()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun navigateToProvisioning(url: String? = null) {
|
||||||
|
try { wsService?.disconnect() } catch (_: Exception) {}
|
||||||
|
if (bound) { try { unbindService(connection) } catch (_: Exception) {}; bound = false }
|
||||||
|
val intent = Intent(this, ProvisioningActivity::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
url?.let { putExtra("EXTRA_SERVER_URL", it) }
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkConnectionFailureBanner() {
|
||||||
|
val failures = wsService?.consecutiveFailures ?: 0
|
||||||
|
if (failures > 10 && !failureBannerShown && wsService?.isConnected() != true) {
|
||||||
|
failureBannerShown = true
|
||||||
|
showStatus("${getString(R.string.settings_connection_failed)}\n${getString(R.string.settings_connection_hint)}")
|
||||||
|
}
|
||||||
|
if (failures == 0) {
|
||||||
|
failureBannerShown = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun isAccessibilityEnabled(): Boolean {
|
private fun isAccessibilityEnabled(): Boolean {
|
||||||
val am = getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
val am = getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
||||||
val myComponent = ComponentName(this, com.remotedisplay.player.service.PowerAccessibilityService::class.java)
|
val myComponent = ComponentName(this, com.remotedisplay.player.service.PowerAccessibilityService::class.java)
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,12 @@ class ProvisioningActivity : AppCompatActivity() {
|
||||||
pairingSection = findViewById(R.id.pairingSection)
|
pairingSection = findViewById(R.id.pairingSection)
|
||||||
serverSection = findViewById(R.id.serverSection)
|
serverSection = findViewById(R.id.serverSection)
|
||||||
|
|
||||||
// Pre-fill if previously entered
|
// Pre-fill if previously entered, OR if an external caller passed a URL
|
||||||
if (config.serverUrl.isNotEmpty()) {
|
// (e.g. MainActivity settings → "Change server").
|
||||||
|
val passedUrl = intent.getStringExtra("EXTRA_SERVER_URL")?.trimEnd('/')
|
||||||
|
if (!passedUrl.isNullOrEmpty()) {
|
||||||
|
serverUrlInput.setText(passedUrl)
|
||||||
|
} else if (config.serverUrl.isNotEmpty()) {
|
||||||
serverUrlInput.setText(config.serverUrl)
|
serverUrlInput.setText(config.serverUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,8 @@ class WebSocketService : Service() {
|
||||||
fun connect(serverUrl: String? = null) {
|
fun connect(serverUrl: String? = null) {
|
||||||
val url = serverUrl ?: config.serverUrl
|
val url = serverUrl ?: config.serverUrl
|
||||||
if (url.isEmpty()) {
|
if (url.isEmpty()) {
|
||||||
Log.e("WebSocketService", "No server URL configured")
|
consecutiveFailures++
|
||||||
|
Log.e("WebSocketService", "No server URL configured (${consecutiveFailures} consecutive)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!ConnectionGuard.shouldOpenNewSocket(socket != null, currentUrl == url, socketActive)) {
|
if (!ConnectionGuard.shouldOpenNewSocket(socket != null, currentUrl == url, socketActive)) {
|
||||||
|
|
@ -153,6 +154,7 @@ class WebSocketService : Service() {
|
||||||
socket = IO.socket(URI.create("$url/device"), options).apply {
|
socket = IO.socket(URI.create("$url/device"), options).apply {
|
||||||
safeOn(Socket.EVENT_CONNECT) {
|
safeOn(Socket.EVENT_CONNECT) {
|
||||||
Log.i("WebSocketService", "Connected to server")
|
Log.i("WebSocketService", "Connected to server")
|
||||||
|
consecutiveFailures = 0
|
||||||
register()
|
register()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,7 +175,8 @@ class WebSocketService : Service() {
|
||||||
}
|
}
|
||||||
|
|
||||||
safeOn(Socket.EVENT_CONNECT_ERROR) { args ->
|
safeOn(Socket.EVENT_CONNECT_ERROR) { args ->
|
||||||
Log.e("WebSocketService", "Connection error: ${args.firstOrNull()}")
|
consecutiveFailures++
|
||||||
|
Log.e("WebSocketService", "Connection error (${consecutiveFailures} consecutive): ${args.firstOrNull()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
safeOn("device:registered") { args ->
|
safeOn("device:registered") { args ->
|
||||||
|
|
@ -679,6 +682,11 @@ class WebSocketService : Service() {
|
||||||
|
|
||||||
fun isConnected(): Boolean = socket?.connected() == true
|
fun isConnected(): Boolean = socket?.connected() == true
|
||||||
|
|
||||||
|
// Consecutive connection failures (reset on any successful connect).
|
||||||
|
// Used by MainActivity to surface a "Stuck connecting?" prompt.
|
||||||
|
@Volatile var consecutiveFailures: Int = 0
|
||||||
|
private set
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
wakeLock?.let { if (it.isHeld) it.release() }
|
wakeLock?.let { if (it.isHeld) it.release() }
|
||||||
disconnect()
|
disconnect()
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,25 @@
|
||||||
<string name="accessibility_description">RemoteDisplay uses accessibility to enable remote power controls and system navigation.</string>
|
<string name="accessibility_description">RemoteDisplay uses accessibility to enable remote power controls and system navigation.</string>
|
||||||
<string name="nothing_scheduled">Nothing scheduled right now</string>
|
<string name="nothing_scheduled">Nothing scheduled right now</string>
|
||||||
<string name="waiting_for_content">Waiting for content…</string>
|
<string name="waiting_for_content">Waiting for content…</string>
|
||||||
|
|
||||||
|
<!-- Hidden settings menu (triggered by 2x BACK or ESC) -->
|
||||||
|
<string name="settings_title">Settings</string>
|
||||||
|
<string name="settings_change_server">Change server URL</string>
|
||||||
|
<string name="settings_reconfigure">Reconfigure device (re-pair)</string>
|
||||||
|
<string name="settings_device_info">Device info</string>
|
||||||
|
<string name="settings_info_device">Device</string>
|
||||||
|
<string name="settings_exit">Exit app</string>
|
||||||
|
<string name="settings_save">Save</string>
|
||||||
|
<string name="settings_exit_title">Exit ScreenTinker?</string>
|
||||||
|
<string name="settings_exit_confirm">The app will stop displaying content. You can reopen it later from the app launcher.</string>
|
||||||
|
<string name="settings_exit_yes">Exit</string>
|
||||||
|
<string name="settings_connection_failed">Can\'t reach the server</string>
|
||||||
|
<string name="settings_connection_hint">Press BACK twice for settings</string>
|
||||||
|
|
||||||
|
<!-- Permissions -->
|
||||||
|
<string name="settings_permissions">Permissions</string>
|
||||||
|
<string name="settings_perm_accessibility">Accessibility</string>
|
||||||
|
<string name="settings_perm_notifications">Notifications</string>
|
||||||
|
<string name="settings_perm_hint">Tap \"Open\" to manage permissions in system settings</string>
|
||||||
|
<string name="settings_perm_open">Open settings</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,22 @@ If the target server is up and on the **same LAN** as the device, the player
|
||||||
|
|
||||||
## Fix: re-point the player to a different server
|
## Fix: re-point the player to a different server
|
||||||
|
|
||||||
The app only shows its **setup screen** when it is *not provisioned/paired*
|
Three ways to reconfigure the server URL, from easiest to most involved:
|
||||||
(`MainActivity`: `if (!config.isProvisioned || !config.isPaired) -> ProvisioningActivity`).
|
|
||||||
So to change servers you must reset that state. Two ways:
|
|
||||||
|
|
||||||
### A. On the phone, no tools (most reliable)
|
### A. In-app settings (APK v1.9.2+) — RECOMMENDED
|
||||||
|
1. **Press BACK (or ESC) twice quickly** on the device/remote — a Settings dialog opens.
|
||||||
|
2. Options available:
|
||||||
|
- **Change server URL** — enter new URL, clears pairing, returns to provisioning
|
||||||
|
- **Reconfigure device** — clears credentials, returns to provisioning screen
|
||||||
|
- **Permissions** — check Accessibility/Notification status, open system settings
|
||||||
|
- **Device info** — device ID, APK version, connection status
|
||||||
|
- **Exit app** — close the kiosk app (3× BACK also opens exit directly)
|
||||||
|
3. After changing server/reconfiguring, enter the pairing code from the dashboard to reconnect.
|
||||||
|
|
||||||
|
If the app is stuck on "Connecting to server…" for more than a minute, it will
|
||||||
|
show a banner: **"Can't reach the server — Press BACK twice for settings."**
|
||||||
|
|
||||||
|
### B. On the phone, no tools (most reliable)
|
||||||
1. **Settings → Apps → RemoteDisplay → Storage → Clear data.**
|
1. **Settings → Apps → RemoteDisplay → Storage → Clear data.**
|
||||||
This wipes the stale server URL and pairing. (Cached content is cleared too;
|
This wipes the stale server URL and pairing. (Cached content is cleared too;
|
||||||
it re-downloads after pairing — no harm.)
|
it re-downloads after pairing — no harm.)
|
||||||
|
|
@ -67,7 +78,7 @@ So to change servers you must reset that state. Two ways:
|
||||||
> remote power/navigation is also reset. Re-enable it if you need remote
|
> remote power/navigation is also reset. Re-enable it if you need remote
|
||||||
> reboot/screen control: Settings → Accessibility → RemoteDisplay → On.
|
> reboot/screen control: Settings → Accessibility → RemoteDisplay → On.
|
||||||
|
|
||||||
### B. Via adb (if you have a working connection)
|
### C. Via adb (if you have a working connection)
|
||||||
```bash
|
```bash
|
||||||
D=<ip:port>
|
D=<ip:port>
|
||||||
# Option 1: reset provisioning the same way "Clear data" does
|
# Option 1: reset provisioning the same way "Clear data" does
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue