fix(celular): sem anúncio na rede, o app varre a /24 atrás da placa
O roteador/chip Wi-Fi descarta o multicast entre aparelhos sem fio: a placa não via ninguém e o celular não recebia o _carinha._tcp. Sem anúncio em 6 s, o app pergunta /app/health a cada endereço da rede (32 em paralelo) e usa quem responde carinha-pc. Testado no S10: achou .35 em ~17 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,55 +7,75 @@ import android.os.Handler;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Acha a placa na rede pelo anúncio mDNS "_carinha._tcp" (avahi na Orange Pi, o mesmo do
|
* Acha a placa na rede quando o endereço salvo não responde, para o app se reconectar sem
|
||||||
* carinha.local). Assim, se o IP da placa mudar, o app se reconecta sem ninguém digitar endereço.
|
* ninguém digitar endereço:
|
||||||
* Uma busca por vez; desiste depois de alguns segundos e responde no fio principal.
|
* 1. pelo anúncio mDNS "_carinha._tcp" (avahi na Orange Pi, o mesmo do carinha.local);
|
||||||
|
* 2. se o anúncio não chegar (alguns roteadores e chips Wi-Fi descartam multicast entre aparelhos
|
||||||
|
* sem fio), perguntando a cada endereço da rede do celular quem responde /app/health como
|
||||||
|
* "carinha-pc".
|
||||||
|
* Uma busca por vez; a resposta chega no fio principal (null = não achou).
|
||||||
*/
|
*/
|
||||||
final class Descoberta {
|
final class Descoberta {
|
||||||
interface Resposta { void achou(String endereco); } // null = não achou
|
interface Resposta { void achou(String endereco); }
|
||||||
|
|
||||||
private static final String TAG = "CarinhaDescoberta";
|
private static final String TAG = "CarinhaDescoberta";
|
||||||
private static final String TIPO = "_carinha._tcp.";
|
private static final String TIPO = "_carinha._tcp.";
|
||||||
private static final long LIMITE = 8000;
|
private static final long LIMITE_ANUNCIO = 6000;
|
||||||
|
private static final int PORTA = 8100;
|
||||||
|
|
||||||
private final NsdManager nsd;
|
private final NsdManager nsd;
|
||||||
private final Handler principal = new Handler(Looper.getMainLooper());
|
private final Handler principal = new Handler(Looper.getMainLooper());
|
||||||
private NsdManager.DiscoveryListener ouvinte;
|
private NsdManager.DiscoveryListener ouvinte;
|
||||||
private Resposta resposta;
|
private Resposta resposta;
|
||||||
|
private boolean ocupada;
|
||||||
|
|
||||||
Descoberta(Context c) {
|
Descoberta(Context c) {
|
||||||
nsd = (NsdManager) c.getApplicationContext().getSystemService(Context.NSD_SERVICE);
|
nsd = (NsdManager) c.getApplicationContext().getSystemService(Context.NSD_SERVICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean procurando() { return ouvinte != null; }
|
boolean procurando() { return ocupada; }
|
||||||
|
|
||||||
void procurar(Resposta r) {
|
void procurar(Resposta r) {
|
||||||
if (nsd == null) { r.achou(null); return; }
|
if (ocupada) return;
|
||||||
if (ouvinte != null) return;
|
ocupada = true;
|
||||||
resposta = r;
|
resposta = r;
|
||||||
|
if (nsd == null) { semAnuncio(); return; }
|
||||||
ouvinte = new NsdManager.DiscoveryListener() {
|
ouvinte = new NsdManager.DiscoveryListener() {
|
||||||
@Override public void onDiscoveryStarted(String tipo) { }
|
@Override public void onDiscoveryStarted(String tipo) { }
|
||||||
@Override public void onDiscoveryStopped(String tipo) { }
|
@Override public void onDiscoveryStopped(String tipo) { }
|
||||||
@Override public void onStartDiscoveryFailed(String tipo, int erro) { terminar(null); }
|
@Override public void onStartDiscoveryFailed(String tipo, int erro) { fimDoAnuncio(null); }
|
||||||
@Override public void onStopDiscoveryFailed(String tipo, int erro) { }
|
@Override public void onStopDiscoveryFailed(String tipo, int erro) { }
|
||||||
@Override public void onServiceLost(NsdServiceInfo s) { }
|
@Override public void onServiceLost(NsdServiceInfo s) { }
|
||||||
@Override public void onServiceFound(NsdServiceInfo s) {
|
@Override public void onServiceFound(NsdServiceInfo s) {
|
||||||
if (!s.getServiceType().startsWith("_carinha._tcp")) return;
|
if (s.getServiceType().startsWith("_carinha._tcp")) resolver(s);
|
||||||
resolver(s);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
nsd.discoverServices(TIPO, NsdManager.PROTOCOL_DNS_SD, ouvinte);
|
nsd.discoverServices(TIPO, NsdManager.PROTOCOL_DNS_SD, ouvinte);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.w(TAG, "busca falhou: " + e);
|
Log.w(TAG, "busca por anúncio falhou: " + e);
|
||||||
ouvinte = null;
|
ouvinte = null;
|
||||||
r.achou(null);
|
semAnuncio();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
principal.postDelayed(new Runnable() {
|
principal.postDelayed(new Runnable() {
|
||||||
@Override public void run() { terminar(null); }
|
@Override public void run() { fimDoAnuncio(null); }
|
||||||
}, LIMITE);
|
}, LIMITE_ANUNCIO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@@ -67,7 +87,7 @@ final class Descoberta {
|
|||||||
if (s.getHost() == null || s.getHost().isLoopbackAddress()) return;
|
if (s.getHost() == null || s.getHost().isLoopbackAddress()) return;
|
||||||
String ip = s.getHost().getHostAddress();
|
String ip = s.getHost().getHostAddress();
|
||||||
if (ip == null || ip.contains(":")) return; // só IPv4: o servidor ouve em 0.0.0.0
|
if (ip == null || ip.contains(":")) return; // só IPv4: o servidor ouve em 0.0.0.0
|
||||||
terminar("http://" + ip + ":" + s.getPort());
|
fimDoAnuncio("http://" + ip + ":" + s.getPort());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -75,19 +95,93 @@ final class Descoberta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void terminar(final String endereco) {
|
/** Fim da busca por anúncio: achou responde; não achou passa para a varredura. */
|
||||||
|
private void fimDoAnuncio(final String endereco) {
|
||||||
principal.post(new Runnable() {
|
principal.post(new Runnable() {
|
||||||
@Override public void run() {
|
@Override public void run() {
|
||||||
if (ouvinte == null) return; // já respondeu
|
if (ouvinte == null) return; // já terminou
|
||||||
try { nsd.stopServiceDiscovery(ouvinte); } catch (Exception e) { /* já parou */ }
|
try { nsd.stopServiceDiscovery(ouvinte); } catch (Exception e) { /* já parou */ }
|
||||||
ouvinte = null;
|
ouvinte = null;
|
||||||
Resposta r = resposta;
|
if (endereco != null) { Log.i(TAG, "placa achada pelo anúncio: " + endereco); responder(endereco); }
|
||||||
resposta = null;
|
else semAnuncio();
|
||||||
Log.i(TAG, endereco == null ? "placa não encontrada na rede" : "placa encontrada: " + endereco);
|
|
||||||
if (r != null) r.achou(endereco);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void parar() { terminar(null); }
|
private void semAnuncio() {
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
final String achado = varrer();
|
||||||
|
Log.i(TAG, achado == null ? "placa não encontrada na rede" : "placa achada na varredura: " + achado);
|
||||||
|
principal.post(new Runnable() { @Override public void run() { responder(achado); } });
|
||||||
|
}
|
||||||
|
}, "procura-placa").start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void responder(String endereco) {
|
||||||
|
ocupada = false;
|
||||||
|
Resposta r = resposta;
|
||||||
|
resposta = null;
|
||||||
|
if (r != null) r.achou(endereco);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pergunta a cada endereço da rede /24 do celular (em paralelo) quem é o servidor do Carinha. */
|
||||||
|
private static String varrer() {
|
||||||
|
final List<String> alvos = new ArrayList<String>();
|
||||||
|
for (InetAddress meu : enderecosLocais()) {
|
||||||
|
byte[] b = meu.getAddress();
|
||||||
|
String prefixo = (b[0] & 255) + "." + (b[1] & 255) + "." + (b[2] & 255) + ".";
|
||||||
|
for (int i = 1; i < 255; i++) {
|
||||||
|
if (i != (b[3] & 255)) alvos.add(prefixo + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alvos.isEmpty()) return null;
|
||||||
|
final AtomicReference<String> achado = new AtomicReference<String>();
|
||||||
|
ExecutorService fios = Executors.newFixedThreadPool(32);
|
||||||
|
for (final String ip : alvos) {
|
||||||
|
fios.execute(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
if (achado.get() == null && ehCarinha(ip)) achado.compareAndSet(null, "http://" + ip + ":" + PORTA);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fios.shutdown();
|
||||||
|
try { fios.awaitTermination(20, TimeUnit.SECONDS); } catch (InterruptedException e) { /* segue */ }
|
||||||
|
fios.shutdownNow();
|
||||||
|
return achado.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean ehCarinha(String ip) {
|
||||||
|
HttpURLConnection c = null;
|
||||||
|
try {
|
||||||
|
c = (HttpURLConnection) new URL("http://" + ip + ":" + PORTA + "/app/health").openConnection();
|
||||||
|
c.setConnectTimeout(600);
|
||||||
|
c.setReadTimeout(1500);
|
||||||
|
if (c.getResponseCode() != 200) return false;
|
||||||
|
byte[] buf = new byte[256];
|
||||||
|
int n;
|
||||||
|
try (InputStream in = c.getInputStream()) { n = in.read(buf); }
|
||||||
|
return n > 0 && new String(buf, 0, n, "UTF-8").contains("carinha-pc");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (c != null) c.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IPv4 privados das redes do celular (Wi-Fi), sem loopback nem dados móveis de operadora. */
|
||||||
|
private static List<InetAddress> enderecosLocais() {
|
||||||
|
List<InetAddress> lista = new ArrayList<InetAddress>();
|
||||||
|
try {
|
||||||
|
for (NetworkInterface ni : Collections.list(NetworkInterface.getNetworkInterfaces())) {
|
||||||
|
if (!ni.isUp() || ni.isLoopback() || ni.getName().startsWith("rmnet")) continue;
|
||||||
|
for (InetAddress a : Collections.list(ni.getInetAddresses())) {
|
||||||
|
if (a instanceof Inet4Address && a.isSiteLocalAddress()) lista.add(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "sem endereços locais: " + e);
|
||||||
|
}
|
||||||
|
return lista;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user