mirror of
git://f0xx.org/android_cast
synced 2026-07-29 06:18:42 +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 ---
|
# --- CI / OTA / local artifacts ---
|
||||||
out/
|
out/
|
||||||
ota-publish/
|
ota-publish/
|
||||||
|
.adb_connect_cache
|
||||||
|
|
||||||
# --- Scratch (not for commit) ---
|
# --- Scratch (not for commit) ---
|
||||||
tmp/
|
tmp/
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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: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"
|
||||||
|
|||||||
@@ -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 Wi‑Fi cast to receiver</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="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_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>
|
||||||
|
|||||||
@@ -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
|
#!/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 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
|
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
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