mirror of
git://f0xx.org/android_cast
synced 2026-07-29 04:18:09 +03:00
test adb reconnect persistence
This commit is contained in:
10
.cursorignore
Normal file
10
.cursorignore
Normal file
@@ -0,0 +1,10 @@
|
||||
tmp/
|
||||
third-party/
|
||||
**/.gradle/
|
||||
**/build/
|
||||
**/.cxx/
|
||||
**/node_modules/
|
||||
out/
|
||||
ota-publish/
|
||||
*.apk
|
||||
*.jar
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -59,6 +59,7 @@ config.php.alpine
|
||||
# --- CI / OTA / local artifacts ---
|
||||
out/
|
||||
ota-publish/
|
||||
.adb_connect_cache
|
||||
|
||||
# --- Scratch (not for commit) ---
|
||||
tmp/
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
<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_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
|
||||
android:name=".AndroidCastApplication"
|
||||
@@ -165,5 +169,13 @@
|
||||
<receiver
|
||||
android:name=".TrayNotificationReceiver"
|
||||
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>
|
||||
</manifest>
|
||||
|
||||
@@ -16,6 +16,7 @@ import android.content.Context;
|
||||
|
||||
import com.foxx.androidcast.commercial.CommercialFeatures;
|
||||
import com.foxx.androidcast.crash.CrashReporter;
|
||||
import com.foxx.androidcast.dev.DevAdbWifiKeeper;
|
||||
import com.foxx.androidcast.display.WiredDisplayMonitor;
|
||||
import com.foxx.androidcast.media.CodecPriorityCatalog;
|
||||
import com.foxx.androidcast.network.CastTransportFactory;
|
||||
@@ -44,5 +45,6 @@ public class AndroidCastApplication extends Application {
|
||||
WiredDisplayMonitor.getInstance(this).ensureRegistered();
|
||||
CommercialFeatures.refreshFromPreferences(this);
|
||||
RemoteAccessCoordinator.onBootIfNeeded(this);
|
||||
DevAdbWifiKeeper.ensureStarted(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_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_ADB_WIFI_KEEPER = "dev_adb_wifi_keeper";
|
||||
|
||||
/** Minimum interval between automatic OTA checks on app launch. */
|
||||
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();
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
return com.foxx.androidcast.remoteaccess.RemoteAccessMode.fromStored(
|
||||
prefs(context).getString(KEY_DEV_REMOTE_ACCESS_MODE, com.foxx.androidcast.remoteaccess.RemoteAccessMode.DISABLED.name()));
|
||||
|
||||
@@ -19,6 +19,7 @@ import android.widget.CheckBox;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
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.ReceiverVideoPreset;
|
||||
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.DevNetworkSelfTestController;
|
||||
import com.foxx.androidcast.network.diag.CastDiagPing;
|
||||
@@ -118,6 +121,7 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
|
||||
bindDiagPingSection();
|
||||
bindSenderResolutionUiModeSection();
|
||||
bindWiredDisplaySection();
|
||||
bindAdbWifiSection();
|
||||
bindCommercialSection();
|
||||
}
|
||||
|
||||
@@ -157,9 +161,64 @@ public class DeveloperSettingsActivity extends AppCompatActivity {
|
||||
bindRemoteAccessSection();
|
||||
bindSenderResolutionUiModeSection();
|
||||
bindWiredDisplaySection();
|
||||
bindAdbWifiSection();
|
||||
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() {
|
||||
remoteAccessSpinner = findViewById(R.id.spinner_dev_remote_access_mode);
|
||||
if (remoteAccessSpinner == null) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
246
app/src/main/java/com/foxx/androidcast/dev/DevAdbWifiHelper.java
Normal file
246
app/src/main/java/com/foxx/androidcast/dev/DevAdbWifiHelper.java
Normal 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);
|
||||
}
|
||||
}
|
||||
274
app/src/main/java/com/foxx/androidcast/dev/DevAdbWifiKeeper.java
Normal file
274
app/src/main/java/com/foxx/androidcast/dev/DevAdbWifiKeeper.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -330,6 +330,51 @@
|
||||
android:text="@string/dev_wired_display_hint"
|
||||
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
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -237,6 +237,20 @@
|
||||
<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 Wi‑Fi cast to receiver</string>
|
||||
<string name="transport_usb_tether">USB tether (experimental)</string>
|
||||
<string name="dev_adb_wifi_section">ADB over Wi‑Fi (debug)</string>
|
||||
<string name="dev_adb_wifi_keeper">Keep wireless adb alive after Wi‑Fi 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 Wi‑Fi 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 Wi‑Fi keeper</string>
|
||||
<string name="dev_adb_wifi_notification_channel_desc">Shows adb connect line while debugging on flaky Wi‑Fi.</string>
|
||||
<string name="dev_adb_wifi_notification_title">ADB Wi‑Fi ready</string>
|
||||
<string name="dev_adb_wifi_notification_waiting">Waiting for Wi‑Fi 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_ads">Enable in-app ads (placeholder)</string>
|
||||
<string name="dev_commercial_iap">Enable in-app purchases (Play Billing)</string>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
96
rebuild.sh
96
rebuild.sh
@@ -1,63 +1,35 @@
|
||||
#!/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
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
RECONNECT_KNOWN_DEVICES=${RECONNECT_KNOWN_DEVICES:-no}
|
||||
RECONNECT_KNOWN_DEVICES=${RECONNECT_KNOWN_DEVICES:-yes}
|
||||
|
||||
# Hints for wireless debugging. Edit as needed.
|
||||
## CONNECTIONS_LIST_HINTS[IP_WITH_UNDERLINES]="hint string"
|
||||
declare -A CONNECTIONS_LIST_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}"
|
||||
}
|
||||
# shellcheck source=scripts/adb-reconnect.sh
|
||||
source "$ROOT/scripts/adb-reconnect.sh"
|
||||
adb_reconnect_init_hints
|
||||
|
||||
APK="$ROOT/app/build/outputs/apk/debug/app-debug.apk"
|
||||
PACKAGE="com.foxx.androidcast"
|
||||
LAUNCHER="$PACKAGE/.MainActivity"
|
||||
|
||||
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
|
||||
case "$arg" in
|
||||
--skip-tests) RUN_UNIT_TESTS=0 ;;
|
||||
--no-tests) RUN_UNIT_TESTS=0 ;;
|
||||
--no-reconnect) RECONNECT_KNOWN_DEVICES=no ;;
|
||||
--java-home=*)
|
||||
ANDROID_CAST_JAVA_HOME="${arg#--java-home=}"
|
||||
;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 [--skip-tests] [--java-home=PATH]"
|
||||
echo " Builds debug APK, runs unit tests (unless skipped), reconnects devices in CONNECTIONS_LIST_HINTS,"
|
||||
echo " installs on all adb devices in 'device' state, and launches the app."
|
||||
echo "Usage: $0 [--skip-tests] [--no-reconnect] [--java-home=PATH]"
|
||||
echo " Builds debug APK, runs unit tests (unless skipped), reconnects adb (mdns + HTTP"
|
||||
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
|
||||
;;
|
||||
esac
|
||||
@@ -98,42 +70,6 @@ if ! picked="$(pick_java_home)"; then
|
||||
fi
|
||||
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
|
||||
source "$ROOT/scripts/android-ndk.sh"
|
||||
# shellcheck source=scripts/native-build-cache.sh
|
||||
@@ -187,15 +123,16 @@ if [[ ! -f "$APK" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${RECONNECT_KNOWN_DEVICES}" =~ "[yY]*" ]]; then
|
||||
reconnect_known_devices
|
||||
if [[ "${RECONNECT_KNOWN_DEVICES}" =~ [yY] ]]; then
|
||||
adb_reconnect_all || true
|
||||
fi
|
||||
|
||||
mapfile -t DEVICES < <(adb devices | awk 'NR > 1 && $2 == "device" { print $1 }')
|
||||
|
||||
if [[ ${#DEVICES[@]} -eq 0 ]]; then
|
||||
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 Wi‑Fi 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
|
||||
fi
|
||||
|
||||
@@ -205,6 +142,9 @@ for serial in "${DEVICES[@]}"; do
|
||||
echo "--- $serial"
|
||||
if adb -s "$serial" install -r "$APK"; then
|
||||
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
|
||||
echo " Launched $LAUNCHER"
|
||||
else
|
||||
@@ -216,6 +156,8 @@ for serial in "${DEVICES[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
adb_update_connect_cache
|
||||
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
echo "Done with $FAILED failure(s)." >&2
|
||||
exit 1
|
||||
|
||||
226
scripts/adb-reconnect.sh
Executable file
226
scripts/adb-reconnect.sh
Executable 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 }'
|
||||
}
|
||||
Reference in New Issue
Block a user