1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 06:39:09 +03:00

test adb reconnect persistence

This commit is contained in:
Anton Afanasyeu
2026-06-11 14:55:46 +02:00
parent 2c3accf4e8
commit 0431bafd74
16 changed files with 1178 additions and 77 deletions

10
.cursorignore Normal file
View File

@@ -0,0 +1,10 @@
tmp/
third-party/
**/.gradle/
**/build/
**/.cxx/
**/node_modules/
out/
ota-publish/
*.apk
*.jar

1
.gitignore vendored
View File

@@ -59,6 +59,7 @@ config.php.alpine
# --- CI / OTA / local artifacts --- # --- CI / OTA / local artifacts ---
out/ out/
ota-publish/ ota-publish/
.adb_connect_cache
# --- Scratch (not for commit) --- # --- Scratch (not for commit) ---
tmp/ tmp/

View File

@@ -19,6 +19,10 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.DUMP"
tools:ignore="ProtectedPermissions" />
<application <application
android:name=".AndroidCastApplication" android:name=".AndroidCastApplication"
@@ -165,5 +169,13 @@
<receiver <receiver
android:name=".TrayNotificationReceiver" android:name=".TrayNotificationReceiver"
android:exported="false" /> android:exported="false" />
<receiver
android:name=".dev.DevAdbWifiReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.foxx.androidcast.dev.REFRESH_ADB_WIFI" />
</intent-filter>
</receiver>
</application> </application>
</manifest> </manifest>

View File

@@ -16,6 +16,7 @@ import android.content.Context;
import com.foxx.androidcast.commercial.CommercialFeatures; import com.foxx.androidcast.commercial.CommercialFeatures;
import com.foxx.androidcast.crash.CrashReporter; import com.foxx.androidcast.crash.CrashReporter;
import com.foxx.androidcast.dev.DevAdbWifiKeeper;
import com.foxx.androidcast.display.WiredDisplayMonitor; import com.foxx.androidcast.display.WiredDisplayMonitor;
import com.foxx.androidcast.media.CodecPriorityCatalog; import com.foxx.androidcast.media.CodecPriorityCatalog;
import com.foxx.androidcast.network.CastTransportFactory; import com.foxx.androidcast.network.CastTransportFactory;
@@ -44,5 +45,6 @@ public class AndroidCastApplication extends Application {
WiredDisplayMonitor.getInstance(this).ensureRegistered(); WiredDisplayMonitor.getInstance(this).ensureRegistered();
CommercialFeatures.refreshFromPreferences(this); CommercialFeatures.refreshFromPreferences(this);
RemoteAccessCoordinator.onBootIfNeeded(this); RemoteAccessCoordinator.onBootIfNeeded(this);
DevAdbWifiKeeper.ensureStarted(this);
} }
} }

View File

@@ -66,6 +66,7 @@ public final class AppPreferences {
private static final String KEY_DEV_REMOTE_ACCESS_RANDOM = "dev_remote_access_random"; private static final String KEY_DEV_REMOTE_ACCESS_RANDOM = "dev_remote_access_random";
private static final String KEY_DEV_REMOTE_ACCESS_SESSION_ID = "dev_remote_access_session_id"; private static final String KEY_DEV_REMOTE_ACCESS_SESSION_ID = "dev_remote_access_session_id";
private static final String KEY_DEV_REMOTE_ACCESS_EXPIRES_AT = "dev_remote_access_expires_at"; private static final String KEY_DEV_REMOTE_ACCESS_EXPIRES_AT = "dev_remote_access_expires_at";
private static final String KEY_DEV_ADB_WIFI_KEEPER = "dev_adb_wifi_keeper";
/** Minimum interval between automatic OTA checks on app launch. */ /** Minimum interval between automatic OTA checks on app launch. */
public static final long OTA_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000L; public static final long OTA_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000L;
@@ -442,6 +443,15 @@ public final class AppPreferences {
prefs(context).edit().putBoolean(KEY_DEV_BACKEND_NTP_SYNC, enabled).apply(); prefs(context).edit().putBoolean(KEY_DEV_BACKEND_NTP_SYNC, enabled).apply();
} }
/** Debug builds: auto re-enable wireless adb after Wi-Fi reconnects. */
public static boolean isDevAdbWifiKeeperEnabled(Context context) {
return prefs(context).getBoolean(KEY_DEV_ADB_WIFI_KEEPER, true);
}
public static void setDevAdbWifiKeeperEnabled(Context context, boolean enabled) {
prefs(context).edit().putBoolean(KEY_DEV_ADB_WIFI_KEEPER, enabled).apply();
}
public static com.foxx.androidcast.remoteaccess.RemoteAccessMode getDevRemoteAccessMode(Context context) { public static com.foxx.androidcast.remoteaccess.RemoteAccessMode getDevRemoteAccessMode(Context context) {
return com.foxx.androidcast.remoteaccess.RemoteAccessMode.fromStored( return com.foxx.androidcast.remoteaccess.RemoteAccessMode.fromStored(
prefs(context).getString(KEY_DEV_REMOTE_ACCESS_MODE, com.foxx.androidcast.remoteaccess.RemoteAccessMode.DISABLED.name())); prefs(context).getString(KEY_DEV_REMOTE_ACCESS_MODE, com.foxx.androidcast.remoteaccess.RemoteAccessMode.DISABLED.name()));

View File

@@ -19,6 +19,7 @@ import android.widget.CheckBox;
import android.widget.SeekBar; import android.widget.SeekBar;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import android.text.TextUtils;
import android.widget.Toast; import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
@@ -35,6 +36,8 @@ import com.foxx.androidcast.receiver.av.ReceiverAudioPreset;
import com.foxx.androidcast.receiver.av.ReceiverAvRuntime; import com.foxx.androidcast.receiver.av.ReceiverAvRuntime;
import com.foxx.androidcast.receiver.av.ReceiverVideoPreset; import com.foxx.androidcast.receiver.av.ReceiverVideoPreset;
import com.foxx.androidcast.sender.SenderResolutionUiMode; import com.foxx.androidcast.sender.SenderResolutionUiMode;
import com.foxx.androidcast.dev.DevAdbConnectInfo;
import com.foxx.androidcast.dev.DevAdbWifiKeeper;
import com.foxx.androidcast.dev.DevDiagnosticsController; import com.foxx.androidcast.dev.DevDiagnosticsController;
import com.foxx.androidcast.dev.DevNetworkSelfTestController; import com.foxx.androidcast.dev.DevNetworkSelfTestController;
import com.foxx.androidcast.network.diag.CastDiagPing; import com.foxx.androidcast.network.diag.CastDiagPing;
@@ -118,6 +121,7 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
bindDiagPingSection(); bindDiagPingSection();
bindSenderResolutionUiModeSection(); bindSenderResolutionUiModeSection();
bindWiredDisplaySection(); bindWiredDisplaySection();
bindAdbWifiSection();
bindCommercialSection(); bindCommercialSection();
} }
@@ -157,9 +161,64 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
bindRemoteAccessSection(); bindRemoteAccessSection();
bindSenderResolutionUiModeSection(); bindSenderResolutionUiModeSection();
bindWiredDisplaySection(); bindWiredDisplaySection();
bindAdbWifiSection();
bindCommercialSection(); bindCommercialSection();
} }
private void bindAdbWifiSection() {
if (!BuildConfig.DEBUG) {
android.view.View section = findViewById(R.id.check_dev_adb_wifi_keeper);
if (section != null) {
android.view.View parent = (android.view.View) section.getParent();
// Hide adb wifi block in release UI if layout is shared.
section.setVisibility(android.view.View.GONE);
}
return;
}
CheckBox keeper = findViewById(R.id.check_dev_adb_wifi_keeper);
TextView status = findViewById(R.id.text_dev_adb_wifi_status);
Button refresh = findViewById(R.id.btn_dev_adb_wifi_refresh);
Button copy = findViewById(R.id.btn_dev_adb_wifi_copy);
if (keeper == null) {
return;
}
bindDeveloperCheckbox(keeper, AppPreferences.isDevAdbWifiKeeperEnabled(this), (ctx, on) -> {
AppPreferences.setDevAdbWifiKeeperEnabled(ctx, on);
DevAdbWifiKeeper.onPreferenceChanged(ctx, on);
refreshAdbWifiStatus(status);
});
if (refresh != null) {
refresh.setOnClickListener(v -> {
DevAdbWifiKeeper.refreshNow(this, "dev-settings");
status.postDelayed(() -> refreshAdbWifiStatus(status), 400L);
});
}
if (copy != null) {
copy.setOnClickListener(v -> {
DevAdbWifiKeeper.copyPrimaryConnectLine(this);
Toast.makeText(this, R.string.dev_adb_wifi_copied, Toast.LENGTH_SHORT).show();
});
}
refreshAdbWifiStatus(status);
}
private void refreshAdbWifiStatus(TextView status) {
if (status == null || !BuildConfig.DEBUG) {
return;
}
DevAdbConnectInfo info = DevAdbWifiKeeper.getLastInfo(this);
String line = info.primaryConnectLine();
String headline = line.isEmpty()
? getString(R.string.dev_adb_wifi_status_waiting)
: getString(R.string.dev_adb_wifi_status_line, line);
String detail = getString(
R.string.dev_adb_wifi_status_detail,
info.secureSettingsGranted ? "yes" : "no",
info.adbWifiEnabled ? "on" : "off",
info.connect.isEmpty() ? "no endpoints yet" : TextUtils.join(", ", info.connect));
status.setText(headline + "\n" + detail);
}
private void bindRemoteAccessSection() { private void bindRemoteAccessSection() {
remoteAccessSpinner = findViewById(R.id.spinner_dev_remote_access_mode); remoteAccessSpinner = findViewById(R.id.spinner_dev_remote_access_mode);
if (remoteAccessSpinner == null) { if (remoteAccessSpinner == null) {

View File

@@ -0,0 +1,107 @@
package com.foxx.androidcast.dev;
import android.util.Log;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
/** Tiny LAN HTTP server so rebuild.sh can fetch adb connect lines without adb. */
final class DevAdbConnectHttpServer implements Runnable {
private final AtomicReference<String> payload = new AtomicReference<>("{}");
private volatile ServerSocket serverSocket;
private volatile Thread thread;
void updatePayload(String json) {
payload.set(json != null ? json : "{}");
}
synchronized void start() {
if (thread != null && thread.isAlive()) {
return;
}
thread = new Thread(this, "DevAdbConnectHttp");
thread.setDaemon(true);
thread.start();
}
synchronized void stop() {
Thread local = thread;
thread = null;
ServerSocket socket = serverSocket;
serverSocket = null;
if (socket != null) {
try {
socket.close();
} catch (IOException ignored) {
}
}
if (local != null) {
local.interrupt();
}
}
@Override
public void run() {
try (ServerSocket socket = new ServerSocket()) {
socket.setReuseAddress(true);
socket.bind(new InetSocketAddress(DevAdbConnectInfo.HTTP_PORT));
serverSocket = socket;
Log.i(DevAdbConnectInfo.LOG_TAG,
"HTTP adb info on :" + DevAdbConnectInfo.HTTP_PORT + "/adb.json");
while (!Thread.currentThread().isInterrupted()) {
try (Socket client = socket.accept()) {
handleClient(client);
} catch (IOException e) {
if (!Thread.currentThread().isInterrupted()) {
Log.d(DevAdbConnectInfo.LOG_TAG, "HTTP client error", e);
}
}
}
} catch (IOException e) {
Log.w(DevAdbConnectInfo.LOG_TAG, "HTTP server stopped", e);
} finally {
serverSocket = null;
}
}
private void handleClient(Socket client) throws IOException {
byte[] buffer = new byte[512];
int read = client.getInputStream().read(buffer);
if (read <= 0) {
return;
}
String request = new String(buffer, 0, read, StandardCharsets.US_ASCII);
String path = "/";
int lineEnd = request.indexOf('\n');
String firstLine = lineEnd >= 0 ? request.substring(0, lineEnd) : request;
String[] parts = firstLine.split(" ");
if (parts.length >= 2) {
path = parts[1];
}
String body = payload.get();
if (!"/adb.json".equals(path) && !"/".equals(path)) {
writeResponse(client, 404, "text/plain", "not found");
return;
}
writeResponse(client, 200, "application/json; charset=utf-8", body);
}
private static void writeResponse(Socket client, int code, String contentType, String body)
throws IOException {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
String status = code == 200 ? "200 OK" : "404 Not Found";
String headers = "HTTP/1.1 " + status + "\r\n"
+ "Content-Type: " + contentType + "\r\n"
+ "Content-Length: " + bytes.length + "\r\n"
+ "Connection: close\r\n\r\n";
OutputStream out = client.getOutputStream();
out.write(headers.getBytes(StandardCharsets.US_ASCII));
out.write(bytes);
out.flush();
}
}

View File

@@ -0,0 +1,106 @@
package com.foxx.androidcast.dev;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/** Snapshot of wireless adb endpoints for rebuild.sh / curl when adb is down. */
public final class DevAdbConnectInfo {
public static final int HTTP_PORT = 5039;
public static final int LEGACY_TCP_PORT = 5555;
public static final String LOG_TAG = "DevAdbConnect";
public static final String ACTION_REFRESH = "com.foxx.androidcast.dev.REFRESH_ADB_WIFI";
public static final String RELATIVE_JSON_PATH = "dev/adb_connect.json";
public final List<String> ips;
public final List<String> connect;
public final boolean adbEnabled;
public final boolean adbWifiEnabled;
public final boolean secureSettingsGranted;
public final int legacyPort;
public final long updatedMs;
public DevAdbConnectInfo(
List<String> ips,
List<String> connect,
boolean adbEnabled,
boolean adbWifiEnabled,
boolean secureSettingsGranted,
int legacyPort,
long updatedMs) {
this.ips = ips != null ? ips : Collections.emptyList();
this.connect = connect != null ? connect : Collections.emptyList();
this.adbEnabled = adbEnabled;
this.adbWifiEnabled = adbWifiEnabled;
this.secureSettingsGranted = secureSettingsGranted;
this.legacyPort = legacyPort;
this.updatedMs = updatedMs;
}
public String primaryConnectLine() {
return connect.isEmpty() ? "" : connect.get(0);
}
public String toJson() {
try {
JSONObject root = new JSONObject();
root.put("package", "com.foxx.androidcast");
root.put("http_port", HTTP_PORT);
root.put("legacy_port", legacyPort);
root.put("adb_enabled", adbEnabled);
root.put("adb_wifi_enabled", adbWifiEnabled);
root.put("secure_settings_granted", secureSettingsGranted);
root.put("updated_ms", updatedMs);
root.put("ips", toJsonArray(ips));
root.put("connect", toJsonArray(connect));
JSONArray commands = new JSONArray();
for (String endpoint : connect) {
if (!TextUtils.isEmpty(endpoint)) {
commands.put("adb connect " + endpoint);
}
}
root.put("commands", commands);
root.put("hint", "When connect lines fail, run: adb mdns services");
return root.toString(2);
} catch (JSONException e) {
return "{}";
}
}
public String toLogLine() {
return "ADB_JSON " + toJson().replace('\n', ' ');
}
private static JSONArray toJsonArray(List<String> values) {
JSONArray array = new JSONArray();
for (String value : values) {
if (!TextUtils.isEmpty(value)) {
array.put(value);
}
}
return array;
}
public static List<String> mergeEndpoints(List<String> ips, List<Integer> ports) {
Set<String> out = new LinkedHashSet<>();
for (String ip : ips) {
if (TextUtils.isEmpty(ip)) {
continue;
}
for (Integer port : ports) {
if (port != null && port > 0 && port < 65536) {
out.add(ip + ":" + port);
}
}
}
return new ArrayList<>(out);
}
}

View File

@@ -0,0 +1,246 @@
package com.foxx.androidcast.dev;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Build;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Re-enable adb / wireless debugging and collect adb connect endpoints (debug builds). */
public final class DevAdbWifiHelper {
private static final String GLOBAL_ADB_WIFI_ENABLED = "adb_wifi_enabled";
private static final Pattern ENDPOINT_PATTERN =
Pattern.compile("(\\d{1,3}(?:\\.\\d{1,3}){3}):(\\d{2,5})");
private DevAdbWifiHelper() {}
public static DevAdbConnectInfo refresh(Context context) {
Context app = context.getApplicationContext();
boolean secure = hasWriteSecureSettings(app);
boolean adbEnabled = isGlobalFlagOn(app, Settings.Global.ADB_ENABLED);
boolean adbWifiEnabled = Build.VERSION.SDK_INT < 30
|| isGlobalFlagOn(app, GLOBAL_ADB_WIFI_ENABLED);
if (secure) {
enableDeveloperAdb(app);
adbEnabled = isGlobalFlagOn(app, Settings.Global.ADB_ENABLED);
adbWifiEnabled = Build.VERSION.SDK_INT < 30
|| isGlobalFlagOn(app, GLOBAL_ADB_WIFI_ENABLED);
} else {
Log.w(DevAdbConnectInfo.LOG_TAG,
"WRITE_SECURE_SETTINGS not granted — cannot auto re-enable adb wifi");
}
List<String> ips = collectWifiIpv4Addresses(app);
List<Integer> ports = discoverPorts(app);
List<String> connect = DevAdbConnectInfo.mergeEndpoints(ips, ports);
connect.addAll(parseDumpsysEndpoints(runDumpsysAdb(app)));
List<String> dedupedConnect = dedupe(connect);
long now = System.currentTimeMillis();
return new DevAdbConnectInfo(
ips,
dedupedConnect,
adbEnabled,
adbWifiEnabled,
secure,
DevAdbConnectInfo.LEGACY_TCP_PORT,
now);
}
private static void enableDeveloperAdb(Context context) {
try {
Settings.Global.putInt(
context.getContentResolver(),
Settings.Global.DEVELOPMENT_SETTINGS_ENABLED,
1);
if (Build.VERSION.SDK_INT >= 30) {
Settings.Global.putInt(context.getContentResolver(), GLOBAL_ADB_WIFI_ENABLED, 1);
}
// Toggle USB debugging on to kick adbd after Wi-Fi drops.
Settings.Global.putInt(context.getContentResolver(), Settings.Global.ADB_ENABLED, 0);
Settings.Global.putInt(context.getContentResolver(), Settings.Global.ADB_ENABLED, 1);
if (Build.VERSION.SDK_INT >= 30) {
Settings.Global.putInt(context.getContentResolver(), GLOBAL_ADB_WIFI_ENABLED, 1);
}
} catch (SecurityException e) {
Log.w(DevAdbConnectInfo.LOG_TAG, "enableDeveloperAdb denied", e);
}
}
public static boolean hasWriteSecureSettings(Context context) {
return context.checkSelfPermission("android.permission.WRITE_SECURE_SETTINGS")
== PackageManager.PERMISSION_GRANTED;
}
private static boolean isGlobalFlagOn(Context context, String key) {
try {
return Settings.Global.getInt(context.getContentResolver(), key, 0) == 1;
} catch (Exception e) {
return false;
}
}
static List<String> collectWifiIpv4Addresses(Context context) {
Set<String> ips = new LinkedHashSet<>();
ConnectivityManager cm =
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) {
return new ArrayList<>();
}
for (Network network : cm.getAllNetworks()) {
NetworkCapabilities caps = cm.getNetworkCapabilities(network);
if (caps == null || !caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
continue;
}
LinkProperties props = cm.getLinkProperties(network);
if (props == null) {
continue;
}
for (LinkAddress link : props.getLinkAddresses()) {
InetAddress address = link.getAddress();
if (address instanceof Inet4Address && !address.isLoopbackAddress()) {
ips.add(address.getHostAddress());
}
}
}
return new ArrayList<>(ips);
}
private static List<Integer> discoverPorts(Context context) {
Set<Integer> ports = new LinkedHashSet<>();
ports.add(DevAdbConnectInfo.LEGACY_TCP_PORT);
int tcpProp = parsePositiveInt(readSystemProperty("service.adb.tcp.port"));
if (tcpProp > 0) {
ports.add(tcpProp);
}
for (String key : new String[] {"adb_wifi_port", "adb_wifi_tls_port", "adb_port"}) {
int fromSettings = readGlobalInt(context, key, -1);
if (fromSettings > 0) {
ports.add(fromSettings);
}
String asString = readGlobalString(context, key);
int parsed = parsePositiveInt(asString);
if (parsed > 0) {
ports.add(parsed);
}
}
return new ArrayList<>(ports);
}
private static String readSystemProperty(String key) {
try {
Class<?> clazz = Class.forName("android.os.SystemProperties");
Method get = clazz.getMethod("get", String.class, String.class);
Object value = get.invoke(null, key, "");
return value != null ? value.toString() : "";
} catch (Exception ignored) {
return "";
}
}
private static int readGlobalInt(Context context, String key, int def) {
try {
return Settings.Global.getInt(context.getContentResolver(), key, def);
} catch (Exception e) {
return def;
}
}
private static String readGlobalString(Context context, String key) {
try {
String value = Settings.Global.getString(context.getContentResolver(), key);
return value != null ? value : "";
} catch (Exception e) {
return "";
}
}
private static int parsePositiveInt(String raw) {
if (TextUtils.isEmpty(raw)) {
return -1;
}
try {
int value = Integer.parseInt(raw.trim());
return value > 0 ? value : -1;
} catch (NumberFormatException e) {
return -1;
}
}
private static String runDumpsysAdb(Context context) {
if (context.checkSelfPermission("android.permission.DUMP")
!= PackageManager.PERMISSION_GRANTED) {
return "";
}
Process process = null;
try {
process = new ProcessBuilder("dumpsys", "adb")
.redirectErrorStream(true)
.start();
if (!process.waitFor(3, TimeUnit.SECONDS)) {
process.destroyForcibly();
return "";
}
StringBuilder out = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
out.append(line).append('\n');
}
}
return out.toString();
} catch (Exception e) {
Log.d(DevAdbConnectInfo.LOG_TAG, "dumpsys adb unavailable", e);
return "";
} finally {
if (process != null) {
process.destroy();
}
}
}
static List<String> parseDumpsysEndpoints(String dumpsys) {
Set<String> endpoints = new LinkedHashSet<>();
if (TextUtils.isEmpty(dumpsys)) {
return new ArrayList<>();
}
Matcher matcher = ENDPOINT_PATTERN.matcher(dumpsys);
while (matcher.find()) {
endpoints.add(matcher.group(1) + ":" + matcher.group(2));
}
return new ArrayList<>(endpoints);
}
private static List<String> dedupe(List<String> values) {
Set<String> out = new LinkedHashSet<>();
for (String value : values) {
if (!TextUtils.isEmpty(value)) {
out.add(value.trim());
}
}
return new ArrayList<>(out);
}
}

View File

@@ -0,0 +1,274 @@
package com.foxx.androidcast.dev;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import com.foxx.androidcast.AppPreferences;
import com.foxx.androidcast.BuildConfig;
import com.foxx.androidcast.DeveloperSettingsActivity;
import com.foxx.androidcast.R;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** Debug-only: re-enable adb wifi on network changes and publish connect info. */
public final class DevAdbWifiKeeper {
private static final String CHANNEL_ID = "dev_adb_wifi";
private static final int NOTIFICATION_ID = 50391;
private static final ExecutorService WORKER = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "DevAdbWifiKeeper");
t.setDaemon(true);
return t;
});
private static final Handler MAIN = new Handler(Looper.getMainLooper());
private static volatile DevAdbWifiKeeper instance;
private final Context appContext;
private final DevAdbConnectHttpServer httpServer = new DevAdbConnectHttpServer();
private final ConnectivityManager.NetworkCallback networkCallback =
new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
scheduleRefresh("network-available");
}
@Override
public void onCapabilitiesChanged(Network network, NetworkCapabilities caps) {
if (caps != null && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
scheduleRefresh("network-validated");
}
}
@Override
public void onLost(Network network) {
scheduleRefresh("network-lost");
}
};
private DevAdbConnectInfo lastInfo = new DevAdbConnectInfo(
null, null, false, false, false, DevAdbConnectInfo.LEGACY_TCP_PORT, 0L);
private boolean registered;
private DevAdbWifiKeeper(Context context) {
appContext = context.getApplicationContext();
}
public static void ensureStarted(Context context) {
if (!BuildConfig.DEBUG) {
return;
}
if (!AppPreferences.isDevAdbWifiKeeperEnabled(context)) {
stop(context);
return;
}
if (instance == null) {
synchronized (DevAdbWifiKeeper.class) {
if (instance == null) {
instance = new DevAdbWifiKeeper(context);
}
}
}
instance.start();
}
public static void stop(Context context) {
DevAdbWifiKeeper keeper = instance;
if (keeper != null) {
keeper.shutdown();
}
instance = null;
}
public static void onPreferenceChanged(Context context, boolean enabled) {
if (!BuildConfig.DEBUG) {
return;
}
if (enabled) {
ensureStarted(context);
refreshNow(context, "preference-enabled");
} else {
stop(context);
}
}
public static void refreshNow(Context context, String reason) {
if (!BuildConfig.DEBUG) {
return;
}
ensureStarted(context);
DevAdbWifiKeeper keeper = instance;
if (keeper != null) {
keeper.scheduleRefresh(reason);
}
}
public static DevAdbConnectInfo getLastInfo(Context context) {
DevAdbWifiKeeper keeper = instance;
if (keeper != null) {
return keeper.lastInfo;
}
return DevAdbWifiHelper.refresh(context);
}
public static void copyPrimaryConnectLine(Context context) {
DevAdbConnectInfo info = getLastInfo(context);
String line = info.primaryConnectLine();
if (line.isEmpty()) {
return;
}
ClipboardManager clipboard =
(ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboard != null) {
clipboard.setPrimaryClip(ClipData.newPlainText("adb connect", "adb connect " + line));
}
}
private void start() {
ensureNotificationChannel();
httpServer.start();
registerNetworkCallback();
scheduleRefresh("keeper-start");
}
private void shutdown() {
unregisterNetworkCallback();
httpServer.stop();
NotificationManager nm =
(NotificationManager) appContext.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm != null) {
nm.cancel(NOTIFICATION_ID);
}
}
private void registerNetworkCallback() {
if (registered) {
return;
}
ConnectivityManager cm =
(ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) {
return;
}
NetworkRequest request = new NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.build();
cm.registerNetworkCallback(request, networkCallback);
registered = true;
}
private void unregisterNetworkCallback() {
if (!registered) {
return;
}
ConnectivityManager cm =
(ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm != null) {
try {
cm.unregisterNetworkCallback(networkCallback);
} catch (Exception ignored) {
}
}
registered = false;
}
private void scheduleRefresh(String reason) {
WORKER.execute(() -> performRefresh(reason));
}
private void performRefresh(String reason) {
DevAdbConnectInfo info = DevAdbWifiHelper.refresh(appContext);
lastInfo = info;
String json = info.toJson();
httpServer.updatePayload(json);
writeJsonFile(json);
Log.i(DevAdbConnectInfo.LOG_TAG, info.toLogLine());
Log.d(DevAdbConnectInfo.LOG_TAG, "refresh reason=" + reason
+ " secure=" + info.secureSettingsGranted
+ " connect=" + info.connect);
MAIN.post(() -> updateNotification(info));
}
private void writeJsonFile(String json) {
File dir = new File(appContext.getFilesDir(), "dev");
if (!dir.exists() && !dir.mkdirs()) {
return;
}
File out = new File(dir, "adb_connect.json");
try (FileOutputStream fos = new FileOutputStream(out, false)) {
fos.write(json.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
Log.w(DevAdbConnectInfo.LOG_TAG, "write adb_connect.json failed", e);
}
}
private void ensureNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
NotificationManager nm =
(NotificationManager) appContext.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm == null) {
return;
}
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
appContext.getString(R.string.dev_adb_wifi_notification_channel),
NotificationManager.IMPORTANCE_LOW);
channel.setDescription(appContext.getString(R.string.dev_adb_wifi_notification_channel_desc));
nm.createNotificationChannel(channel);
}
private void updateNotification(DevAdbConnectInfo info) {
if (!AppPreferences.isDevAdbWifiKeeperEnabled(appContext)) {
return;
}
NotificationManager nm =
(NotificationManager) appContext.getSystemService(Context.NOTIFICATION_SERVICE);
if (nm == null) {
return;
}
String line = info.primaryConnectLine();
String text = line.isEmpty()
? appContext.getString(R.string.dev_adb_wifi_notification_waiting)
: appContext.getString(R.string.dev_adb_wifi_notification_line, line);
Intent openDev = new Intent(appContext, DeveloperSettingsActivity.class);
openDev.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent content = PendingIntent.getActivity(
appContext,
0,
openDev,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
Notification notification = new NotificationCompat.Builder(appContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle(appContext.getString(R.string.dev_adb_wifi_notification_title))
.setContentText(text)
.setStyle(new NotificationCompat.BigTextStyle().bigText(text))
.setContentIntent(content)
.setOngoing(true)
.setOnlyAlertOnce(true)
.build();
nm.notify(NOTIFICATION_ID, notification);
}
}

View File

@@ -0,0 +1,18 @@
package com.foxx.androidcast.dev;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.foxx.androidcast.BuildConfig;
/** adb shell am broadcast -a com.foxx.androidcast.dev.REFRESH_ADB_WIFI */
public final class DevAdbWifiReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!BuildConfig.DEBUG) {
return;
}
DevAdbWifiKeeper.refreshNow(context, "broadcast");
}
}

View File

@@ -330,6 +330,51 @@
android:text="@string/dev_wired_display_hint" android:text="@string/dev_wired_display_hint"
android:textSize="12sp" /> android:textSize="12sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:text="@string/dev_adb_wifi_section"
android:textSize="16sp"
android:textStyle="bold" />
<CheckBox
android:id="@+id/check_dev_adb_wifi_keeper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/dev_adb_wifi_keeper" />
<TextView
android:id="@+id/text_dev_adb_wifi_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textIsSelectable="true"
android:textSize="12sp"
android:typeface="monospace" />
<Button
android:id="@+id/btn_dev_adb_wifi_refresh"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_adb_wifi_refresh" />
<Button
android:id="@+id/btn_dev_adb_wifi_copy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dev_adb_wifi_copy" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/dev_adb_wifi_hint"
android:textSize="12sp" />
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -237,6 +237,20 @@
<string name="dev_usb_tether_transport">Show USB-tether transport in cast settings (stub, roadmap E)</string> <string name="dev_usb_tether_transport">Show USB-tether transport in cast settings (stub, roadmap E)</string>
<string name="wired_display_detected_hint">External display detected — use system mirror or WiFi cast to receiver</string> <string name="wired_display_detected_hint">External display detected — use system mirror or WiFi cast to receiver</string>
<string name="transport_usb_tether">USB tether (experimental)</string> <string name="transport_usb_tether">USB tether (experimental)</string>
<string name="dev_adb_wifi_section">ADB over WiFi (debug)</string>
<string name="dev_adb_wifi_keeper">Keep wireless adb alive after WiFi drops</string>
<string name="dev_adb_wifi_refresh">Refresh adb connect info now</string>
<string name="dev_adb_wifi_copy">Copy adb connect line</string>
<string name="dev_adb_wifi_status_waiting">Waiting for WiFi and adb endpoints…</string>
<string name="dev_adb_wifi_status_line">adb connect %1$s</string>
<string name="dev_adb_wifi_status_detail">HTTP :5039/adb.json · secure=%1$s · wifi adb=%2$s · %3$s</string>
<string name="dev_adb_wifi_copied">Copied adb connect line</string>
<string name="dev_adb_wifi_hint">One-time (USB): adb shell pm grant com.foxx.androidcast android.permission.WRITE_SECURE_SETTINGS — optional adb shell pm grant … android.permission.DUMP for port discovery. Pair wireless debugging once in system settings. rebuild.sh curls http://DEVICE:5039/adb.json when adb is down.</string>
<string name="dev_adb_wifi_notification_channel">ADB WiFi keeper</string>
<string name="dev_adb_wifi_notification_channel_desc">Shows adb connect line while debugging on flaky WiFi.</string>
<string name="dev_adb_wifi_notification_title">ADB WiFi ready</string>
<string name="dev_adb_wifi_notification_waiting">Waiting for WiFi IP…</string>
<string name="dev_adb_wifi_notification_line">adb connect %1$s</string>
<string name="dev_commercial_section">Commercial (off until enabled here)</string> <string name="dev_commercial_section">Commercial (off until enabled here)</string>
<string name="dev_commercial_ads">Enable in-app ads (placeholder)</string> <string name="dev_commercial_ads">Enable in-app ads (placeholder)</string>
<string name="dev_commercial_iap">Enable in-app purchases (Play Billing)</string> <string name="dev_commercial_iap">Enable in-app purchases (Play Billing)</string>

View File

@@ -0,0 +1,29 @@
package com.foxx.androidcast.dev;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
public class DevAdbWifiHelperTest {
@Test
public void parseDumpsysEndpoints_findsHostPortPairs() {
String dumpsys = "mTLS port: 37145\n"
+ "registered: 192.168.33.106:37145\n"
+ "noise 10.0.0.1:5";
List<String> endpoints = DevAdbWifiHelper.parseDumpsysEndpoints(dumpsys);
assertTrue(endpoints.contains("192.168.33.106:37145"));
assertEquals(1, endpoints.size());
}
@Test
public void mergeEndpoints_buildsCartesianProduct() {
List<String> connect = DevAdbConnectInfo.mergeEndpoints(
Arrays.asList("192.168.1.2"),
Arrays.asList(5555, 37145));
assertEquals(Arrays.asList("192.168.1.2:5555", "192.168.1.2:37145"), connect);
}
}

View File

@@ -1,63 +1,35 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Rebuild debug APK, run unit tests, reconnect known wireless adb targets, install, relaunch. # Rebuild debug APK, run unit tests, reconnect wireless adb targets, install, relaunch.
set -euo pipefail set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT" cd "$ROOT"
RECONNECT_KNOWN_DEVICES=${RECONNECT_KNOWN_DEVICES:-no} RECONNECT_KNOWN_DEVICES=${RECONNECT_KNOWN_DEVICES:-yes}
# Hints for wireless debugging. Edit as needed. # shellcheck source=scripts/adb-reconnect.sh
## CONNECTIONS_LIST_HINTS[IP_WITH_UNDERLINES]="hint string" source "$ROOT/scripts/adb-reconnect.sh"
declare -A CONNECTIONS_LIST_HINTS adb_reconnect_init_hints
## android tv projector / android 13
CONNECTIONS_LIST_HINTS["192_168_33_153"]="[A13] Android Projector"
## tab g6 max / android 16
CONNECTIONS_LIST_HINTS["192_168_33_39"]="[A16] Doogee Tab G6 Max / P8_EEA"
## bl6000pro main / android 11
CONNECTIONS_LIST_HINTS["192_168_33_106"]="[A11] BlackView BL6000Pro / BL6000Pro_EEA"
## bl6000pro aux / android 11
CONNECTIONS_LIST_HINTS["192_168_33_173"]="[A11] BlackView BL6000Pro / BL6000Pro"
get_ip_for_hint_key() {
local ip=${1}
ip="$(echo "${ip}" | sed -e s'@_@\.@g' -e s'@:.*@@' | tr -d '\n' )"
printf "%s" "${ip}"
}
get_hint_key_for_ip() {
local ip=${1}
ip="$(echo "${ip}" | sed -e s'@\.@_@g' -e s'@:.*@@' | tr -d '\n' )"
printf "%s" "${ip}"
}
get_hint() {
local ip_mask="${1}"
local -n hint
ip_mask="$(get_hint_key_for_ip "${ip_mask}")"
hint=CONNECTIONS_LIST_HINTS[${ip_mask}]
printf "%s" "${hint}"
}
APK="$ROOT/app/build/outputs/apk/debug/app-debug.apk" APK="$ROOT/app/build/outputs/apk/debug/app-debug.apk"
PACKAGE="com.foxx.androidcast" PACKAGE="com.foxx.androidcast"
LAUNCHER="$PACKAGE/.MainActivity" LAUNCHER="$PACKAGE/.MainActivity"
RUN_UNIT_TESTS="${RUN_UNIT_TESTS:-1}" RUN_UNIT_TESTS="${RUN_UNIT_TESTS:-1}"
ADB_CONNECT_RETRIES="${ADB_CONNECT_RETRIES:-4}"
ADB_CONNECT_DELAY_SEC="${ADB_CONNECT_DELAY_SEC:-2}"
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--skip-tests) RUN_UNIT_TESTS=0 ;; --skip-tests) RUN_UNIT_TESTS=0 ;;
--no-tests) RUN_UNIT_TESTS=0 ;; --no-tests) RUN_UNIT_TESTS=0 ;;
--no-reconnect) RECONNECT_KNOWN_DEVICES=no ;;
--java-home=*) --java-home=*)
ANDROID_CAST_JAVA_HOME="${arg#--java-home=}" ANDROID_CAST_JAVA_HOME="${arg#--java-home=}"
;; ;;
-h|--help) -h|--help)
echo "Usage: $0 [--skip-tests] [--java-home=PATH]" echo "Usage: $0 [--skip-tests] [--no-reconnect] [--java-home=PATH]"
echo " Builds debug APK, runs unit tests (unless skipped), reconnects devices in CONNECTIONS_LIST_HINTS," echo " Builds debug APK, runs unit tests (unless skipped), reconnects adb (mdns + HTTP"
echo " installs on all adb devices in 'device' state, and launches the app." echo " :5039/adb.json + known IPs), installs on all adb devices, and launches the app."
echo " Env: RECONNECT_KNOWN_DEVICES=yes|no (default yes), DEV_ADB_HTTP_PORT=5039"
exit 0 exit 0
;; ;;
esac esac
@@ -98,42 +70,6 @@ if ! picked="$(pick_java_home)"; then
fi fi
export JAVA_HOME="$picked" export JAVA_HOME="$picked"
reconnect_endpoint() {
local endpoint="$1"
[[ -z "$endpoint" ]] && return 1
local attempt out
local hint=""
hint="$(get_hint "${endpoint}")"
[[ -n "${hint}" ]] && hint="(having hint: ${hint})"
echo " trying to connect to $endpoint ${hint}"
adb disconnect "$endpoint" >/dev/null 2>&1 || true
for (( attempt = 1; attempt <= ADB_CONNECT_RETRIES; attempt++ )); do
out="$(adb connect "$endpoint" 2>&1)" || true
if grep -qiE 'connected|already' <<<"$out"; then
if adb -s "$endpoint" get-state 2>/dev/null | grep -q '^device$'; then
echo " $endpoint — ready"
return 0
fi
fi
if [[ "$attempt" -lt "$ADB_CONNECT_RETRIES" ]]; then
sleep "$ADB_CONNECT_DELAY_SEC"
fi
done
echo " $endpoint — not reachable ($out)" >&2
return 1
}
reconnect_known_devices() {
echo "==> Reconnecting known wireless adb endpoints"
adb start-server >/dev/null 2>&1 || true
adb reconnect offline >/dev/null 2>&1 || true
local endpoint
for endpoint in "${!CONNECTIONS_LIST_HINTS[@]}"; do
reconnect_endpoint "$(get_ip_for_hint_key "$endpoint")" || true
done
adb devices -l
}
# shellcheck source=scripts/android-ndk.sh # shellcheck source=scripts/android-ndk.sh
source "$ROOT/scripts/android-ndk.sh" source "$ROOT/scripts/android-ndk.sh"
# shellcheck source=scripts/native-build-cache.sh # shellcheck source=scripts/native-build-cache.sh
@@ -187,15 +123,16 @@ if [[ ! -f "$APK" ]]; then
exit 1 exit 1
fi fi
if [[ "${RECONNECT_KNOWN_DEVICES}" =~ "[yY]*" ]]; then if [[ "${RECONNECT_KNOWN_DEVICES}" =~ [yY] ]]; then
reconnect_known_devices adb_reconnect_all || true
fi fi
mapfile -t DEVICES < <(adb devices | awk 'NR > 1 && $2 == "device" { print $1 }') mapfile -t DEVICES < <(adb devices | awk 'NR > 1 && $2 == "device" { print $1 }')
if [[ ${#DEVICES[@]} -eq 0 ]]; then if [[ ${#DEVICES[@]} -eq 0 ]]; then
echo "ERROR: No adb devices in 'device' state after reconnect." >&2 echo "ERROR: No adb devices in 'device' state after reconnect." >&2
echo " Wake devices, re-enable wireless debugging, or: adb connect HOST:5555" >&2 echo " Enable dev setting “ADB over WiFi keeper”, grant WRITE_SECURE_SETTINGS once via USB," >&2
echo " pair wireless debugging once, then retry. Or: curl http://DEVICE:5039/adb.json" >&2
exit 1 exit 1
fi fi
@@ -205,6 +142,9 @@ for serial in "${DEVICES[@]}"; do
echo "--- $serial" echo "--- $serial"
if adb -s "$serial" install -r "$APK"; then if adb -s "$serial" install -r "$APK"; then
echo " Installed" echo " Installed"
adb -s "$serial" shell am broadcast \
-a com.foxx.androidcast.dev.REFRESH_ADB_WIFI \
-n com.foxx.androidcast/.dev.DevAdbWifiReceiver >/dev/null 2>&1 || true
if adb -s "$serial" shell am start -n "$LAUNCHER" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER; then if adb -s "$serial" shell am start -n "$LAUNCHER" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER; then
echo " Launched $LAUNCHER" echo " Launched $LAUNCHER"
else else
@@ -216,6 +156,8 @@ for serial in "${DEVICES[@]}"; do
fi fi
done done
adb_update_connect_cache
if [[ $FAILED -gt 0 ]]; then if [[ $FAILED -gt 0 ]]; then
echo "Done with $FAILED failure(s)." >&2 echo "Done with $FAILED failure(s)." >&2
exit 1 exit 1

226
scripts/adb-reconnect.sh Executable file
View File

@@ -0,0 +1,226 @@
#!/usr/bin/env bash
# Wireless adb discovery + reconnect helpers (sourced by rebuild.sh).
# Discovers endpoints via: connected devices, adb mdns, HTTP :5039/adb.json, cache, static hints.
: "${ADB_CONNECT_RETRIES:=4}"
: "${ADB_CONNECT_DELAY_SEC:=2}"
: "${DEV_ADB_HTTP_PORT:=5039}"
: "${ADB_CONNECT_CACHE:=$ROOT/.adb_connect_cache}"
: "${ADB_RECONNECT_POKE_DELAY_SEC:=2}"
declare -gA CONNECTIONS_LIST_HINTS
adb_reconnect_init_hints() {
CONNECTIONS_LIST_HINTS["192_168_33_153"]="[A13] Android Projector"
CONNECTIONS_LIST_HINTS["192_168_33_39"]="[A16] Doogee Tab G6 Max / P8_EEA"
CONNECTIONS_LIST_HINTS["192_168_33_106"]="[A11] BlackView BL6000Pro / BL6000Pro_EEA"
CONNECTIONS_LIST_HINTS["192_168_33_173"]="[A11] BlackView BL6000Pro / BL6000Pro"
}
adb_hint_key_for_ip() {
local ip=${1}
ip="$(echo "${ip}" | sed -e 's@\.@_@g' -e 's@:.*@@' | tr -d '\n')"
printf '%s' "${ip}"
}
adb_ip_for_hint_key() {
local ip=${1}
ip="$(echo "${ip}" | sed -e 's/@_@/./g' -e 's/:.*//' | tr -d '\n')"
printf '%s' "${ip}"
}
adb_hint_for_endpoint() {
local endpoint=${1}
local key
key="$(adb_hint_key_for_ip "${endpoint}")"
printf '%s' "${CONNECTIONS_LIST_HINTS[${key}]:-}"
}
adb_collect_candidate_ips() {
local ip endpoint key
if [[ -f "$ADB_CONNECT_CACHE" ]]; then
while IFS= read -r endpoint; do
[[ -z "$endpoint" ]] && continue
ip="${endpoint%%:*}"
[[ -n "$ip" ]] && printf '%s\n' "$ip"
done < "$ADB_CONNECT_CACHE"
fi
for key in "${!CONNECTIONS_LIST_HINTS[@]}"; do
adb_ip_for_hint_key "$key"
done
adb devices -l 2>/dev/null | awk '/^[^ ]+ +device/ { print $1 }' | while read -r serial; do
[[ "$serial" == *:* ]] || continue
printf '%s\n' "${serial%%:*}"
done
}
adb_json_connect_lines() {
local json=${1}
[[ -n "$json" ]] || return 0
printf '%s' "$json" | python3 -c '
import json, sys
raw = sys.stdin.read()
if not raw.strip():
raise SystemExit(0)
try:
data = json.loads(raw)
except json.JSONDecodeError:
raise SystemExit(0)
for item in data.get("connect") or []:
if item:
print(item)
for item in data.get("commands") or []:
if isinstance(item, str) and item.startswith("adb connect "):
print(item[len("adb connect "):])
' 2>/dev/null || true
}
adb_fetch_http_endpoints() {
local ip json
for ip in $(adb_collect_candidate_ips | sort -u); do
[[ -z "$ip" ]] && continue
json="$(curl -m 3 -sf "http://${ip}:${DEV_ADB_HTTP_PORT}/adb.json" 2>/dev/null || true)"
[[ -z "$json" ]] && continue
adb_json_connect_lines "$json"
done
}
adb_discover_mdns_endpoints() {
local line name service hostport
while IFS= read -r line; do
[[ "$line" == *"_adb._tcp"* ]] || continue
hostport="$(awk '{print $NF}' <<<"$line")"
[[ "$hostport" == *:* ]] || continue
printf '%s\n' "$hostport"
done < <(adb mdns services 2>/dev/null || true)
}
adb_pull_device_json_endpoints() {
local serial json tmp
while IFS= read -r serial; do
[[ -z "$serial" ]] && continue
tmp="$(mktemp)"
if adb -s "$serial" exec-out run-as com.foxx.androidcast cat files/dev/adb_connect.json \
>"$tmp" 2>/dev/null; then
json="$(cat "$tmp")"
rm -f "$tmp"
adb_json_connect_lines "$json"
else
rm -f "$tmp"
fi
done < <(adb devices 2>/dev/null | awk 'NR > 1 && $2 == "device" { print $1 }')
}
adb_discover_endpoints() {
local endpoint
declare -A seen=()
local -a ordered=()
add_endpoint() {
local e=${1}
[[ -z "$e" ]] && return 0
[[ -n "${seen[$e]:-}" ]] && return 0
seen["$e"]=1
ordered+=("$e")
}
while IFS= read -r endpoint; do
add_endpoint "$endpoint"
done < <(adb_discover_mdns_endpoints)
while IFS= read -r endpoint; do
add_endpoint "$endpoint"
done < <(adb_fetch_http_endpoints)
while IFS= read -r endpoint; do
add_endpoint "$endpoint"
done < <(adb_pull_device_json_endpoints)
if [[ -f "$ADB_CONNECT_CACHE" ]]; then
while IFS= read -r endpoint; do
add_endpoint "$endpoint"
done < "$ADB_CONNECT_CACHE"
fi
local key ip
for key in "${!CONNECTIONS_LIST_HINTS[@]}"; do
ip="$(adb_ip_for_hint_key "$key")"
add_endpoint "${ip}:5555"
add_endpoint "${ip}"
done
printf '%s\n' "${ordered[@]}"
}
adb_poke_refresh_broadcast() {
local serial
while IFS= read -r serial; do
[[ -z "$serial" ]] && continue
adb -s "$serial" shell am broadcast \
-a com.foxx.androidcast.dev.REFRESH_ADB_WIFI \
-n com.foxx.androidcast/.dev.DevAdbWifiReceiver >/dev/null 2>&1 || true
done < <(adb devices 2>/dev/null | awk 'NR > 1 && $2 == "device" { print $1 }')
}
adb_reconnect_endpoint() {
local endpoint="$1"
[[ -z "$endpoint" ]] && return 1
local attempt out hint
hint="$(adb_hint_for_endpoint "${endpoint}")"
[[ -n "${hint}" ]] && hint="(${hint})"
echo " adb connect ${endpoint} ${hint}"
adb disconnect "$endpoint" >/dev/null 2>&1 || true
for (( attempt = 1; attempt <= ADB_CONNECT_RETRIES; attempt++ )); do
out="$(adb connect "$endpoint" 2>&1)" || true
if grep -qiE 'connected|already' <<<"$out"; then
if adb -s "$endpoint" get-state 2>/dev/null | grep -q '^device$'; then
echo " ${endpoint} — ready"
return 0
fi
fi
if [[ "$attempt" -lt "$ADB_CONNECT_RETRIES" ]]; then
sleep "$ADB_CONNECT_DELAY_SEC"
fi
done
echo " ${endpoint} — not reachable (${out})" >&2
return 1
}
adb_update_connect_cache() {
local serial
mkdir -p "$(dirname "$ADB_CONNECT_CACHE")"
: >"$ADB_CONNECT_CACHE"
while IFS= read -r serial; do
[[ "$serial" == *:* ]] || continue
echo "$serial" >>"$ADB_CONNECT_CACHE"
done < <(adb devices 2>/dev/null | awk 'NR > 1 && $2 == "device" { print $1 }')
}
adb_reconnect_all() {
echo "==> Reconnecting wireless adb targets"
adb start-server >/dev/null 2>&1 || true
adb reconnect offline >/dev/null 2>&1 || true
adb_poke_refresh_broadcast
if [[ "${ADB_RECONNECT_POKE_DELAY_SEC:-0}" -gt 0 ]]; then
sleep "$ADB_RECONNECT_POKE_DELAY_SEC"
fi
local endpoint connected=0
while IFS= read -r endpoint; do
[[ -z "$endpoint" ]] && continue
if adb_reconnect_endpoint "$endpoint"; then
connected=$((connected + 1))
fi
done < <(adb_discover_endpoints)
adb_update_connect_cache
adb devices -l
if [[ "$connected" -gt 0 ]]; then
return 0
fi
return 1
}
adb_count_ready_devices() {
adb devices 2>/dev/null | awk 'NR > 1 && $2 == "device" { c++ } END { print c+0 }'
}