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:
@@ -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 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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user