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:
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="br.carinha.rosto"
|
package="br.carinha.rosto"
|
||||||
android:versionCode="11"
|
android:versionCode="12"
|
||||||
android:versionName="0.8.3">
|
android:versionName="0.9.0">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="34" />
|
||||||
|
|
||||||
@@ -36,6 +36,23 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- no modo quiosque a carinha é a tela inicial do aparelho -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.HOME" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- dono do aparelho (modo quiosque): adb shell dpm set-device-owner br.carinha.rosto/.Admin -->
|
||||||
|
<receiver
|
||||||
|
android:name=".Admin"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.BIND_DEVICE_ADMIN">
|
||||||
|
<meta-data android:name="android.app.device_admin" android:resource="@xml/admin" />
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
+156
-11
@@ -20,6 +20,52 @@
|
|||||||
@keyframes aparecerFigura{from{opacity:0}to{opacity:1}}
|
@keyframes aparecerFigura{from{opacity:0}to{opacity:1}}
|
||||||
@media(prefers-reduced-motion:reduce){.iconeOlho,.figurinhaDinamica{animation:none}}
|
@media(prefers-reduced-motion:reduce){.iconeOlho,.figurinhaDinamica{animation:none}}
|
||||||
#fps{position:fixed;left:4px;top:2px;color:#2b3a4a;font:12px monospace;display:none}
|
#fps{position:fixed;left:4px;top:2px;color:#2b3a4a;font:12px monospace;display:none}
|
||||||
|
/* sem a placa: placa ✕ celular na cor dos olhos. Deitado, o rosto dormindo fica onde está e o
|
||||||
|
desenho fica em pé no canto direito; em pé, o rosto sobe e o desenho vai embaixo. */
|
||||||
|
#semServidor{position:fixed;right:2.5vw;top:50%;transform:translateY(-50%);width:min(24vw,42vh);
|
||||||
|
z-index:5;pointer-events:none;text-align:center;opacity:.82;font-family:system-ui,sans-serif;
|
||||||
|
animation:semServidorEntra 1.2s ease-out}
|
||||||
|
#semServidor svg{display:block;margin:0 auto;overflow:visible;filter:drop-shadow(0 0 3px currentColor)}
|
||||||
|
#semServidor svg.horizontal{display:none}
|
||||||
|
#semServidor svg.vertical{height:58vh;width:auto;max-width:100%}
|
||||||
|
#semServidor svg.vertical .olhinhos{animation-name:semServidorOlharCima}
|
||||||
|
@media (orientation:portrait){
|
||||||
|
body.semServidor #tela,body.semServidor #rostoMotor{transform:translateY(-20%) scale(.8)}
|
||||||
|
#semServidor{right:auto;top:auto;left:50%;bottom:3.5vh;transform:translateX(-50%);width:min(80vw,400px)}
|
||||||
|
#semServidor svg.horizontal{display:block;width:100%;height:auto}
|
||||||
|
#semServidor svg.vertical{display:none}
|
||||||
|
}
|
||||||
|
#semServidor .titulo{text-wrap:balance;font-weight:600;font-size:clamp(12px,3.2vh,20px);margin-top:.4em;letter-spacing:.02em}
|
||||||
|
#semServidor .sub{font-size:clamp(10px,2.4vh,15px);opacity:.6;margin-top:.25em;font-variant-numeric:tabular-nums}
|
||||||
|
#semServidor .placa rect,#semServidor .celular>rect{fill:none;stroke:currentColor;stroke-width:3}
|
||||||
|
#semServidor .chip{fill:currentColor;fill-opacity:.25}
|
||||||
|
#semServidor .pino{fill:currentColor;opacity:.7}
|
||||||
|
#semServidor .led{fill:#ff5a5a;animation:semServidorLed 1.6s steps(1) infinite}
|
||||||
|
#semServidor .cabo{stroke:currentColor;stroke-width:3;stroke-dasharray:5 6;opacity:.5;fill:none}
|
||||||
|
#semServidor .arco{fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:round;opacity:0;
|
||||||
|
animation:semServidorArco 2.4s ease-out infinite}
|
||||||
|
#semServidor .a2{animation-delay:.3s}
|
||||||
|
#semServidor .a3{animation-delay:.6s}
|
||||||
|
#semServidor .xis path{stroke:#ff5a5a;stroke-width:5;stroke-linecap:round;fill:none}
|
||||||
|
#semServidor .xis{transform-box:fill-box;transform-origin:center;animation:semServidorXis 2.4s ease-in-out infinite}
|
||||||
|
#semServidor .olhinhos{fill:currentColor;transform-box:fill-box;transform-origin:center;
|
||||||
|
animation:semServidorOlhar 5s ease-in-out infinite}
|
||||||
|
#semServidor .boquinha{fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round}
|
||||||
|
#semServidor .celular{transform-box:fill-box;transform-origin:50% 100%;animation:semServidorProcura 4.8s ease-in-out infinite}
|
||||||
|
#semServidor.cutucado .celular{animation:semServidorChacoalha .5s ease-in-out}
|
||||||
|
@keyframes semServidorEntra{from{opacity:0}to{opacity:.82}}
|
||||||
|
/* em pé no canto direito: o celularzinho olha para cima, para a placa */
|
||||||
|
@keyframes semServidorOlharCima{0%,30%{transform:translateY(-3px) scaleY(1)}34%{transform:translateY(-3px) scaleY(.1)}38%,60%{transform:translateY(-3px) scaleY(1)}
|
||||||
|
70%,90%{transform:translateY(1px) scaleY(1)}100%{transform:translateY(-3px)}}
|
||||||
|
@keyframes semServidorArco{0%{opacity:0}25%{opacity:.9}70%,100%{opacity:0}}
|
||||||
|
@keyframes semServidorXis{0%,55%,100%{transform:scale(1);opacity:.75}65%{transform:scale(1.25);opacity:1}80%{transform:scale(1);opacity:.85}}
|
||||||
|
@keyframes semServidorLed{0%{opacity:1}50%{opacity:.15}}
|
||||||
|
/* o celularzinho olha para a placa, olha de volta e pisca */
|
||||||
|
@keyframes semServidorOlhar{0%,30%{transform:translateX(-3px) scaleY(1)}34%{transform:translateX(-3px) scaleY(.1)}38%,60%{transform:translateX(-3px) scaleY(1)}
|
||||||
|
70%,90%{transform:translateX(1px) scaleY(1)}100%{transform:translateX(-3px)}}
|
||||||
|
@keyframes semServidorProcura{0%,100%{transform:rotate(0)}20%{transform:rotate(-5deg)}40%{transform:rotate(0)}}
|
||||||
|
@keyframes semServidorChacoalha{0%,100%{transform:rotate(0)}25%{transform:rotate(-9deg)}50%{transform:rotate(7deg)}75%{transform:rotate(-4deg)}}
|
||||||
|
@media(prefers-reduced-motion:reduce){#semServidor *,#semServidor{animation:none!important}#semServidor .arco{opacity:.6}}
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<svg id="tela" viewBox="0 0 480 320" preserveAspectRatio="xMidYMid meet">
|
<svg id="tela" viewBox="0 0 480 320" preserveAspectRatio="xMidYMid meet">
|
||||||
@@ -106,7 +152,7 @@ function cuidarDaTela() {
|
|||||||
const agora = performance.now();
|
const agora = performance.now();
|
||||||
if (!r.dormindo) SONO.desde = 0;
|
if (!r.dormindo) SONO.desde = 0;
|
||||||
else if (!SONO.desde) SONO.desde = agora;
|
else if (!SONO.desde) SONO.desde = agora;
|
||||||
const apagar = SONO.apagar && r.dormindo && SONO.desde && agora - SONO.desde > SONO.minutos * 60000;
|
const apagar = SONO.apagar && r.dormindo && !window.carinhaSemServidor && SONO.desde && agora - SONO.desde > SONO.minutos * 60000;
|
||||||
if (apagar !== SONO.apagada) {
|
if (apagar !== SONO.apagada) {
|
||||||
SONO.apagada = apagar;
|
SONO.apagada = apagar;
|
||||||
window.carinhaTelaApagada = apagar;
|
window.carinhaTelaApagada = apagar;
|
||||||
@@ -725,6 +771,11 @@ function trata(ordem) {
|
|||||||
r.dormindo = true;
|
r.dormindo = true;
|
||||||
atividade(null);
|
atividade(null);
|
||||||
} else if (ordem === "acordar") {
|
} else if (ordem === "acordar") {
|
||||||
|
if (window.carinhaSemServidor) { // sem a placa continua dormindo; o aviso chacoalha
|
||||||
|
const aviso = document.getElementById("semServidor");
|
||||||
|
if (aviso) { aviso.classList.remove("cutucado"); void aviso.offsetWidth; aviso.classList.add("cutucado"); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
r.dormindo = false; animar("surpreso");
|
r.dormindo = false; animar("surpreso");
|
||||||
} else if (ordem === "aleatorio") {
|
} else if (ordem === "aleatorio") {
|
||||||
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
|
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
|
||||||
@@ -777,27 +828,44 @@ async function buscarOrdens() {
|
|||||||
try {
|
try {
|
||||||
const resp = await fetch("fila"+(par.get("aparelho")==="1"?"?aparelho=1":""), { cache: "no-store" });
|
const resp = await fetch("fila"+(par.get("aparelho")==="1"?"?aparelho=1":""), { cache: "no-store" });
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
|
conexaoOk();
|
||||||
const lista = await resp.json();
|
const lista = await resp.json();
|
||||||
for (const nome of lista) trata(nome);
|
for (const nome of lista) trata(nome);
|
||||||
} catch (e) { /* sem servidor (file://) — ignora */ }
|
} catch (e) { /* sem servidor (file://) — ignora */ }
|
||||||
}
|
}
|
||||||
setInterval(buscarOrdens, 500);
|
setInterval(buscarOrdens, 500);
|
||||||
|
|
||||||
|
// O app (window.CarinhaApp) guarda a ultima aparencia e os ajustes vindos do servidor:
|
||||||
|
// a copia do APK (file://) nao enxerga o armazenamento da pagina do servidor.
|
||||||
|
const LOCAL = location.protocol === "file:";
|
||||||
|
function lembrar(chave, valor) {
|
||||||
|
try { if (window.CarinhaApp && !LOCAL) window.CarinhaApp.guardar(chave, JSON.stringify(valor)); } catch (e) {}
|
||||||
|
}
|
||||||
|
function lembrado(chave) {
|
||||||
|
try { const s = window.CarinhaApp && window.CarinhaApp.lembrado(chave); return s ? JSON.parse(s) : null; } catch (e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
// A aparencia guardada no servidor (rosto + cor) e aplicada ao carregar: o
|
// A aparencia guardada no servidor (rosto + cor) e aplicada ao carregar: o
|
||||||
// painel de controle personaliza e a carinha ja abre do jeito escolhido. Sem
|
// painel de controle personaliza e a carinha ja abre do jeito escolhido. Sem
|
||||||
// servidor (file://) isso falha calado e vale o padrao.
|
// servidor vale a ultima que o app guardou (ou o padrao).
|
||||||
async function aplicarAparencia() {
|
async function aplicarAparencia() {
|
||||||
try {
|
let c = null;
|
||||||
const resp = await fetch("config", { cache: "no-store" });
|
if (!LOCAL) {
|
||||||
if (!resp.ok) return;
|
try {
|
||||||
const c = await resp.json();
|
const resp = await fetch("config", { cache: "no-store" });
|
||||||
if (c.rosto && ROSTOS[c.rosto]) trocarRosto(c.rosto);
|
if (resp.ok) c = await resp.json();
|
||||||
if (FORMATOS.includes(c.formato)) FORMATO = c.formato;
|
} catch (e) { /* sem servidor */ }
|
||||||
if (Array.isArray(c.cor) && c.cor.length === 3 && c.cor.every((n) => !isNaN(n)))
|
}
|
||||||
{ CIANO.splice(0, 3, ...c.cor.map(Number)); r.cor = c.cor.map(Number); }
|
if (!c) c = lembrado("aparencia");
|
||||||
} catch (e) { /* sem servidor — segue com o padrao */ }
|
if (!c) return;
|
||||||
|
if (c.rosto && ROSTOS[c.rosto]) trocarRosto(c.rosto);
|
||||||
|
if (FORMATOS.includes(c.formato)) FORMATO = c.formato;
|
||||||
|
if (Array.isArray(c.cor) && c.cor.length === 3 && c.cor.every((n) => !isNaN(n)))
|
||||||
|
{ CIANO.splice(0, 3, ...c.cor.map(Number)); r.cor = c.cor.map(Number); }
|
||||||
}
|
}
|
||||||
setTimeout(aplicarAparencia, 200);
|
setTimeout(aplicarAparencia, 200);
|
||||||
|
// O painel troca rosto/cor por ordens: guarda a aparencia atual de tempos em tempos.
|
||||||
|
setInterval(() => { if (!window.carinhaSemServidor) lembrar("aparencia", { rosto: ROSTO_NOME, formato: FORMATO, cor: CIANO.slice() }); }, 15000);
|
||||||
// ?painel=1: prévia dentro do painel novo. Nada de polling; o painel repassa os eventos
|
// ?painel=1: prévia dentro do painel novo. Nada de polling; o painel repassa os eventos
|
||||||
// do servidor (SSE) por postMessage.
|
// do servidor (SSE) por postMessage.
|
||||||
const MODO_PAINEL = new URLSearchParams(location.search).get('painel') === '1';
|
const MODO_PAINEL = new URLSearchParams(location.search).get('painel') === '1';
|
||||||
@@ -1238,6 +1306,7 @@ function configurarApp(c) {
|
|||||||
MOV.cooldown = c.movcooldown; MOV.olharMax = c.movolhar;
|
MOV.cooldown = c.movcooldown; MOV.olharMax = c.movolhar;
|
||||||
if (!c.reagir) { MOV.olharX = MOV.olharY = 0; MOV.ultimoMov = 0; }
|
if (!c.reagir) { MOV.olharX = MOV.olharY = 0; MOV.ultimoMov = 0; }
|
||||||
configRecebida = c;
|
configRecebida = c;
|
||||||
|
lembrar('config', c);
|
||||||
if(!c.seguirRastreamento || !c.sensorCamera || !c.reagir) TRACK.alvo=null;
|
if(!c.seguirRastreamento || !c.sensorCamera || !c.reagir) TRACK.alvo=null;
|
||||||
}
|
}
|
||||||
let sincronizando = false;
|
let sincronizando = false;
|
||||||
@@ -1363,6 +1432,7 @@ function laco(t) {
|
|||||||
if (r.dormindo && window.MotorRosto && window.MotorRosto.atividadeAtual && window.MotorRosto.atividadeAtual()) atividade(null);
|
if (r.dormindo && window.MotorRosto && window.MotorRosto.atividadeAtual && window.MotorRosto.atividadeAtual()) atividade(null);
|
||||||
document.body.classList.toggle("atividade", !!ATIV && !ATIV.saindo);
|
document.body.classList.toggle("atividade", !!ATIV && !ATIV.saindo);
|
||||||
|
|
||||||
|
if (window.carinhaSemServidor && !r.dormindo) { r.fila = []; r.kf = null; r.dormindo = true; } // sem a placa: só dorme
|
||||||
const ocupado = congelado ? true : passo(dt);
|
const ocupado = congelado ? true : passo(dt);
|
||||||
if (!ocupado) {
|
if (!ocupado) {
|
||||||
descanso += dt;
|
descanso += dt;
|
||||||
@@ -1434,6 +1504,81 @@ setTimeout(relatar, 1500);
|
|||||||
setInterval(poll,1000);poll();
|
setInterval(poll,1000);poll();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- sem servidor
|
||||||
|
Sem a placa (copia do APK, ou o servidor caiu com a pagina aberta) a carinha dorme com a
|
||||||
|
ultima aparencia e ajustes guardados e mostra placa ✕ celular. Volta sozinha quando a placa
|
||||||
|
responde (o app recarrega a pagina do servidor; aberta pelo servidor, o SSE reconecta). */
|
||||||
|
let ultimoOk = performance.now();
|
||||||
|
const TOLERANCIA_SEM_SERVIDOR = 15000; // ms sem resposta antes de dormir
|
||||||
|
function conexaoOk() { ultimoOk = performance.now(); }
|
||||||
|
|
||||||
|
function mostrarSemServidor(ligar) {
|
||||||
|
let el = document.getElementById("semServidor");
|
||||||
|
document.body.classList.toggle("semServidor", ligar);
|
||||||
|
if (!ligar) { if (el) el.hidden = true; return; }
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement("div");
|
||||||
|
el.id = "semServidor";
|
||||||
|
// placa e celular pelo canto de cima à esquerda; o desenho vertical vai no canto direito com o celular deitado
|
||||||
|
const placa = (px, py) =>
|
||||||
|
'<g class="placa">' +
|
||||||
|
'<rect x="' + px + '" y="' + py + '" width="84" height="56" rx="6"/>' +
|
||||||
|
'<rect class="chip" x="' + (px + 29) + '" y="' + (py + 15) + '" width="26" height="26" rx="3"/>' +
|
||||||
|
[0,1,2,3,4,5,6,7].map((i) => '<circle class="pino" cx="' + (px + 11 + i * 9) + '" cy="' + (py + 7) + '" r="2"/>').join("") +
|
||||||
|
'<circle class="pino" cx="' + (px + 7) + '" cy="' + (py + 50) + '" r="2.4"/><circle class="pino" cx="' + (px + 77) + '" cy="' + (py + 50) + '" r="2.4"/>' +
|
||||||
|
'<circle class="led" cx="' + (px + 72) + '" cy="' + (py + 22) + '" r="2.6"/>' +
|
||||||
|
'</g>';
|
||||||
|
const celular = (cx, cy) =>
|
||||||
|
'<g class="celular">' +
|
||||||
|
'<rect x="' + cx + '" y="' + cy + '" width="40" height="76" rx="8"/>' +
|
||||||
|
'<g class="olhinhos"><ellipse cx="' + (cx + 13) + '" cy="' + (cy + 34) + '" rx="3.2" ry="4.2"/><ellipse cx="' + (cx + 27) + '" cy="' + (cy + 34) + '" rx="3.2" ry="4.2"/></g>' +
|
||||||
|
'<path class="boquinha" d="M' + (cx + 14) + ' ' + (cy + 48) + ' Q' + (cx + 20) + ' ' + (cy + 44) + ' ' + (cx + 26) + ' ' + (cy + 48) + '"/>' +
|
||||||
|
'</g>';
|
||||||
|
const xis = (x, y) => '<g class="xis"><path d="M' + (x - 10) + ' ' + (y - 10) + ' L' + (x + 10) + ' ' + (y + 10) + ' M' + (x + 10) + ' ' + (y - 10) + ' L' + (x - 10) + ' ' + (y + 10) + '"/></g>';
|
||||||
|
el.innerHTML =
|
||||||
|
'<svg class="horizontal" viewBox="0 0 300 104" aria-hidden="true">' + placa(18, 30) +
|
||||||
|
'<path class="cabo" d="M104 58 H146"/>' +
|
||||||
|
'<g class="arcos">' +
|
||||||
|
'<path class="arco a1" d="M212 50 A10 10 0 0 0 212 66"/>' +
|
||||||
|
'<path class="arco a2" d="M203 44 A18 18 0 0 0 203 72"/>' +
|
||||||
|
'<path class="arco a3" d="M194 38 A26 26 0 0 0 194 78"/>' +
|
||||||
|
'</g>' + xis(160, 58) + celular(220, 18) +
|
||||||
|
'</svg>' +
|
||||||
|
'<svg class="vertical" viewBox="0 0 120 262" aria-hidden="true">' + placa(18, 8) +
|
||||||
|
'<path class="cabo" d="M60 66 V108"/>' +
|
||||||
|
'<g class="arcos">' +
|
||||||
|
'<path class="arco a1" d="M52 172 A10 10 0 0 1 68 172"/>' +
|
||||||
|
'<path class="arco a2" d="M46 163 A18 18 0 0 1 74 163"/>' +
|
||||||
|
'<path class="arco a3" d="M40 154 A26 26 0 0 1 80 154"/>' +
|
||||||
|
'</g>' + xis(60, 122) + celular(40, 182) +
|
||||||
|
'</svg>' +
|
||||||
|
'<div class="titulo">sem conexão com a placa</div>' +
|
||||||
|
'<div class="sub"></div>';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
}
|
||||||
|
let alvo = "";
|
||||||
|
try { alvo = (window.CarinhaApp && window.CarinhaApp.servidor()) || (LOCAL ? "" : location.host); } catch (e) {}
|
||||||
|
el.querySelector(".sub").textContent = "tentando de novo" + (alvo ? " · " + String(alvo).replace(/^https?:\/\//, "") : "") + "…";
|
||||||
|
el.style.color = "rgb(" + CIANO.map(Math.round).join(",") + ")";
|
||||||
|
el.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checarServidor() {
|
||||||
|
if (MODO_PAINEL || par.get("espelho") === "1") return;
|
||||||
|
if (window.carinhaSSE) conexaoOk();
|
||||||
|
const fora = LOCAL || performance.now() - ultimoOk > TOLERANCIA_SEM_SERVIDOR;
|
||||||
|
if (window.carinhaSemServidor) mostrarSemServidor(true); // acompanha a cor guardada
|
||||||
|
if (fora === !!window.carinhaSemServidor) return;
|
||||||
|
window.carinhaSemServidor = fora;
|
||||||
|
console.log(fora ? "sem servidor: dormindo" : "servidor de volta");
|
||||||
|
mostrarSemServidor(fora);
|
||||||
|
if (fora) { atividade(null); r.fila = []; r.kf = null; r.dormindo = true; }
|
||||||
|
else trata("acordar");
|
||||||
|
}
|
||||||
|
// Copia do APK: os ultimos ajustes guardados (boca, sobrancelhas, sono...) valem também aqui.
|
||||||
|
setTimeout(() => { if (LOCAL) { const c = lembrado("config"); if (c) configurarApp(c); } checarServidor(); }, 300);
|
||||||
|
setInterval(checarServidor, 1000);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script src="/reacoes-render.js"></script>
|
<script src="/reacoes-render.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package br.carinha.rosto;
|
||||||
|
|
||||||
|
import android.app.admin.DeviceAdminReceiver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Administrador do aparelho. Vira "dono" (device owner) uma vez, pelo ADB:
|
||||||
|
* adb shell dpm set-device-owner br.carinha.rosto/.Admin
|
||||||
|
* A partir daí o app trava a tela nele mesmo (ver Quiosque).
|
||||||
|
*/
|
||||||
|
public final class Admin extends DeviceAdminReceiver { }
|
||||||
@@ -85,7 +85,11 @@ public class MainActivity extends Activity {
|
|||||||
private static final String PREFS = "carinha";
|
private static final String PREFS = "carinha";
|
||||||
private static final String CHAVE = "servidor";
|
private static final String CHAVE = "servidor";
|
||||||
private static final String CHAVE_TOKEN = "token";
|
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 = "CarinhaMic";
|
||||||
private static final String TAG_JS = "CarinhaJS"; // console.log da pagina
|
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);
|
cfg.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
web.addJavascriptInterface(new PonteApp(), "CarinhaApp");
|
||||||
web.setWebViewClient(new WebViewClient() {
|
web.setWebViewClient(new WebViewClient() {
|
||||||
@Override
|
@Override
|
||||||
public void onReceivedError(WebView vista, WebResourceRequest pedido,
|
public void onReceivedError(WebView vista, WebResourceRequest pedido,
|
||||||
@@ -414,7 +419,8 @@ public class MainActivity extends Activity {
|
|||||||
private void menu() {
|
private void menu() {
|
||||||
final String[] opcoes = {"Ver a camera", "Voltar para a carinha",
|
final String[] opcoes = {"Ver a camera", "Voltar para a carinha",
|
||||||
(movimentoLigado ? "Desligar reacao ao movimento" : "Ligar reacao ao movimento"),
|
(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)
|
new AlertDialog.Builder(this)
|
||||||
.setTitle("Carinha")
|
.setTitle("Carinha")
|
||||||
.setItems(opcoes, new DialogInterface.OnClickListener() {
|
.setItems(opcoes, new DialogInterface.OnClickListener() {
|
||||||
@@ -426,12 +432,55 @@ public class MainActivity extends Activity {
|
|||||||
else if (i == 3) pedirServidor();
|
else if (i == 3) pedirServidor();
|
||||||
else if (i == 4) menuSensores();
|
else if (i == 4) menuSensores();
|
||||||
else if (i == 5) menuVideo();
|
else if (i == 5) menuVideo();
|
||||||
else pedirToken();
|
else if (i == 6) pedirToken();
|
||||||
|
else menuQuiosque();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.show();
|
.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). */
|
/** Liga/desliga a reacao ao movimento (guardado nas preferencias). */
|
||||||
private void salvarAjuste(String chave, Object valor) {
|
private void salvarAjuste(String chave, Object valor) {
|
||||||
try { JSONObject c = new JSONObject(); c.put(chave, valor); remoto.configurar(c);
|
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();
|
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) {
|
private void cairParaLocal(WebView vista) {
|
||||||
if (jaCaiuParaLocal) return;
|
if (jaCaiuParaLocal) return;
|
||||||
jaCaiuParaLocal = true;
|
jaCaiuParaLocal = true;
|
||||||
usandoLocal = true;
|
usandoLocal = true;
|
||||||
naCamera = false;
|
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");
|
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() {
|
private void pedirServidor() {
|
||||||
@@ -742,12 +840,14 @@ public class MainActivity extends Activity {
|
|||||||
rtcWeb.onResume();
|
rtcWeb.onResume();
|
||||||
web.evaluateJavascript("window.carinhaAtiva=true", null);
|
web.evaluateJavascript("window.carinhaAtiva=true", null);
|
||||||
telaCheia();
|
telaCheia();
|
||||||
|
Quiosque.aplicar(this);
|
||||||
// voltou do segundo plano: atualiza o rosto — mas sem expulsar quem
|
// voltou do segundo plano: atualiza o rosto — mas sem expulsar quem
|
||||||
// estava vendo a pagina da camera
|
// estava vendo a pagina da camera
|
||||||
if (jaRecarregou && !naCamera) {
|
if (jaRecarregou && !naCamera) {
|
||||||
abrirRosto();
|
abrirRosto();
|
||||||
}
|
}
|
||||||
jaRecarregou = true;
|
jaRecarregou = true;
|
||||||
|
if (usandoLocal) procurarServidor();
|
||||||
pedirPermissoes(); // religa o microfone (onPause desligou)
|
pedirPermissoes(); // religa o microfone (onPause desligou)
|
||||||
if (!naCamera) iniciarMovimento(); // e volta a enxergar o movimento
|
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 {
|
private final class PonteRTC {
|
||||||
/** Foto do vídeo ao vivo (data:image/jpeg;base64,...) para o histórico. */
|
/** Foto do vídeo ao vivo (data:image/jpeg;base64,...) para o histórico. */
|
||||||
@JavascriptInterface public void foto(final String dataUrl) {
|
@JavascriptInterface public void foto(final String dataUrl) {
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package br.carinha.rosto;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
import android.app.admin.DevicePolicyManager;
|
||||||
|
import android.app.admin.SystemUpdatePolicy;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.os.BatteryManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modo quiosque: com o app como dono do aparelho, a carinha é a tela inicial,
|
||||||
|
* abre sozinha ao ligar e ninguém sai dela (sem barra de status, sem tela de bloqueio).
|
||||||
|
* Sem ser dono, tudo aqui é ignorado e o app funciona como antes.
|
||||||
|
*/
|
||||||
|
public final class Quiosque {
|
||||||
|
private static final String TAG = "CarinhaQuiosque";
|
||||||
|
/** Pausado pelo menu para manutenção; volta sozinho quando o celular reinicia. */
|
||||||
|
private static boolean pausado;
|
||||||
|
|
||||||
|
private Quiosque() { }
|
||||||
|
|
||||||
|
private static DevicePolicyManager dpm(Context c) {
|
||||||
|
return (DevicePolicyManager) c.getSystemService(Context.DEVICE_POLICY_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ComponentName admin(Context c) { return new ComponentName(c, Admin.class); }
|
||||||
|
|
||||||
|
public static boolean dono(Context c) {
|
||||||
|
DevicePolicyManager d = dpm(c);
|
||||||
|
return d != null && d.isDeviceOwnerApp(c.getPackageName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean pausado() { return pausado; }
|
||||||
|
|
||||||
|
/** Chamado no onResume: reaplica as travas (idempotente). */
|
||||||
|
public static void aplicar(Activity a) {
|
||||||
|
if (pausado || !dono(a)) return;
|
||||||
|
DevicePolicyManager d = dpm(a);
|
||||||
|
ComponentName adm = admin(a);
|
||||||
|
String pacote = a.getPackageName();
|
||||||
|
try {
|
||||||
|
d.setLockTaskPackages(adm, new String[]{pacote});
|
||||||
|
// a carinha é a tela inicial: botão Home e o fim do boot caem nela
|
||||||
|
IntentFilter inicio = new IntentFilter(Intent.ACTION_MAIN);
|
||||||
|
inicio.addCategory(Intent.CATEGORY_HOME);
|
||||||
|
inicio.addCategory(Intent.CATEGORY_DEFAULT);
|
||||||
|
d.addPersistentPreferredActivity(adm, inicio, new ComponentName(a, MainActivity.class));
|
||||||
|
// tela ligada enquanto estiver na tomada, USB ou carregador sem fio
|
||||||
|
d.setGlobalSetting(adm, Settings.Global.STAY_ON_WHILE_PLUGGED_IN, String.valueOf(
|
||||||
|
BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS));
|
||||||
|
if (Build.VERSION.SDK_INT >= 23) {
|
||||||
|
d.setKeyguardDisabled(adm, true);
|
||||||
|
d.setStatusBarDisabled(adm, true);
|
||||||
|
// atualização do sistema só de madrugada (3h–5h), nunca no meio do dia
|
||||||
|
d.setSystemUpdatePolicy(adm, SystemUpdatePolicy.createWindowedInstallPolicy(180, 300));
|
||||||
|
// câmera e microfone sem perguntar, inclusive depois de reinstalar o app
|
||||||
|
for (String p : new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO})
|
||||||
|
d.setPermissionGrantState(adm, pacote, p, DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED);
|
||||||
|
}
|
||||||
|
// o botão Ligar continua abrindo o menu de desligar/reiniciar
|
||||||
|
if (Build.VERSION.SDK_INT >= 28)
|
||||||
|
d.setLockTaskFeatures(adm, DevicePolicyManager.LOCK_TASK_FEATURE_GLOBAL_ACTIONS);
|
||||||
|
if (!travado(a)) a.startLockTask();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "aplicar: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Solta a tela até o próximo reinício, para mexer nas configurações do Android. */
|
||||||
|
public static void pausar(Activity a) {
|
||||||
|
if (!dono(a)) return;
|
||||||
|
pausado = true;
|
||||||
|
DevicePolicyManager d = dpm(a);
|
||||||
|
ComponentName adm = admin(a);
|
||||||
|
try {
|
||||||
|
if (travado(a)) a.stopLockTask();
|
||||||
|
d.clearPackagePersistentPreferredActivities(adm, a.getPackageName());
|
||||||
|
if (Build.VERSION.SDK_INT >= 23) {
|
||||||
|
d.setKeyguardDisabled(adm, false);
|
||||||
|
d.setStatusBarDisabled(adm, false);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "pausar: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Volta a travar sem precisar reiniciar. */
|
||||||
|
public static void retomar(Activity a) {
|
||||||
|
pausado = false;
|
||||||
|
aplicar(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Deixa de ser dono do aparelho de vez (para desinstalar ou trocar de app). */
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public static void remover(Activity a) {
|
||||||
|
if (!dono(a)) return;
|
||||||
|
pausar(a);
|
||||||
|
try {
|
||||||
|
dpm(a).clearDeviceOwnerApp(a.getPackageName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "remover: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean travado(Activity a) {
|
||||||
|
ActivityManager am = (ActivityManager) a.getSystemService(Context.ACTIVITY_SERVICE);
|
||||||
|
if (am == null) return false;
|
||||||
|
if (Build.VERSION.SDK_INT >= 23) return am.getLockTaskModeState() != ActivityManager.LOCK_TASK_MODE_NONE;
|
||||||
|
return am.isInLockTaskMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Como dono do aparelho, o quiosque não precisa de políticas de administrador antigas -->
|
||||||
|
<device-admin>
|
||||||
|
<uses-policies />
|
||||||
|
</device-admin>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Desativa no S10 os apps que não servem ao robô (reversível: .\enxugar-s10.ps1 -Reativar)
|
||||||
|
param([switch]$Reativar)
|
||||||
|
$adb = 'C:/Users/JullioPC/Documents/Open/orange-pi-zero3/android/tools/android-sdk/platform-tools/adb.exe'
|
||||||
|
$pacotes = @(
|
||||||
|
# Bixby
|
||||||
|
'com.samsung.android.bixby.agent', 'com.samsung.android.bixby.service', 'com.samsung.android.bixby.wakeup',
|
||||||
|
'com.samsung.systemui.bixby2', 'com.samsung.android.app.settings.bixby', 'com.samsung.android.visionintelligence',
|
||||||
|
'com.samsung.android.app.spage', 'com.samsung.android.app.routines',
|
||||||
|
# enfeites da Samsung
|
||||||
|
'com.samsung.android.app.tips', 'com.samsung.android.aremoji', 'com.samsung.android.ardrawing', 'com.samsung.android.arzone',
|
||||||
|
'com.samsung.android.livestickers', 'com.samsung.android.stickercenter', 'com.samsung.android.app.camera.sticker.facearavatar.preload',
|
||||||
|
'com.samsung.android.app.dressroom', 'com.samsung.android.app.ledbackcover', 'com.samsung.android.app.watchmanagerstub',
|
||||||
|
'com.samsung.android.themestore', 'com.samsung.android.app.aodservice', 'com.samsung.android.game.gamehome',
|
||||||
|
'com.samsung.android.game.gametools', 'com.samsung.android.app.reminder', 'com.samsung.android.forest',
|
||||||
|
'com.sec.android.app.vepreload', 'com.samsung.android.mateagent', 'com.samsung.android.beaconmanager',
|
||||||
|
'com.samsung.android.kidsinstaller',
|
||||||
|
# contas e loja Samsung
|
||||||
|
'com.sec.android.app.samsungapps', 'com.sec.android.app.billing', 'com.samsung.android.scloud',
|
||||||
|
'com.samsung.android.samsungpass', 'com.samsung.android.samsungpassautofill', 'com.samsung.android.spayfw',
|
||||||
|
'com.samsung.android.smartswitchassistant', 'com.sec.android.easyMover.Agent', 'com.samsung.android.easysetup',
|
||||||
|
'com.samsung.android.app.sharelive', 'com.sec.android.app.desktoplauncher', 'com.sec.android.desktopmode.uiservice',
|
||||||
|
'com.samsung.android.mdx', 'com.samsung.android.mdx.kit', 'com.samsung.android.mdx.quickboard',
|
||||||
|
# apps de uso pessoal
|
||||||
|
'com.samsung.android.messaging', 'com.samsung.android.calendar', 'com.google.android.gm', 'com.google.android.youtube',
|
||||||
|
'com.google.android.apps.maps', 'com.google.android.apps.tachyon', 'com.google.android.googlequicksearchbox',
|
||||||
|
# pré-instalados
|
||||||
|
'com.facebook.katana', 'com.facebook.services', 'com.facebook.system', 'com.facebook.appmanager',
|
||||||
|
'com.microsoft.skydrive', 'com.microsoft.appmanager', 'br.com.bemobi.appsclub.vivo'
|
||||||
|
)
|
||||||
|
$acao = if ($Reativar) { 'enable' } else { 'disable-user --user 0' }
|
||||||
|
$ok = 0; $falhas = @()
|
||||||
|
foreach ($p in $pacotes) {
|
||||||
|
$r = & $adb -d shell "pm $acao $p" 2>&1 | Out-String
|
||||||
|
if ($r -match 'new state') { $ok++ } else { $falhas += "$p -> $($r.Trim())" }
|
||||||
|
}
|
||||||
|
"$ok de $($pacotes.Count) " + $(if ($Reativar) { 'reativados' } else { 'desativados' })
|
||||||
|
$falhas
|
||||||
+156
-11
@@ -20,6 +20,52 @@
|
|||||||
@keyframes aparecerFigura{from{opacity:0}to{opacity:1}}
|
@keyframes aparecerFigura{from{opacity:0}to{opacity:1}}
|
||||||
@media(prefers-reduced-motion:reduce){.iconeOlho,.figurinhaDinamica{animation:none}}
|
@media(prefers-reduced-motion:reduce){.iconeOlho,.figurinhaDinamica{animation:none}}
|
||||||
#fps{position:fixed;left:4px;top:2px;color:#2b3a4a;font:12px monospace;display:none}
|
#fps{position:fixed;left:4px;top:2px;color:#2b3a4a;font:12px monospace;display:none}
|
||||||
|
/* sem a placa: placa ✕ celular na cor dos olhos. Deitado, o rosto dormindo fica onde está e o
|
||||||
|
desenho fica em pé no canto direito; em pé, o rosto sobe e o desenho vai embaixo. */
|
||||||
|
#semServidor{position:fixed;right:2.5vw;top:50%;transform:translateY(-50%);width:min(24vw,42vh);
|
||||||
|
z-index:5;pointer-events:none;text-align:center;opacity:.82;font-family:system-ui,sans-serif;
|
||||||
|
animation:semServidorEntra 1.2s ease-out}
|
||||||
|
#semServidor svg{display:block;margin:0 auto;overflow:visible;filter:drop-shadow(0 0 3px currentColor)}
|
||||||
|
#semServidor svg.horizontal{display:none}
|
||||||
|
#semServidor svg.vertical{height:58vh;width:auto;max-width:100%}
|
||||||
|
#semServidor svg.vertical .olhinhos{animation-name:semServidorOlharCima}
|
||||||
|
@media (orientation:portrait){
|
||||||
|
body.semServidor #tela,body.semServidor #rostoMotor{transform:translateY(-20%) scale(.8)}
|
||||||
|
#semServidor{right:auto;top:auto;left:50%;bottom:3.5vh;transform:translateX(-50%);width:min(80vw,400px)}
|
||||||
|
#semServidor svg.horizontal{display:block;width:100%;height:auto}
|
||||||
|
#semServidor svg.vertical{display:none}
|
||||||
|
}
|
||||||
|
#semServidor .titulo{text-wrap:balance;font-weight:600;font-size:clamp(12px,3.2vh,20px);margin-top:.4em;letter-spacing:.02em}
|
||||||
|
#semServidor .sub{font-size:clamp(10px,2.4vh,15px);opacity:.6;margin-top:.25em;font-variant-numeric:tabular-nums}
|
||||||
|
#semServidor .placa rect,#semServidor .celular>rect{fill:none;stroke:currentColor;stroke-width:3}
|
||||||
|
#semServidor .chip{fill:currentColor;fill-opacity:.25}
|
||||||
|
#semServidor .pino{fill:currentColor;opacity:.7}
|
||||||
|
#semServidor .led{fill:#ff5a5a;animation:semServidorLed 1.6s steps(1) infinite}
|
||||||
|
#semServidor .cabo{stroke:currentColor;stroke-width:3;stroke-dasharray:5 6;opacity:.5;fill:none}
|
||||||
|
#semServidor .arco{fill:none;stroke:currentColor;stroke-width:3;stroke-linecap:round;opacity:0;
|
||||||
|
animation:semServidorArco 2.4s ease-out infinite}
|
||||||
|
#semServidor .a2{animation-delay:.3s}
|
||||||
|
#semServidor .a3{animation-delay:.6s}
|
||||||
|
#semServidor .xis path{stroke:#ff5a5a;stroke-width:5;stroke-linecap:round;fill:none}
|
||||||
|
#semServidor .xis{transform-box:fill-box;transform-origin:center;animation:semServidorXis 2.4s ease-in-out infinite}
|
||||||
|
#semServidor .olhinhos{fill:currentColor;transform-box:fill-box;transform-origin:center;
|
||||||
|
animation:semServidorOlhar 5s ease-in-out infinite}
|
||||||
|
#semServidor .boquinha{fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round}
|
||||||
|
#semServidor .celular{transform-box:fill-box;transform-origin:50% 100%;animation:semServidorProcura 4.8s ease-in-out infinite}
|
||||||
|
#semServidor.cutucado .celular{animation:semServidorChacoalha .5s ease-in-out}
|
||||||
|
@keyframes semServidorEntra{from{opacity:0}to{opacity:.82}}
|
||||||
|
/* em pé no canto direito: o celularzinho olha para cima, para a placa */
|
||||||
|
@keyframes semServidorOlharCima{0%,30%{transform:translateY(-3px) scaleY(1)}34%{transform:translateY(-3px) scaleY(.1)}38%,60%{transform:translateY(-3px) scaleY(1)}
|
||||||
|
70%,90%{transform:translateY(1px) scaleY(1)}100%{transform:translateY(-3px)}}
|
||||||
|
@keyframes semServidorArco{0%{opacity:0}25%{opacity:.9}70%,100%{opacity:0}}
|
||||||
|
@keyframes semServidorXis{0%,55%,100%{transform:scale(1);opacity:.75}65%{transform:scale(1.25);opacity:1}80%{transform:scale(1);opacity:.85}}
|
||||||
|
@keyframes semServidorLed{0%{opacity:1}50%{opacity:.15}}
|
||||||
|
/* o celularzinho olha para a placa, olha de volta e pisca */
|
||||||
|
@keyframes semServidorOlhar{0%,30%{transform:translateX(-3px) scaleY(1)}34%{transform:translateX(-3px) scaleY(.1)}38%,60%{transform:translateX(-3px) scaleY(1)}
|
||||||
|
70%,90%{transform:translateX(1px) scaleY(1)}100%{transform:translateX(-3px)}}
|
||||||
|
@keyframes semServidorProcura{0%,100%{transform:rotate(0)}20%{transform:rotate(-5deg)}40%{transform:rotate(0)}}
|
||||||
|
@keyframes semServidorChacoalha{0%,100%{transform:rotate(0)}25%{transform:rotate(-9deg)}50%{transform:rotate(7deg)}75%{transform:rotate(-4deg)}}
|
||||||
|
@media(prefers-reduced-motion:reduce){#semServidor *,#semServidor{animation:none!important}#semServidor .arco{opacity:.6}}
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<svg id="tela" viewBox="0 0 480 320" preserveAspectRatio="xMidYMid meet">
|
<svg id="tela" viewBox="0 0 480 320" preserveAspectRatio="xMidYMid meet">
|
||||||
@@ -106,7 +152,7 @@ function cuidarDaTela() {
|
|||||||
const agora = performance.now();
|
const agora = performance.now();
|
||||||
if (!r.dormindo) SONO.desde = 0;
|
if (!r.dormindo) SONO.desde = 0;
|
||||||
else if (!SONO.desde) SONO.desde = agora;
|
else if (!SONO.desde) SONO.desde = agora;
|
||||||
const apagar = SONO.apagar && r.dormindo && SONO.desde && agora - SONO.desde > SONO.minutos * 60000;
|
const apagar = SONO.apagar && r.dormindo && !window.carinhaSemServidor && SONO.desde && agora - SONO.desde > SONO.minutos * 60000;
|
||||||
if (apagar !== SONO.apagada) {
|
if (apagar !== SONO.apagada) {
|
||||||
SONO.apagada = apagar;
|
SONO.apagada = apagar;
|
||||||
window.carinhaTelaApagada = apagar;
|
window.carinhaTelaApagada = apagar;
|
||||||
@@ -725,6 +771,11 @@ function trata(ordem) {
|
|||||||
r.dormindo = true;
|
r.dormindo = true;
|
||||||
atividade(null);
|
atividade(null);
|
||||||
} else if (ordem === "acordar") {
|
} else if (ordem === "acordar") {
|
||||||
|
if (window.carinhaSemServidor) { // sem a placa continua dormindo; o aviso chacoalha
|
||||||
|
const aviso = document.getElementById("semServidor");
|
||||||
|
if (aviso) { aviso.classList.remove("cutucado"); void aviso.offsetWidth; aviso.classList.add("cutucado"); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
r.dormindo = false; animar("surpreso");
|
r.dormindo = false; animar("surpreso");
|
||||||
} else if (ordem === "aleatorio") {
|
} else if (ordem === "aleatorio") {
|
||||||
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
|
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
|
||||||
@@ -777,27 +828,44 @@ async function buscarOrdens() {
|
|||||||
try {
|
try {
|
||||||
const resp = await fetch("fila"+(par.get("aparelho")==="1"?"?aparelho=1":""), { cache: "no-store" });
|
const resp = await fetch("fila"+(par.get("aparelho")==="1"?"?aparelho=1":""), { cache: "no-store" });
|
||||||
if (!resp.ok) return;
|
if (!resp.ok) return;
|
||||||
|
conexaoOk();
|
||||||
const lista = await resp.json();
|
const lista = await resp.json();
|
||||||
for (const nome of lista) trata(nome);
|
for (const nome of lista) trata(nome);
|
||||||
} catch (e) { /* sem servidor (file://) — ignora */ }
|
} catch (e) { /* sem servidor (file://) — ignora */ }
|
||||||
}
|
}
|
||||||
setInterval(buscarOrdens, 500);
|
setInterval(buscarOrdens, 500);
|
||||||
|
|
||||||
|
// O app (window.CarinhaApp) guarda a ultima aparencia e os ajustes vindos do servidor:
|
||||||
|
// a copia do APK (file://) nao enxerga o armazenamento da pagina do servidor.
|
||||||
|
const LOCAL = location.protocol === "file:";
|
||||||
|
function lembrar(chave, valor) {
|
||||||
|
try { if (window.CarinhaApp && !LOCAL) window.CarinhaApp.guardar(chave, JSON.stringify(valor)); } catch (e) {}
|
||||||
|
}
|
||||||
|
function lembrado(chave) {
|
||||||
|
try { const s = window.CarinhaApp && window.CarinhaApp.lembrado(chave); return s ? JSON.parse(s) : null; } catch (e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
// A aparencia guardada no servidor (rosto + cor) e aplicada ao carregar: o
|
// A aparencia guardada no servidor (rosto + cor) e aplicada ao carregar: o
|
||||||
// painel de controle personaliza e a carinha ja abre do jeito escolhido. Sem
|
// painel de controle personaliza e a carinha ja abre do jeito escolhido. Sem
|
||||||
// servidor (file://) isso falha calado e vale o padrao.
|
// servidor vale a ultima que o app guardou (ou o padrao).
|
||||||
async function aplicarAparencia() {
|
async function aplicarAparencia() {
|
||||||
try {
|
let c = null;
|
||||||
const resp = await fetch("config", { cache: "no-store" });
|
if (!LOCAL) {
|
||||||
if (!resp.ok) return;
|
try {
|
||||||
const c = await resp.json();
|
const resp = await fetch("config", { cache: "no-store" });
|
||||||
if (c.rosto && ROSTOS[c.rosto]) trocarRosto(c.rosto);
|
if (resp.ok) c = await resp.json();
|
||||||
if (FORMATOS.includes(c.formato)) FORMATO = c.formato;
|
} catch (e) { /* sem servidor */ }
|
||||||
if (Array.isArray(c.cor) && c.cor.length === 3 && c.cor.every((n) => !isNaN(n)))
|
}
|
||||||
{ CIANO.splice(0, 3, ...c.cor.map(Number)); r.cor = c.cor.map(Number); }
|
if (!c) c = lembrado("aparencia");
|
||||||
} catch (e) { /* sem servidor — segue com o padrao */ }
|
if (!c) return;
|
||||||
|
if (c.rosto && ROSTOS[c.rosto]) trocarRosto(c.rosto);
|
||||||
|
if (FORMATOS.includes(c.formato)) FORMATO = c.formato;
|
||||||
|
if (Array.isArray(c.cor) && c.cor.length === 3 && c.cor.every((n) => !isNaN(n)))
|
||||||
|
{ CIANO.splice(0, 3, ...c.cor.map(Number)); r.cor = c.cor.map(Number); }
|
||||||
}
|
}
|
||||||
setTimeout(aplicarAparencia, 200);
|
setTimeout(aplicarAparencia, 200);
|
||||||
|
// O painel troca rosto/cor por ordens: guarda a aparencia atual de tempos em tempos.
|
||||||
|
setInterval(() => { if (!window.carinhaSemServidor) lembrar("aparencia", { rosto: ROSTO_NOME, formato: FORMATO, cor: CIANO.slice() }); }, 15000);
|
||||||
// ?painel=1: prévia dentro do painel novo. Nada de polling; o painel repassa os eventos
|
// ?painel=1: prévia dentro do painel novo. Nada de polling; o painel repassa os eventos
|
||||||
// do servidor (SSE) por postMessage.
|
// do servidor (SSE) por postMessage.
|
||||||
const MODO_PAINEL = new URLSearchParams(location.search).get('painel') === '1';
|
const MODO_PAINEL = new URLSearchParams(location.search).get('painel') === '1';
|
||||||
@@ -1238,6 +1306,7 @@ function configurarApp(c) {
|
|||||||
MOV.cooldown = c.movcooldown; MOV.olharMax = c.movolhar;
|
MOV.cooldown = c.movcooldown; MOV.olharMax = c.movolhar;
|
||||||
if (!c.reagir) { MOV.olharX = MOV.olharY = 0; MOV.ultimoMov = 0; }
|
if (!c.reagir) { MOV.olharX = MOV.olharY = 0; MOV.ultimoMov = 0; }
|
||||||
configRecebida = c;
|
configRecebida = c;
|
||||||
|
lembrar('config', c);
|
||||||
if(!c.seguirRastreamento || !c.sensorCamera || !c.reagir) TRACK.alvo=null;
|
if(!c.seguirRastreamento || !c.sensorCamera || !c.reagir) TRACK.alvo=null;
|
||||||
}
|
}
|
||||||
let sincronizando = false;
|
let sincronizando = false;
|
||||||
@@ -1363,6 +1432,7 @@ function laco(t) {
|
|||||||
if (r.dormindo && window.MotorRosto && window.MotorRosto.atividadeAtual && window.MotorRosto.atividadeAtual()) atividade(null);
|
if (r.dormindo && window.MotorRosto && window.MotorRosto.atividadeAtual && window.MotorRosto.atividadeAtual()) atividade(null);
|
||||||
document.body.classList.toggle("atividade", !!ATIV && !ATIV.saindo);
|
document.body.classList.toggle("atividade", !!ATIV && !ATIV.saindo);
|
||||||
|
|
||||||
|
if (window.carinhaSemServidor && !r.dormindo) { r.fila = []; r.kf = null; r.dormindo = true; } // sem a placa: só dorme
|
||||||
const ocupado = congelado ? true : passo(dt);
|
const ocupado = congelado ? true : passo(dt);
|
||||||
if (!ocupado) {
|
if (!ocupado) {
|
||||||
descanso += dt;
|
descanso += dt;
|
||||||
@@ -1434,6 +1504,81 @@ setTimeout(relatar, 1500);
|
|||||||
setInterval(poll,1000);poll();
|
setInterval(poll,1000);poll();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- sem servidor
|
||||||
|
Sem a placa (copia do APK, ou o servidor caiu com a pagina aberta) a carinha dorme com a
|
||||||
|
ultima aparencia e ajustes guardados e mostra placa ✕ celular. Volta sozinha quando a placa
|
||||||
|
responde (o app recarrega a pagina do servidor; aberta pelo servidor, o SSE reconecta). */
|
||||||
|
let ultimoOk = performance.now();
|
||||||
|
const TOLERANCIA_SEM_SERVIDOR = 15000; // ms sem resposta antes de dormir
|
||||||
|
function conexaoOk() { ultimoOk = performance.now(); }
|
||||||
|
|
||||||
|
function mostrarSemServidor(ligar) {
|
||||||
|
let el = document.getElementById("semServidor");
|
||||||
|
document.body.classList.toggle("semServidor", ligar);
|
||||||
|
if (!ligar) { if (el) el.hidden = true; return; }
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement("div");
|
||||||
|
el.id = "semServidor";
|
||||||
|
// placa e celular pelo canto de cima à esquerda; o desenho vertical vai no canto direito com o celular deitado
|
||||||
|
const placa = (px, py) =>
|
||||||
|
'<g class="placa">' +
|
||||||
|
'<rect x="' + px + '" y="' + py + '" width="84" height="56" rx="6"/>' +
|
||||||
|
'<rect class="chip" x="' + (px + 29) + '" y="' + (py + 15) + '" width="26" height="26" rx="3"/>' +
|
||||||
|
[0,1,2,3,4,5,6,7].map((i) => '<circle class="pino" cx="' + (px + 11 + i * 9) + '" cy="' + (py + 7) + '" r="2"/>').join("") +
|
||||||
|
'<circle class="pino" cx="' + (px + 7) + '" cy="' + (py + 50) + '" r="2.4"/><circle class="pino" cx="' + (px + 77) + '" cy="' + (py + 50) + '" r="2.4"/>' +
|
||||||
|
'<circle class="led" cx="' + (px + 72) + '" cy="' + (py + 22) + '" r="2.6"/>' +
|
||||||
|
'</g>';
|
||||||
|
const celular = (cx, cy) =>
|
||||||
|
'<g class="celular">' +
|
||||||
|
'<rect x="' + cx + '" y="' + cy + '" width="40" height="76" rx="8"/>' +
|
||||||
|
'<g class="olhinhos"><ellipse cx="' + (cx + 13) + '" cy="' + (cy + 34) + '" rx="3.2" ry="4.2"/><ellipse cx="' + (cx + 27) + '" cy="' + (cy + 34) + '" rx="3.2" ry="4.2"/></g>' +
|
||||||
|
'<path class="boquinha" d="M' + (cx + 14) + ' ' + (cy + 48) + ' Q' + (cx + 20) + ' ' + (cy + 44) + ' ' + (cx + 26) + ' ' + (cy + 48) + '"/>' +
|
||||||
|
'</g>';
|
||||||
|
const xis = (x, y) => '<g class="xis"><path d="M' + (x - 10) + ' ' + (y - 10) + ' L' + (x + 10) + ' ' + (y + 10) + ' M' + (x + 10) + ' ' + (y - 10) + ' L' + (x - 10) + ' ' + (y + 10) + '"/></g>';
|
||||||
|
el.innerHTML =
|
||||||
|
'<svg class="horizontal" viewBox="0 0 300 104" aria-hidden="true">' + placa(18, 30) +
|
||||||
|
'<path class="cabo" d="M104 58 H146"/>' +
|
||||||
|
'<g class="arcos">' +
|
||||||
|
'<path class="arco a1" d="M212 50 A10 10 0 0 0 212 66"/>' +
|
||||||
|
'<path class="arco a2" d="M203 44 A18 18 0 0 0 203 72"/>' +
|
||||||
|
'<path class="arco a3" d="M194 38 A26 26 0 0 0 194 78"/>' +
|
||||||
|
'</g>' + xis(160, 58) + celular(220, 18) +
|
||||||
|
'</svg>' +
|
||||||
|
'<svg class="vertical" viewBox="0 0 120 262" aria-hidden="true">' + placa(18, 8) +
|
||||||
|
'<path class="cabo" d="M60 66 V108"/>' +
|
||||||
|
'<g class="arcos">' +
|
||||||
|
'<path class="arco a1" d="M52 172 A10 10 0 0 1 68 172"/>' +
|
||||||
|
'<path class="arco a2" d="M46 163 A18 18 0 0 1 74 163"/>' +
|
||||||
|
'<path class="arco a3" d="M40 154 A26 26 0 0 1 80 154"/>' +
|
||||||
|
'</g>' + xis(60, 122) + celular(40, 182) +
|
||||||
|
'</svg>' +
|
||||||
|
'<div class="titulo">sem conexão com a placa</div>' +
|
||||||
|
'<div class="sub"></div>';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
}
|
||||||
|
let alvo = "";
|
||||||
|
try { alvo = (window.CarinhaApp && window.CarinhaApp.servidor()) || (LOCAL ? "" : location.host); } catch (e) {}
|
||||||
|
el.querySelector(".sub").textContent = "tentando de novo" + (alvo ? " · " + String(alvo).replace(/^https?:\/\//, "") : "") + "…";
|
||||||
|
el.style.color = "rgb(" + CIANO.map(Math.round).join(",") + ")";
|
||||||
|
el.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checarServidor() {
|
||||||
|
if (MODO_PAINEL || par.get("espelho") === "1") return;
|
||||||
|
if (window.carinhaSSE) conexaoOk();
|
||||||
|
const fora = LOCAL || performance.now() - ultimoOk > TOLERANCIA_SEM_SERVIDOR;
|
||||||
|
if (window.carinhaSemServidor) mostrarSemServidor(true); // acompanha a cor guardada
|
||||||
|
if (fora === !!window.carinhaSemServidor) return;
|
||||||
|
window.carinhaSemServidor = fora;
|
||||||
|
console.log(fora ? "sem servidor: dormindo" : "servidor de volta");
|
||||||
|
mostrarSemServidor(fora);
|
||||||
|
if (fora) { atividade(null); r.fila = []; r.kf = null; r.dormindo = true; }
|
||||||
|
else trata("acordar");
|
||||||
|
}
|
||||||
|
// Copia do APK: os ultimos ajustes guardados (boca, sobrancelhas, sono...) valem também aqui.
|
||||||
|
setTimeout(() => { if (LOCAL) { const c = lembrado("config"); if (c) configurarApp(c); } checarServidor(); }, 300);
|
||||||
|
setInterval(checarServidor, 1000);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script src="/reacoes-render.js"></script>
|
<script src="/reacoes-render.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user