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>
188 lines
7.6 KiB
Java
188 lines
7.6 KiB
Java
package br.carinha.rosto;
|
|
|
|
import android.content.Context;
|
|
import android.net.nsd.NsdManager;
|
|
import android.net.nsd.NsdServiceInfo;
|
|
import android.os.Handler;
|
|
import android.os.Looper;
|
|
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 quando o endereço salvo não responde, para o app se reconectar sem
|
|
* ninguém digitar endereço:
|
|
* 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 {
|
|
interface Resposta { void achou(String endereco); }
|
|
|
|
private static final String TAG = "CarinhaDescoberta";
|
|
private static final String TIPO = "_carinha._tcp.";
|
|
private static final long LIMITE_ANUNCIO = 6000;
|
|
private static final int PORTA = 8100;
|
|
|
|
private final NsdManager nsd;
|
|
private final Handler principal = new Handler(Looper.getMainLooper());
|
|
private NsdManager.DiscoveryListener ouvinte;
|
|
private Resposta resposta;
|
|
private boolean ocupada;
|
|
|
|
Descoberta(Context c) {
|
|
nsd = (NsdManager) c.getApplicationContext().getSystemService(Context.NSD_SERVICE);
|
|
}
|
|
|
|
boolean procurando() { return ocupada; }
|
|
|
|
void procurar(Resposta r) {
|
|
if (ocupada) return;
|
|
ocupada = true;
|
|
resposta = r;
|
|
if (nsd == null) { semAnuncio(); return; }
|
|
ouvinte = new NsdManager.DiscoveryListener() {
|
|
@Override public void onDiscoveryStarted(String tipo) { }
|
|
@Override public void onDiscoveryStopped(String tipo) { }
|
|
@Override public void onStartDiscoveryFailed(String tipo, int erro) { fimDoAnuncio(null); }
|
|
@Override public void onStopDiscoveryFailed(String tipo, int erro) { }
|
|
@Override public void onServiceLost(NsdServiceInfo s) { }
|
|
@Override public void onServiceFound(NsdServiceInfo s) {
|
|
if (s.getServiceType().startsWith("_carinha._tcp")) resolver(s);
|
|
}
|
|
};
|
|
try {
|
|
nsd.discoverServices(TIPO, NsdManager.PROTOCOL_DNS_SD, ouvinte);
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "busca por anúncio falhou: " + e);
|
|
ouvinte = null;
|
|
semAnuncio();
|
|
return;
|
|
}
|
|
principal.postDelayed(new Runnable() {
|
|
@Override public void run() { fimDoAnuncio(null); }
|
|
}, LIMITE_ANUNCIO);
|
|
}
|
|
|
|
@SuppressWarnings("deprecation")
|
|
private void resolver(NsdServiceInfo s) {
|
|
try {
|
|
nsd.resolveService(s, new NsdManager.ResolveListener() {
|
|
@Override public void onResolveFailed(NsdServiceInfo s, int erro) { Log.w(TAG, "não resolveu: " + erro); }
|
|
@Override public void onServiceResolved(NsdServiceInfo s) {
|
|
if (s.getHost() == null || s.getHost().isLoopbackAddress()) return;
|
|
String ip = s.getHost().getHostAddress();
|
|
if (ip == null || ip.contains(":")) return; // só IPv4: o servidor ouve em 0.0.0.0
|
|
fimDoAnuncio("http://" + ip + ":" + s.getPort());
|
|
}
|
|
});
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "resolver falhou: " + e);
|
|
}
|
|
}
|
|
|
|
/** 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() {
|
|
@Override public void run() {
|
|
if (ouvinte == null) return; // já terminou
|
|
try { nsd.stopServiceDiscovery(ouvinte); } catch (Exception e) { /* já parou */ }
|
|
ouvinte = null;
|
|
if (endereco != null) { Log.i(TAG, "placa achada pelo anúncio: " + endereco); responder(endereco); }
|
|
else semAnuncio();
|
|
}
|
|
});
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|