package br.carinha.rosto; import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.os.BatteryManager; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Color; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.Image; import android.media.ImageReader; import android.media.MediaRecorder; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.text.InputType; import android.util.Log; import android.util.Size; import android.view.View; import android.view.WindowManager; import android.webkit.ConsoleMessage; import android.webkit.PermissionRequest; import android.webkit.WebChromeClient; import android.webkit.WebResourceError; import android.webkit.WebResourceRequest; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.webkit.ValueCallback; import android.webkit.JavascriptInterface; import android.widget.FrameLayout; import org.json.JSONObject; import android.widget.EditText; import android.widget.Toast; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Carinha — mostra o rosto (HTML/SVG) em tela cheia, com a tela sempre ligada. * * A pagina vem do servidor da carinha (o mesmo kiosk-server.py da placa), entao * trocar o rosto ou mandar um gesto pela rede vale na hora para este celular. * Se o servidor nao responder, cai para a copia local embutida no APK (rosto * funcionando, sem controle remoto). * * Segure o dedo na tela para trocar o endereco do servidor. * * --------------------------------------------------------------------------- * PONTE DO MICROFONE * O Java le o nivel do microfone (AudioRecord) e manda para a pagina chamando * window.eilik.som(nivel), com nivel de 0 (silencio) a 1 (bem alto). Quem decide * o que fazer com isso e a propria pagina: barulho alto e repentino vira * "surpreso" e silencio prolongado faz dormir. Assim a mesma pagina continua * funcionando sozinha na telinha do Pi (sem microfone), sem nenhuma mudanca la. * * --------------------------------------------------------------------------- * PONTE DA PRESENCA (movimento) * O Java abre a camera FRONTAL (Camera2), compara a luminancia de um quadro com * a do anterior e manda o resultado para a pagina chamando * window.eilik.movimento(vel, cx, cy), com vel de 0 (parado) a 1 (muito * movimento) e cx,cy o centro do que se mexeu (0 a 1 na imagem). Assim a carinha * acorda quando algo se mexe na frente dela e os olhos acompanham o movimento. * Isto funciona mesmo com o rosto vindo do servidor por http, onde o navegador * nao deixaria a pagina usar a camera (contexto inseguro). */ public class MainActivity extends Activity { private static final String PREFS = "carinha"; private static final String CHAVE = "servidor"; private static final String PADRAO = "http://192.168.1.15:8100"; private static final String TAG = "CarinhaMic"; private static final String TAG_JS = "CarinhaJS"; // console.log da pagina private static final int PEDIDO_PERMISSOES = 1001; private static final String PAGINA_CAMERA = "file:///android_asset/camera.html"; private static final int TAXA_MIC = 16000; // Hz — de sobra para medir volume private static final long INTERVALO_ENVIO = 50; // ms entre envios a pagina (~20/s) private static final String TAG_MOV = "CarinhaMov"; // movimento da camera frontal private static final String CHAVE_MOV = "movimento"; // liga/desliga nas preferencias private static final long INTERVALO_MOV = 33; // ms entre envios (~30/s) private static final int LIMIAR_MOV = 26; // salto de luminancia (igual a pagina) private static final int AMOSTRAS_MOV = 30000; // maximo de pixels analisados por quadro private WebView web; private String servidor; private final CerebroLocal cerebroLocal = new CerebroLocal(); private boolean jaCaiuParaLocal = false; private boolean jaRecarregou = false; private boolean usandoLocal = false; // servidor caiu: ficamos na copia do APK private boolean naCamera = false; // a pagina aberta agora e a da camera // --- microfone --- private AudioRecord gravador; private volatile boolean pararMic; private Thread fioMic; // --- presenca (movimento pela camera frontal) --- private boolean movimentoLigado = true; // reagir ao movimento (preferencia) private volatile boolean querMovimento; // a captura deve estar ativa private boolean abrindoCamara; // openCamera ja foi pedido e ainda nao respondeu private CameraManager camMan; private CameraDevice camara; private CameraCaptureSession sessao; private ImageReader leitor; private HandlerThread fioCam; private Handler manipCam; private int[] lumaAnt; // luminancia do quadro anterior private int[] lumaAtual; private int quadrosMov; // quadros ja analisados (aquecimento) private long ultimoEnvioMov; private long proximoLogMov; private ControleRemoto remoto; private volatile boolean emPrimeiroPlano; private WebView rtcWeb; private volatile boolean rtcPronto; private boolean rtcAtivo; private int rotacaoPreview; private volatile long ultimoUsoTela; private volatile boolean dormindo; private String ladoDetector = "user"; private String ajustesPreview = ""; private String urlCamera() { JSONObject c = remoto.config(); return PAGINA_CAMERA + "?lado=" + c.optString("cameraLado", "user") + "&resolucao=" + c.optString("videoResolucao", "auto") + "&fps=" + c.optInt("videoFPS", 0); } private boolean micPermitido() { JSONObject c = remoto.config(); return Sensores.ativo(c.optBoolean("sensorMic", true), c.optBoolean("micSono", true), dormindo); } private boolean cameraPermitida() { JSONObject c = remoto.config(); return Sensores.ativo(c.optBoolean("sensorCamera", true), c.optBoolean("cameraSono", false), dormindo); } private void atualizarSensores() { if (!emPrimeiroPlano) return; if (micPermitido()) iniciarCaptura(); else pararCaptura(); if (!cameraPermitida()) { if (naCamera) abrirRosto(); pararMovimento(); rtcWeb.evaluateJavascript("window.rtcParar&&window.rtcParar()", null); rtcAtivo = false; } else if (naCamera) { String url = urlCamera(); if (!url.equals(ajustesPreview)) { ajustesPreview = url; web.loadUrl(url); } } else { String lado = remoto.config().optString("cameraLado", "user"); if (!lado.equals(ladoDetector)) { pararMovimento(); ladoDetector = lado; } iniciarMovimento(); } web.evaluateJavascript("window.carinhaSensores=" + "{microfone:" + (micPermitido() && gravador != null) + ",camera:" + (cameraPermitida() && (camara != null || rtcAtivo)) + "}", null); } @Override protected void onCreate(Bundle estado) { super.onCreate(estado); // tela sempre ligada: isto e um enfeite de mesa, nao pode apagar getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); SharedPreferences p = getSharedPreferences(PREFS, MODE_PRIVATE); servidor = p.getString(CHAVE, PADRAO); movimentoLigado = p.getBoolean(CHAVE_MOV, true); usarServidorDoPedido(getIntent()); // permite apontar o servidor ao lancar cerebroLocal.iniciar(this, servidor); remoto = new ControleRemoto(new Runnable() { @Override public void run() { runOnUiThread(new Runnable() { @Override public void run() { if (emPrimeiroPlano) { if (!remoto.videoRTC() && rtcAtivo) { rtcWeb.evaluateJavascript("window.rtcParar&&window.rtcParar()", null); rtcAtivo = false; } atualizarSensores(); atualizarBrilho(); } } }); } }); remoto.rtcListener(new ControleRemoto.RTCListener() { @Override public boolean receber(final JSONObject pacote) { if (!rtcPronto || !emPrimeiroPlano || naCamera) return false; runOnUiThread(new Runnable() { @Override public void run() { if (!emPrimeiroPlano || naCamera) return; boolean ativa = !pacote.isNull("id") && remoto.preview() && cameraPermitida(); if (ativa && !rtcAtivo) { rtcAtivo = true; pararMovimento(); } else if (!ativa && rtcAtivo) { rtcWeb.evaluateJavascript("window.rtcParar&&window.rtcParar()", null); rtcAtivo = false; web.postDelayed(new Runnable() { @Override public void run() { if (emPrimeiroPlano && !rtcAtivo && !naCamera) iniciarMovimento(); } }, 600); } if (cameraPermitida()) { try { pacote.put("config", remoto.config()); pacote.put("movimento", movimentoLigado && remoto.config().optBoolean("reagir", true)); } catch (Exception e) { /* JSON */ } rtcWeb.evaluateJavascript("window.rtcReceber&&window.rtcReceber(" + pacote.toString() + ")", null); } } }); return true; } }); web = new WebView(this); web.setBackgroundColor(Color.BLACK); WebSettings cfg = web.getSettings(); cfg.setJavaScriptEnabled(true); cfg.setDomStorageEnabled(true); cfg.setUseWideViewPort(true); cfg.setLoadWithOverviewMode(true); cfg.setMediaPlaybackRequiresUserGesture(false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { cfg.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); } web.setWebViewClient(new WebViewClient() { @Override public void onReceivedError(WebView vista, WebResourceRequest pedido, WebResourceError erro) { // so a pagina principal importa aqui: um 404 de favicon, por // exemplo, nao pode derrubar para a copia local if (pedido.isForMainFrame()) cairParaLocal(vista); } @SuppressWarnings("deprecation") @Override public void onReceivedError(WebView vista, int codigo, String descricao, String url) { // API 21/22 nao recebem o pedido, entao so aqui vale o aviso antigo if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) cairParaLocal(vista); } }); // o console.log da pagina aparece no logcat (ajuda a ver a carinha reagindo) web.setWebChromeClient(new WebChromeClient() { @Override public boolean onConsoleMessage(ConsoleMessage m) { Log.i(TAG_JS, m.message() + " (" + m.sourceId() + ":" + m.lineNumber() + ")"); return true; } @Override public void onPermissionRequest(final PermissionRequest pedido) { // a pagina da camera (getUserMedia) pede permissao ao WebView; // concedemos so o que o app realmente ja tem runOnUiThread(new Runnable() { @Override public void run() { boolean pode = true; for (String recurso : pedido.getResources()) { if (PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(recurso) && checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) pode = false; if (PermissionRequest.RESOURCE_AUDIO_CAPTURE.equals(recurso) && checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) pode = false; } if (pode) { pedido.grant(pedido.getResources()); Log.i(TAG, "camera/audio concedidos a pagina"); } else { pedido.deny(); Log.i(TAG, "pagina pediu permissao que o app nao tem"); } } }); } }); // segurar o dedo abre o menu (camera / servidor) web.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { menu(); return true; } }); rtcWeb = new WebView(this); rtcWeb.getSettings().setJavaScriptEnabled(true); rtcWeb.getSettings().setMediaPlaybackRequiresUserGesture(false); rtcWeb.addJavascriptInterface(new PonteRTC(), "CarinhaRTC"); rtcWeb.setWebViewClient(new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { rtcPronto = true; } @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { return true; } }); rtcWeb.setWebChromeClient(new WebChromeClient() { @Override public boolean onConsoleMessage(ConsoleMessage msg) { Log.i("CarinhaRTC", msg.message()); return true; } @Override public void onPermissionRequest(final PermissionRequest request) { runOnUiThread(new Runnable() { @Override public void run() { if (Build.VERSION.SDK_INT < 23 || checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) request.grant(new String[]{PermissionRequest.RESOURCE_VIDEO_CAPTURE}); else request.deny(); } }); } }); FrameLayout root = new FrameLayout(this); root.addView(rtcWeb, new FrameLayout.LayoutParams(1, 1)); root.addView(web, new FrameLayout.LayoutParams(-1, -1)); setContentView(root); rtcWeb.loadUrl("file:///android_asset/transmissao.html"); abrirRosto(); telaCheia(); pedirPermissoes(); } /** * Permite escolher o servidor na hora de iniciar o app, sem passar pelo menu: * * adb shell am start -n br.carinha.rosto/.MainActivity \ * -e servidor http://192.168.1.15:8099 * * Serve para hospedar o servidor em outra maquina (o PC, por exemplo) e * testar sem reconstruir o APK. O endereco fica guardado para as proximas vezes. */ private void usarServidorDoPedido(Intent it) { if (it == null) return; String novo = it.getStringExtra("servidor"); if (novo == null) return; novo = novo.trim(); while (novo.endsWith("/")) novo = novo.substring(0, novo.length() - 1); if (novo.length() == 0) return; servidor = novo; getSharedPreferences(PREFS, MODE_PRIVATE).edit().putString(CHAVE, servidor).apply(); jaCaiuParaLocal = false; jaRecarregou = false; usandoLocal = false; Log.i(TAG, "servidor escolhido ao iniciar: " + servidor); } /** Retomado em primeiro plano (singleTask): aceita um servidor novo no pedido. */ @Override protected void onNewIntent(Intent it) { super.onNewIntent(it); setIntent(it); if (it != null && it.getStringExtra("servidor") != null) { usarServidorDoPedido(it); abrirRosto(); } } /** Volta a mostrar a carinha (a pagina do servidor, ou a copia local). */ private void abrirRosto() { naCamera = false; remoto.servidor(servidor); if (usandoLocal) web.loadUrl("file:///android_asset/carinha.html"); else web.loadUrl(servidor + "/carinha.html?aparelho=1"); iniciarMovimento(); // volta a observar quem passa na frente } /** * Mostra a pagina da camera. Ela vem SEMPRE do arquivo embutido no APK * (file://), porque so um contexto seguro deixa a pagina pedir a camera — * servida por http, o navegador bloquearia o getUserMedia. */ private void abrirCamera() { if (!cameraPermitida()) { aviso("Camera desativada nos ajustes de sensores"); return; } naCamera = true; rtcWeb.evaluateJavascript("window.rtcParar&&window.rtcParar()", null); rtcAtivo = false; pararMovimento(); // a pagina da camera vai usar a camera: solta a nossa ajustesPreview = urlCamera(); web.loadUrl(ajustesPreview); } /** Menu do toque longo. */ 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"}; new AlertDialog.Builder(this) .setTitle("Carinha") .setItems(opcoes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface d, int i) { if (i == 0) abrirCamera(); else if (i == 1) abrirRosto(); else if (i == 2) alternarMovimento(); else if (i == 3) pedirServidor(); else if (i == 4) menuSensores(); else menuVideo(); } }) .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); aviso("Ajuste enviado ao PC"); } catch (Exception e) { aviso("Falha ao alterar ajuste"); } } private void menuSensores() { final String[] chaves = {"sensorMic", "sensorCamera", "micSono", "cameraSono"}; final boolean[] valores = {remoto.config().optBoolean(chaves[0], true), remoto.config().optBoolean(chaves[1], true), remoto.config().optBoolean(chaves[2], true), remoto.config().optBoolean(chaves[3], false)}; new AlertDialog.Builder(this).setTitle("Sensores") .setMultiChoiceItems(new String[]{"Microfone ligado", "Camera ligada", "Permitir microfone durante sono", "Permitir camera durante sono"}, valores, new DialogInterface.OnMultiChoiceClickListener() { @Override public void onClick(DialogInterface d, int i, boolean valor) { valores[i] = valor; } }) .setPositiveButton("Salvar", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface d, int w) { try { JSONObject c = new JSONObject(); for (int i=0;i<4;i++) c.put(chaves[i], valores[i]); remoto.configurar(c); aviso("Ajustes enviados ao PC"); } catch (Exception e) {} } }).setNegativeButton("Cancelar", null).show(); } private void menuVideo() { new AlertDialog.Builder(this).setTitle("Camera e video") .setItems(new String[]{"Camera frontal / traseira", "Resolucao", "Quadros por segundo"}, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface d, final int i) { final String[] textos = i == 0 ? new String[]{"Frontal", "Traseira"} : i == 1 ? new String[]{"Automatica (padrao)", "Maxima disponivel", "480p", "720p", "1080p", "4K", "8K"} : new String[]{"Automatico (padrao)", "Maximo disponivel", "15", "24", "30", "60", "120", "240"}; final String[] valores = i == 0 ? new String[]{"user", "environment"} : i == 1 ? new String[]{"auto", "max", "480", "720", "1080", "2160", "4320"} : new String[]{"0", "-1", "15", "24", "30", "60", "120", "240"}; final String chave = i == 0 ? "cameraLado" : i == 1 ? "videoResolucao" : "videoFPS"; int selecionado = 0; for (int n=0;n faltando = new ArrayList(); if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) faltando.add(Manifest.permission.RECORD_AUDIO); if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) faltando.add(Manifest.permission.CAMERA); if (faltando.isEmpty()) { iniciarCaptura(); return; } requestPermissions(faltando.toArray(new String[0]), PEDIDO_PERMISSOES); } @Override public void onRequestPermissionsResult(int pedido, String[] permissoes, int[] resultado) { super.onRequestPermissionsResult(pedido, permissoes, resultado); if (pedido != PEDIDO_PERMISSOES) return; boolean temMic = false, temCamera = false; for (int i = 0; i < permissoes.length && i < resultado.length; i++) { boolean ok = resultado[i] == PackageManager.PERMISSION_GRANTED; if (Manifest.permission.RECORD_AUDIO.equals(permissoes[i])) temMic = ok; if (Manifest.permission.CAMERA.equals(permissoes[i])) temCamera = ok; } if (temMic) iniciarCaptura(); else Log.i(TAG, "permissao do microfone negada — a carinha segue sem ouvir"); if (temCamera) iniciarMovimento(); else Log.i(TAG, "permissao da camera negada — a pagina da camera nao vai abrir"); } /** Abre o AudioRecord e dispara a thread que mede o volume. */ private void iniciarCaptura() { if (!emPrimeiroPlano || !micPermitido()) return; if (Build.VERSION.SDK_INT >= 23 && checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) return; if (fioMic != null && fioMic.isAlive()) return; int minimo = AudioRecord.getMinBufferSize(TAXA_MIC, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); if (minimo <= 0) minimo = TAXA_MIC * 2; try { gravador = new AudioRecord(MediaRecorder.AudioSource.MIC, TAXA_MIC, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, Math.max(minimo, TAXA_MIC)); if (gravador.getState() != AudioRecord.STATE_INITIALIZED) { Log.w(TAG, "AudioRecord nao inicializou"); gravador.release(); gravador = null; return; } gravador.startRecording(); } catch (Exception e) { Log.w(TAG, "falha ao abrir o microfone: " + e); if (gravador != null) { gravador.release(); gravador = null; } return; } Log.i(TAG, "microfone ligado — " + TAXA_MIC + " Hz mono"); pararMic = false; final AudioRecord rec = gravador; fioMic = new Thread(new Runnable() { @Override public void run() { short[] buf = new short[TAXA_MIC / 50]; // ~20 ms long ultimoEnvio = 0; double envelope = 0; // pico com decaimento long proximoResumo = 0; double picoJanela = 0; while (!pararMic) { int lidos; try { lidos = rec.read(buf, 0, buf.length); } catch (Exception e) { break; } if (lidos <= 0) continue; double soma = 0; for (int i = 0; i < lidos; i++) { double s = buf[i]; soma += s * s; } double rms = Math.sqrt(soma / lidos); double nivel = nivelDe(rms); // sobe na hora e cai devagar: assim uma palma aparece bem envelope = Math.max(nivel, envelope * 0.80); long agora = System.currentTimeMillis(); if (agora - ultimoEnvio >= INTERVALO_ENVIO) { ultimoEnvio = agora; enviarNivel(arredondar(envelope)); } if (envelope > picoJanela) picoJanela = envelope; if (agora >= proximoResumo) { // uma linha por segundo proximoResumo = agora + 1000; Log.i(TAG, "nivel=" + String.format(java.util.Locale.US, "%.3f", envelope) + " pico=" + String.format(java.util.Locale.US, "%.3f", picoJanela)); picoJanela = 0; } } } }, "mic"); fioMic.start(); } /** Fecha o AudioRecord e encerra a thread. */ private void pararCaptura() { pararMic = true; if (gravador != null) { try { gravador.stop(); } catch (Exception e) {} } if (fioMic != null) { try { fioMic.join(400); } catch (InterruptedException e) { /* segue */ } fioMic = null; } if (gravador != null) { try { gravador.stop(); } catch (Exception e) { /* ja parado */ } gravador.release(); gravador = null; Log.i(TAG, "microfone desligado"); } } /** * Converte o RMS (0..32768) numa escala de 0 a 1 que a pagina entende. * Trabalha em dBFS: numa sala silenciosa fica por volta de 0,2; fala comum * passa de 0,6; palma/barulho alto chega perto de 1. */ private static double nivelDe(double rms) { if (rms < 1.0) return 0.0; double db = 20.0 * Math.log10(rms / 32768.0); // tipicamente -90..0 double n = (db + 55.0) / 50.0; // -55 dB -> 0 ; -5 dB -> 1 if (n < 0) n = 0; if (n > 1) n = 1; return n; } private static double arredondar(double v) { return Math.round(v * 1000.0) / 1000.0; } /** Manda o nivel para a pagina, que decide o que fazer (gesto/ dormir). */ private void enviarNivel(final double nivel) { if (!micPermitido() || !emPrimeiroPlano) return; if (nivel >= remoto.somlim()) ultimoUsoTela = System.currentTimeMillis(); runOnUiThread(new Runnable() { @Override public void run() { try { web.evaluateJavascript( "window.eilik&&window.eilik.som(" + nivel + ")", null); } catch (Exception e) { /* pagina ainda nao pronta */ } } }); } @Override protected void onResume() { super.onResume(); emPrimeiroPlano = true; ultimoUsoTela = System.currentTimeMillis(); remoto.servidor(servidor); remoto.iniciar(); web.onResume(); rtcWeb.onResume(); web.evaluateJavascript("window.carinhaAtiva=true", null); telaCheia(); // voltou do segundo plano: atualiza o rosto — mas sem expulsar quem // estava vendo a pagina da camera if (jaRecarregou && !naCamera) { abrirRosto(); } jaRecarregou = true; pedirPermissoes(); // religa o microfone (onPause desligou) if (!naCamera) iniciarMovimento(); // e volta a enxergar o movimento } @Override protected void onPause() { super.onPause(); emPrimeiroPlano = false; remoto.parar(); rtcWeb.evaluateJavascript("window.rtcParar&&window.rtcParar()", null); rtcAtivo = false; rtcWeb.onPause(); web.evaluateJavascript("window.carinhaAtiva=false", null); web.onPause(); WindowManager.LayoutParams attr = getWindow().getAttributes(); attr.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE; getWindow().setAttributes(attr); pararCaptura(); // em segundo plano nao precisa ouvir pararMovimento(); // nem observar o movimento } @Override protected void onDestroy() { cerebroLocal.fechar(); super.onDestroy(); remoto.parar(); pararCaptura(); pararMovimento(); rtcWeb.destroy(); } // ===================================================================== // Presenca (movimento pela camera frontal) // ===================================================================== /* Abre a camera FRONTAL com Camera2 num tamanho pequeno, mede o movimento comparando a luminancia de um quadro com a do anterior (mesma ideia da pagina camera.html) e conta para a pagina chamando window.eilik.movimento(vel,cx,cy). Fica desligado enquanto a pagina da camera esta aberta, para nao disputar a camera com o getUserMedia dela. */ /** (Re)abre a camera frontal para detectar movimento, se for para reagir. */ private void iniciarMovimento() { if (!emPrimeiroPlano || !cameraPermitida()) return; if (rtcAtivo) return; querMovimento = true; if ((!movimentoLigado && !remoto.preview()) || naCamera) return; if (camara != null || abrindoCamara) return; // ja aberta ou abrindo if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) return; try { camMan = (CameraManager) getSystemService(CAMERA_SERVICE); if (camMan == null) return; String id = acharFrontal(); if (id == null) { Log.i(TAG_MOV, "sem camera frontal neste aparelho"); querMovimento = false; return; } if (fioCam == null) { // a thread do detector, so uma vez fioCam = new HandlerThread("cam"); fioCam.start(); manipCam = new Handler(fioCam.getLooper()); } abrindoCamara = true; camMan.openCamera(id, retornoCam, manipCam); Log.i(TAG_MOV, "abrindo a camera frontal " + id); } catch (Exception e) { Log.w(TAG_MOV, "falha ao abrir a camera: " + e); pararMovimento(); } } /** Fecha a camera e a thread do detector. */ private void pararMovimento() { querMovimento = false; abrindoCamara = false; liberarCaptura(); if (fioCam != null) { fioCam.quitSafely(); fioCam = null; } manipCam = null; camMan = null; } /** Fecha sessao, camera e leitor (sem mexer na thread do detector). */ private void liberarCaptura() { if (sessao != null) { try { sessao.close(); } catch (Exception e) { /* ja fechou */ } sessao = null; } if (camara != null) { try { camara.close(); } catch (Exception e) { /* ja fechou */ } camara = null; } if (leitor != null) { leitor.close(); leitor = null; } lumaAnt = null; lumaAtual = null; quadrosMov = 0; } private String acharFrontal() throws CameraAccessException { for (String id : camMan.getCameraIdList()) { Integer face = camMan.getCameraCharacteristics(id).get(CameraCharacteristics.LENS_FACING); int desejada = remoto.config().optString("cameraLado", "user").equals("environment") ? CameraCharacteristics.LENS_FACING_BACK : CameraCharacteristics.LENS_FACING_FRONT; if (face != null && face == desejada) return id; } return null; } private final CameraDevice.StateCallback retornoCam = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice d) { abrindoCamara = false; camara = d; abrirSessao(d); } @Override public void onDisconnected(CameraDevice d) { Log.i(TAG_MOV, "camera desconectada"); abrindoCamara = false; try { d.close(); } catch (Exception e) { /* ja fechou */ } liberarCaptura(); } @Override public void onError(CameraDevice d, int e) { Log.w(TAG_MOV, "erro na camera: " + e); abrindoCamara = false; try { d.close(); } catch (Exception e2) { /* ja fechou */ } liberarCaptura(); } }; /** Escolhe um ImageReader pequeno (YUV) e comeca a receber quadros. */ private void abrirSessao(CameraDevice dev) { if (!querMovimento) { dev.close(); camara = null; return; } try { StreamConfigurationMap mapa = camMan.getCameraCharacteristics(dev.getId()) .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); Size tam = menorTamanho(mapa); Integer sensor = camMan.getCameraCharacteristics(dev.getId()).get(CameraCharacteristics.SENSOR_ORIENTATION); int display = getWindowManager().getDefaultDisplay().getRotation() * 90; rotacaoPreview = (360 - ((sensor == null ? 0 : sensor) + display) % 360) % 360; if (tam == null) { Log.w(TAG_MOV, "sem tamanho YUV disponivel"); return; } leitor = ImageReader.newInstance(tam.getWidth(), tam.getHeight(), android.graphics.ImageFormat.YUV_420_888, 2); lumaAnt = null; lumaAtual = null; quadrosMov = 0; leitor.setOnImageAvailableListener(ouvinteQuadros, manipCam); dev.createCaptureSession(Arrays.asList(leitor.getSurface()), retornoSessao, manipCam); Log.i(TAG_MOV, "detector " + tam.getWidth() + "x" + tam.getHeight()); } catch (Exception e) { Log.w(TAG_MOV, "falha ao montar a sessao: " + e); } } /** O menor formato YUV util (o detector nao precisa de resolucao). */ private static Size menorTamanho(StreamConfigurationMap mapa) { if (mapa == null) return null; Size[] todos = mapa.getOutputSizes(android.graphics.ImageFormat.YUV_420_888); if (todos == null || todos.length == 0) return null; Size melhor = null; for (Size s : todos) { if (s.getWidth() < 640 || s.getHeight() < 360) continue; if (melhor == null || (long) s.getWidth() * s.getHeight() < (long) melhor.getWidth() * melhor.getHeight()) melhor = s; } if (melhor == null) { // nenhuma acima de 160: pega a menor for (Size s : todos) if (melhor == null || (long) s.getWidth() * s.getHeight() < (long) melhor.getWidth() * melhor.getHeight()) melhor = s; } return melhor; } private final CameraCaptureSession.StateCallback retornoSessao = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession s) { if (!querMovimento || camara == null) return; try { sessao = s; CaptureRequest.Builder pedido = camara.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); pedido.addTarget(leitor.getSurface()); pedido.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); s.setRepeatingRequest(pedido.build(), null, manipCam); Log.i(TAG_MOV, "detector ligado"); } catch (Exception e) { Log.w(TAG_MOV, "falha no pedido de captura: " + e); } } @Override public void onConfigureFailed(CameraCaptureSession s) { Log.w(TAG_MOV, "nao consegui configurar a captura"); } }; private final ImageReader.OnImageAvailableListener ouvinteQuadros = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader r) { Image img; try { img = r.acquireLatestImage(); } catch (Exception e) { return; } if (img == null) return; try { if (movimentoLigado) analisar(img); remoto.quadro(img, rotacaoPreview); } catch (Exception e) { /* quadro ruim: segue */ } finally { img.close(); } } }; /** Compara a luminancia do quadro com a do anterior: umidade de movimento e centro. */ private void analisar(Image img) { int w = img.getWidth(), h = img.getHeight(); if (w <= 0 || h <= 0) return; Image.Plane plano = img.getPlanes()[0]; ByteBuffer y = plano.getBuffer(); int linha = plano.getRowStride(); int passoPx = plano.getPixelStride(); int passo = 1; while (passo < 16 && (w / passo) * (h / passo) > AMOSTRAS_MOV) passo++; int colunas = (w + passo - 1) / passo; int linhas = (h + passo - 1) / passo; int total = colunas * linhas; if (lumaAnt == null || lumaAnt.length != total || lumaAtual == null || lumaAtual.length != total) { lumaAnt = new int[total]; lumaAtual = new int[total]; return; // primeiro quadro: so guarda } int n = 0, mexidos = 0; long sx = 0, sy = 0, somaLuz = 0; for (int yy = 0; yy < h; yy += passo) { int base = yy * linha; for (int xx = 0; xx < w; xx += passo) { int l = y.get(base + xx * passoPx) & 0xFF; somaLuz += l; if (Math.abs(l - lumaAnt[n]) > LIMIAR_MOV) { mexidos++; sx += xx; sy += yy; } lumaAtual[n] = l; n++; } } int[] troca = lumaAnt; lumaAnt = lumaAtual; lumaAtual = troca; // aquecimento: a exposicao ainda esta assentando nos primeiros quadros, // o que daria um pico de "100% de movimento" e um susto falso quadrosMov++; if (quadrosMov <= 3) return; long agora = System.currentTimeMillis(); double vel = Math.min(1.0, mexidos / ((double) total * 0.25)); double cx = mexidos > 0 ? (double) sx / mexidos / w : 0.5; double cy = mexidos > 0 ? (double) sy / mexidos / h : 0.5; if (agora >= proximoLogMov) { // uma linha a cada 2 s (logcat) proximoLogMov = agora + 2000; Log.i(TAG_MOV, "movimento=" + Math.round(vel * 100) + "% centro=" + Math.round(cx * 100) + "," + Math.round(cy * 100) + "% brilho=" + Math.round(somaLuz / (double) total)); } if (agora - ultimoEnvioMov < INTERVALO_MOV) return; ultimoEnvioMov = agora; enviarMovimento(vel, cx, cy); } private void enviarMovimento(final double vel, final double cx, final double cy) { if (!cameraPermitida() || !emPrimeiroPlano) return; if (vel >= remoto.movlim()) ultimoUsoTela = System.currentTimeMillis(); final String js = "window.eilik&&window.eilik.movimento(" + fmt(vel) + "," + fmt(cx) + "," + fmt(cy) + ")"; runOnUiThread(new Runnable() { @Override public void run() { try { web.evaluateJavascript(js, null); } catch (Exception e) { /* pagina nao pronta */ } } }); } /** Numero com ponto decimal (a pagina espera 0.42, nao 0,42). */ private static String fmt(double v) { return String.format(java.util.Locale.US, "%.3f", v); } private void atualizarBrilho() { if (naCamera) return; Intent bateria = registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); if (bateria != null) { int level = bateria.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); int scale = bateria.getIntExtra(BatteryManager.EXTRA_SCALE, -1); int status = bateria.getIntExtra(BatteryManager.EXTRA_STATUS, -1); if (level >= 0 && scale > 0) { int percent = Math.round(level * 100f / scale); boolean charging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL; web.evaluateJavascript("window.carinhaBateria={nivel:" + percent + ",carregando:" + charging + "}", null); } } web.evaluateJavascript("window.eilik?({dormindo:window.eilik.face.dormindo,comando:window.carinhaUltimoComando||0,ocupado:(Date.now()-(window.carinhaUltimoComando||0)<3000)&&!!(window.eilik.face.kf||window.eilik.face.fila.length)}):null", new ValueCallback() { @Override public void onReceiveValue(String valor) { if (!emPrimeiroPlano) return; try { JSONObject estado = new JSONObject(valor); dormindo = estado.optBoolean("dormindo"); atualizarSensores(); ultimoUsoTela = Math.max(ultimoUsoTela, estado.optLong("comando", 0)); float nivel = BrilhoRepouso.nivel(remoto.economia(), remoto.brilho(), remoto.brilhoRepouso(), System.currentTimeMillis() - ultimoUsoTela, remoto.repousoTela(), estado.optBoolean("ocupado"), estado.optBoolean("dormindo")); WindowManager.LayoutParams attr = getWindow().getAttributes(); if (Math.abs(attr.screenBrightness - nivel) > 0.005f) { attr.screenBrightness = nivel; getWindow().setAttributes(attr); Log.i("CarinhaPC", "brilho=" + nivel); } web.evaluateJavascript("window.carinhaBrilho=" + fmt(nivel), null); } catch (Exception e) { /* rosto ainda carregando */ } } }); } private final class PonteRTC { @JavascriptInterface public void rastrear(final String json) { if (!emPrimeiroPlano || !cameraPermitida() || !remoto.config().optBoolean("seguirRastreamento", true)) return; try { final JSONObject alvo = "null".equals(json) ? null : new JSONObject(json); if (alvo != null) { double x = alvo.getDouble("x"), y = alvo.getDouble("y"); if (Double.isNaN(x) || Double.isNaN(y) || x<0 || x>1 || y<0 || y>1) return; String tipo=alvo.optString("tipo"); if (!("mao".equals(tipo) && remoto.config().optBoolean("rastrearMaos", true)) && !("rosto".equals(tipo) && remoto.config().optBoolean("rastrearRosto", true))) return; if (alvo.optBoolean("atividade")) ultimoUsoTela=System.currentTimeMillis(); } runOnUiThread(new Runnable() { @Override public void run() { if (emPrimeiroPlano && cameraPermitida()) web.evaluateJavascript("window.eilik&&window.eilik.rastrear("+ (alvo==null?"null":alvo.toString())+")",null); } }); } catch(Exception e) { Log.w("CarinhaRTC","Alvo invalido"); } } @JavascriptInterface public void enviar(String json) { remoto.sinalRTC(json); } @JavascriptInterface public void movimento(double vel, double cx, double cy) { if (emPrimeiroPlano) enviarMovimento(vel, cx, cy); } @JavascriptInterface public void estado(final String json) { try { final String seguro = new JSONObject(json).toString(); runOnUiThread(new Runnable() { @Override public void run() { if (emPrimeiroPlano) web.evaluateJavascript("window.carinhaVideo=" + seguro, null); } }); } catch (Exception e) { Log.w("CarinhaRTC", "estado invalido"); } } } }