feat(celular): sem a placa a carinha dorme e mostra placa ✕ celular

Sem servidor o app não finge mais que está tudo normal:
- o app guarda a última aparência e os ajustes vindos da placa (ponte
  CarinhaApp); a cópia do APK abre com eles;
- a carinha dorme e não acorda com nada; no canto direito aparece a
  placa ✕ celular animada com "sem conexão com a placa";
- o app tenta a placa a cada 10 s e volta sozinho; se a placa cai com
  a página aberta, em 15 s ela dorme e mostra o aviso;
- endereço padrão passa a ser a Orange Pi (192.168.1.35:8100).

Também no app (0.9.0):
- modo quiosque como dono do aparelho (Quiosque/Admin, menu "Modo
  quiosque"); a Samsung recusa com bootloader desbloqueado, vale no
  LineageOS;
- carinha como tela inicial (HOME);
- android/enxugar-s10.ps1 desativa 57 apps sem uso no S10 (-Reativar
  desfaz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-08 03:17:28 -03:00
co-authored by Claude Opus 5.5
parent f8d2ebf705
commit 43770c6d67
8 changed files with 620 additions and 29 deletions
@@ -85,7 +85,11 @@ public class MainActivity extends Activity {
private static final String PREFS = "carinha";
private static final String CHAVE = "servidor";
private static final String CHAVE_TOKEN = "token";
private static final String PADRAO = "http://192.168.1.15:8100";
private static final String PADRAO = "http://192.168.1.35:8100"; // a Orange Pi
private static final long ESPERA_SERVIDOR = 10000; // ms entre tentativas sem servidor
/** O que a pagina pode guardar no app (sobrevive sem servidor; a copia do APK le). */
private static final java.util.Set<String> LEMBRAVEIS =
new java.util.HashSet<String>(Arrays.asList("aparencia", "config"));
private static final String TAG = "CarinhaMic";
private static final String TAG_JS = "CarinhaJS"; // console.log da pagina
@@ -247,6 +251,7 @@ public class MainActivity extends Activity {
cfg.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}
web.addJavascriptInterface(new PonteApp(), "CarinhaApp");
web.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedError(WebView vista, WebResourceRequest pedido,
@@ -414,7 +419,8 @@ public class MainActivity extends Activity {
private void menu() {
final String[] opcoes = {"Ver a camera", "Voltar para a carinha",
(movimentoLigado ? "Desligar reacao ao movimento" : "Ligar reacao ao movimento"),
"Mudar o servidor", "Sensores acordado / dormindo", "Camera e qualidade do video", "Token do servidor"};
"Mudar o servidor", "Sensores acordado / dormindo", "Camera e qualidade do video", "Token do servidor",
"Modo quiosque"};
new AlertDialog.Builder(this)
.setTitle("Carinha")
.setItems(opcoes, new DialogInterface.OnClickListener() {
@@ -426,12 +432,55 @@ public class MainActivity extends Activity {
else if (i == 3) pedirServidor();
else if (i == 4) menuSensores();
else if (i == 5) menuVideo();
else pedirToken();
else if (i == 6) pedirToken();
else menuQuiosque();
}
})
.show();
}
/** Manutenção: soltar a tela para usar o Android, ou deixar de ser dono do aparelho. */
private void menuQuiosque() {
if (!Quiosque.dono(this)) {
new AlertDialog.Builder(this)
.setTitle("Modo quiosque desligado")
.setMessage("Para ligar, pelo PC:\nadb shell dpm set-device-owner br.carinha.rosto/.Admin")
.setPositiveButton("OK", null)
.show();
return;
}
final boolean pausado = Quiosque.pausado();
new AlertDialog.Builder(this)
.setTitle(pausado ? "Quiosque pausado" : "Quiosque ligado")
.setItems(new String[]{
pausado ? "Travar de novo" : "Pausar até reiniciar (abre o Android)",
"Desligar o modo quiosque de vez"}, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface d, int i) {
if (i == 0) {
if (pausado) { Quiosque.retomar(MainActivity.this); aviso("quiosque ligado"); }
else { Quiosque.pausar(MainActivity.this); aviso("quiosque pausado: Home e barra liberados até reiniciar"); }
} else confirmarRemoverQuiosque();
}
})
.show();
}
private void confirmarRemoverQuiosque() {
new AlertDialog.Builder(this)
.setTitle("Desligar o modo quiosque?")
.setMessage("A carinha deixa de ser dona do aparelho. Para voltar, será preciso o ADB de novo.")
.setPositiveButton("Desligar", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface d, int w) {
Quiosque.remover(MainActivity.this);
aviso("modo quiosque desligado");
}
})
.setNegativeButton("Cancelar", null)
.show();
}
/** Liga/desliga a reacao ao movimento (guardado nas preferencias). */
private void salvarAjuste(String chave, Object valor) {
try { JSONObject c = new JSONObject(); c.put(chave, valor); remoto.configurar(c);
@@ -500,14 +549,63 @@ public class MainActivity extends Activity {
Toast.makeText(this, texto, Toast.LENGTH_LONG).show();
}
/** Servidor fora do ar: mostra a carinha embutida no APK (sem controle remoto). */
/**
* Servidor fora do ar: a copia do APK mostra a carinha dormindo, com a ultima aparencia
* guardada, e o desenho "placa x celular". Enquanto isso o app tenta o servidor de novo.
*/
private void cairParaLocal(WebView vista) {
if (jaCaiuParaLocal) return;
jaCaiuParaLocal = true;
usandoLocal = true;
naCamera = false;
aviso("sem servidor " + servidor + " — usando o rosto local");
Log.i(TAG, "sem servidor " + servidor + ": rosto local dormindo, tentando de novo");
vista.loadUrl("file:///android_asset/carinha.html");
procurarServidor();
}
/** Sem servidor: a cada 10 s pergunta se ele voltou; voltou, abre o rosto dele de novo. */
private void procurarServidor() {
web.removeCallbacks(tentarServidor);
web.postDelayed(tentarServidor, ESPERA_SERVIDOR);
}
private final Runnable tentarServidor = new Runnable() {
@Override public void run() {
if (!usandoLocal) return;
if (!emPrimeiroPlano) { procurarServidor(); return; }
final String alvo = servidor;
new Thread(new Runnable() {
@Override public void run() {
final boolean voltou = servidorResponde(alvo);
runOnUiThread(new Runnable() {
@Override public void run() {
if (!usandoLocal || !alvo.equals(servidor)) return;
if (!voltou) { procurarServidor(); return; }
Log.i(TAG, "servidor voltou: " + servidor);
usandoLocal = false;
jaCaiuParaLocal = false;
abrirRosto();
}
});
}
}, "procura-servidor").start();
}
};
/** Qualquer resposta HTTP (até 401 de token) quer dizer que o servidor está no ar. */
private static boolean servidorResponde(String endereco) {
java.net.HttpURLConnection c = null;
try {
c = (java.net.HttpURLConnection) new java.net.URL(endereco + "/config").openConnection();
c.setConnectTimeout(2500);
c.setReadTimeout(2500);
c.setUseCaches(false);
return c.getResponseCode() > 0;
} catch (Exception e) {
return false;
} finally {
if (c != null) c.disconnect();
}
}
private void pedirServidor() {
@@ -742,12 +840,14 @@ public class MainActivity extends Activity {
rtcWeb.onResume();
web.evaluateJavascript("window.carinhaAtiva=true", null);
telaCheia();
Quiosque.aplicar(this);
// voltou do segundo plano: atualiza o rosto — mas sem expulsar quem
// estava vendo a pagina da camera
if (jaRecarregou && !naCamera) {
abrirRosto();
}
jaRecarregou = true;
if (usandoLocal) procurarServidor();
pedirPermissoes(); // religa o microfone (onPause desligou)
if (!naCamera) iniciarMovimento(); // e volta a enxergar o movimento
}
@@ -1106,6 +1206,20 @@ public class MainActivity extends Activity {
});
}
/** window.CarinhaApp: guarda a ultima aparencia/ajustes vindos do servidor para a copia do APK. */
private final class PonteApp {
@JavascriptInterface public void guardar(String chave, String json) {
if (!LEMBRAVEIS.contains(chave) || json == null || json.length() > 50000) return;
try { new JSONObject(json); } catch (Exception e) { return; }
getSharedPreferences(PREFS, MODE_PRIVATE).edit().putString("ultimo_" + chave, json).apply();
}
@JavascriptInterface public String lembrado(String chave) {
if (!LEMBRAVEIS.contains(chave)) return null;
return getSharedPreferences(PREFS, MODE_PRIVATE).getString("ultimo_" + chave, null);
}
@JavascriptInterface public String servidor() { return servidor; }
}
private final class PonteRTC {
/** Foto do vídeo ao vivo (data:image/jpeg;base64,...) para o histórico. */
@JavascriptInterface public void foto(final String dataUrl) {