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 alvos = new ArrayList(); 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 achado = new AtomicReference(); 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 enderecosLocais() { List lista = new ArrayList(); 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; } }