diff --git a/android/app/AndroidManifest.xml b/android/app/AndroidManifest.xml index 222852d..16bc51b 100644 --- a/android/app/AndroidManifest.xml +++ b/android/app/AndroidManifest.xml @@ -1,14 +1,16 @@ + android:versionCode="9" + android:versionName="0.8.1"> + + diff --git a/android/app/assets/carinha.html b/android/app/assets/carinha.html index 1b54384..ea3365a 100644 --- a/android/app/assets/carinha.html +++ b/android/app/assets/carinha.html @@ -865,6 +865,7 @@ async function sincronizarApp() { centro: [MOV.cx, MOV.cy], config: configRecebida, brilho: window.carinhaBrilho, bateria: window.carinhaBateria, + aparelho: window.carinhaAparelho, video: window.carinhaVideo, }) }); } diff --git a/android/app/java/br/carinha/rosto/MainActivity.java b/android/app/java/br/carinha/rosto/MainActivity.java index 7971eb5..f99ce25 100644 --- a/android/app/java/br/carinha/rosto/MainActivity.java +++ b/android/app/java/br/carinha/rosto/MainActivity.java @@ -1021,8 +1021,52 @@ public class MainActivity extends Activity { return String.format(java.util.Locale.US, "%.3f", v); } + private String aparelhoJson = "null"; + private long aparelhoEm = 0; + + /** IP, modelo, Android, versão do app e sinal do Wi-Fi; recalculado a cada 10 s. */ + private String infoAparelho() { + long agora = System.currentTimeMillis(); + if (agora - aparelhoEm < 10000) return aparelhoJson; + aparelhoEm = agora; + try { + JSONObject a = new JSONObject(); + a.put("ip", ipLocal()); + a.put("modelo", (Build.MANUFACTURER + " " + Build.MODEL).trim()); + a.put("android", Build.VERSION.RELEASE); + try { a.put("app", getPackageManager().getPackageInfo(getPackageName(), 0).versionName); } + catch (Exception e) { a.put("app", JSONObject.NULL); } + try { + android.net.wifi.WifiManager wifi = (android.net.wifi.WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE); + android.net.wifi.WifiInfo info = wifi == null ? null : wifi.getConnectionInfo(); + if (info != null && info.getNetworkId() != -1) { + a.put("wifi", android.net.wifi.WifiManager.calculateSignalLevel(info.getRssi(), 5)); + a.put("rssi", info.getRssi()); + } + } catch (Exception e) { /* sem permissão ou sem Wi-Fi */ } + aparelhoJson = a.toString(); + } catch (Exception e) { + aparelhoJson = "null"; + } + return aparelhoJson; + } + + /** Primeiro IPv4 privado da rede (Wi-Fi), sem precisar de permissão extra. */ + private static String ipLocal() { + try { + for (java.net.NetworkInterface n : java.util.Collections.list(java.net.NetworkInterface.getNetworkInterfaces())) { + if (!n.isUp() || n.isLoopback()) continue; + for (java.net.InetAddress e : java.util.Collections.list(n.getInetAddresses())) { + if (e instanceof java.net.Inet4Address && e.isSiteLocalAddress()) return e.getHostAddress(); + } + } + } catch (Exception e) { /* sem rede */ } + return null; + } + private void atualizarBrilho() { if (naCamera) return; + web.evaluateJavascript("window.carinhaAparelho=" + infoAparelho(), null); Intent bateria = registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); if (bateria != null) { int level = bateria.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); diff --git a/carinha.html b/carinha.html index 1b54384..ea3365a 100644 --- a/carinha.html +++ b/carinha.html @@ -865,6 +865,7 @@ async function sincronizarApp() { centro: [MOV.cx, MOV.cy], config: configRecebida, brilho: window.carinhaBrilho, bateria: window.carinhaBateria, + aparelho: window.carinhaAparelho, video: window.carinhaVideo, }) }); } diff --git a/controle_pc.py b/controle_pc.py index d5d4ed3..0a0bf26 100644 --- a/controle_pc.py +++ b/controle_pc.py @@ -1,4 +1,5 @@ """Configuração persistente e telemetria do celular; sinalização do vídeo WebRTC.""" +import ipaddress import json import math from pathlib import Path @@ -51,6 +52,16 @@ def validar(dados): return dados +def ip_informado(dados): + """IP que o próprio app informa (aparelho.ip); o endereço da conexão pode ser de um proxy.""" + aparelho = dados.get('aparelho') + ip = aparelho.get('ip') if isinstance(aparelho, dict) else None + try: + return str(ipaddress.IPv4Address(ip)) if isinstance(ip, str) else None + except ValueError: + return None + + class ControlePC: def __init__(self, arquivo): self.arquivo = Path(arquivo) @@ -134,7 +145,7 @@ class ControlePC: raise ValueError('Estado deve ser objeto JSON') with self.lock: self.estado = dados - self.celular_ip = h.client_address[0] + self.celular_ip = ip_informado(dados) or h.client_address[0] if self.recarga["id"] and str(dados.get("tela_atualizada", "")) == self.recarga["id"]: self.recarga["confirmada"] = True self.status_at = time.monotonic() diff --git a/tests/test_controle_pc.py b/tests/test_controle_pc.py index 8eb78ec..c390e9b 100644 --- a/tests/test_controle_pc.py +++ b/tests/test_controle_pc.py @@ -86,6 +86,12 @@ class ControleTest(unittest.TestCase): self.assertTrue(json.loads(body)['online']) self.assertEqual(json.loads(body)['estado']['rosto'], 'miudo') + def test_phone_reports_its_own_ip(self): + self.request('/app/status', {'aparelho': {'ip': '192.168.1.16', 'modelo': 'samsung SM-G973F'}}) + self.assertEqual(json.loads(self.request('/app/status')[1])['celular_ip'], '192.168.1.16') + self.request('/app/status', {'aparelho': {'ip': 'não é ip'}}) + self.assertEqual(json.loads(self.request('/app/status')[1])['celular_ip'], '127.0.0.1') + def test_legacy_camera_and_adb_routes_are_gone(self): self.request('/app/config', {'preview': True}) self.assertEqual(self.request('/camera/frame', b'\xff\xd8x\xff\xd9', 'image/jpeg')[0], 404) diff --git a/tests/test_painel_web_browser.py b/tests/test_painel_web_browser.py index 00d7e25..8be0c43 100644 --- a/tests/test_painel_web_browser.py +++ b/tests/test_painel_web_browser.py @@ -74,8 +74,13 @@ def testar(base, server, capturas): # 2. telemetria do celular chega por SSE page.request.post(base + '/app/status', headers=auth, data={'dormindo': False, 'bateria': 0.8, - 'microfone_ativo': True, 'microfone': 0.3}) + 'microfone_ativo': True, 'microfone': 0.3, + 'aparelho': {'ip': '192.168.1.16', 'modelo': 'samsung SM-G973F', 'android': '12', + 'app': '0.8.1', 'wifi': 3, 'rssi': -58}}) expect(page.get_by_text('Celular online · 80%')).to_be_visible(timeout=5000) + # Dados que o próprio APK informa aparecem no cartão do celular. + expect(page.get_by_text('samsung SM-G973F')).to_be_visible() + expect(page.get_by_text('192.168.1.16')).to_be_visible() # 3. gesto pelo dock kiosk.FILA.limpar() diff --git a/web/src/lib/componentes/Celular.svelte b/web/src/lib/componentes/Celular.svelte new file mode 100644 index 0000000..43cd9f4 --- /dev/null +++ b/web/src/lib/componentes/Celular.svelte @@ -0,0 +1,181 @@ + + +
+
+ +
+ {ap?.modelo || 'Celular do rosto'} + + {#if estado.celularOnline} + {[ap?.android && 'Android ' + ap.android, ap?.app && 'app ' + ap.app].filter(Boolean).join(' · ') || 'online'} + {:else} + offline{visto !== null ? ` · visto há ${Math.round(visto)} s` : ''} + {/if} + +
+ {estado.celularOnline ? 'Online' : 'Offline'} +
+ + {#if estado.celularOnline} +
+
+
Bateria
+
+ + {bateria !== null ? bateria + '%' : '—'} + {#if carregando}carregando{/if} +
+
+
+
Wi-Fi
+
+ {#if typeof ap?.wifi === 'number'} + + {ap.wifi} de 4 + {#if ap.rssi}{ap.rssi} dBm{/if} + {:else} + sem dado + {/if} +
+
+
+
IP
+
{ip}
+
+
+
Rosto
+
+ {#if s.dormindo} Dormindo{:else} Acordado{/if} +
+
+
+ {:else} +

Abra o app Carinha no celular e confira se ele aponta para este servidor (toque longo › Mudar o servidor).

+ {/if} +
+ + diff --git a/web/src/lib/componentes/Grupo.svelte b/web/src/lib/componentes/Grupo.svelte new file mode 100644 index 0000000..8594b3d --- /dev/null +++ b/web/src/lib/componentes/Grupo.svelte @@ -0,0 +1,38 @@ + + +
+ {#if titulo}

{titulo}

{/if} +
{@render children()}
+ {#if rodape}

{rodape}

{/if} +
+ + diff --git a/web/src/lib/componentes/Interruptor.svelte b/web/src/lib/componentes/Interruptor.svelte new file mode 100644 index 0000000..fc177ad --- /dev/null +++ b/web/src/lib/componentes/Interruptor.svelte @@ -0,0 +1,62 @@ + + + + + diff --git a/web/src/lib/componentes/Linha.svelte b/web/src/lib/componentes/Linha.svelte new file mode 100644 index 0000000..4075ea9 --- /dev/null +++ b/web/src/lib/componentes/Linha.svelte @@ -0,0 +1,141 @@ + + +{#snippet corpo()} + {#if icone} + {@const Icone = icone} + + {/if} + + {rotulo} + {#if detalhe}{detalhe}{/if} + + {#if acessorio}{@render acessorio()}{/if} + {#if clicavel}