Beacons: Trabalhando com serviços em background contínuos em aplicações Android Oreo+ sem irritar o usuário.
O ano de 2019, apesar dos pesares, foi um ano de conhecimento e aventuras para mim. Isso não é exagero, afinal, foi o ano que eu descobri meu amor pelo desenvolvimento mobile, começando por React Native e me aprofundando mais do que nunca em Android. Em um dos projetos que atuei, trabalhamos com uma tecnologia que até então, nunca tinha ouvido falar: Beacons.
Antes de eu explicar o B.O. que enfrentei, vamos conhecer um pouco sobre a tecnologia em questão.
Entendendo os beacons
Beacons são devices que utilizam BLE — Bluetooth Low Energy — para localizar dispositivos em um espaço. Em outras palavras, são pequenos dispositivos que você consegue comprar até mesmo via AliExpress que, ao ligar, emitem sinais Bluetooth capazes de mapear a distância entre outros dispositivos que emitem sinais Bluetooth, nesse caso, podendo ser celulares ou pulseiras por exemplo.
Pois bem, voltando ao projeto: assim que os clientes entrassem na área de atuação do dispositivo, receberiam uma notificação personalizada a fim de facilitar o atendimento, bem legal né?! Para ser algo rápido e preciso em relação a localização do cliente para curtas distâncias, foi sugerido a utilização de Beacons. Até então, seria algo fácil, oras. Utilizando a biblioteca Android Beacon Library — ABL — para oprotocolo AltBeacon, seria apenas criar um serviço em background, o qual, assim que o celular com Bluetooth ligado, identificasse um Beacon com o ID correspondente ao do estabelecimento, é só emitir a notificação. Inclusive, a biblioteca até disponibiliza em sua documentação um exemplo:
//Classe para monitoramento de Beacons na regiãopublic class MonitorandoBeaconsActivity extends Activity implements BeaconConsumer {
protected static final String TAG = "MonitorandoBeaconsActivity";
private BeaconManager beaconManager;@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ranging);
beaconManager = BeaconManager.getInstanceForApplication(this); // Configurando para detectar beacons de um protocolo em específico, nesse caso, o AltBeacon. Existem outros protocolos como iBeacon, Eddystone, então se atente a esse detalhe!
beaconManager.getBeaconParsers().add(new BeaconParser().setBeaconLayout("m:2-3=beac,i:4-19,i:20-21,i:22-23,p:24-24,d:25-25")); beaconManager.bind(this);
}.
.
. //Método que verifica ao iniciar o serviço se entrou em uma região com beacons
@Override
public void onBeaconServiceConnect() {
beaconManager.removeAllMonitorNotifiers();
beaconManager.addMonitorNotifier(new MonitorNotifier() {
@Override
public void didEnterRegion(Region region) {
Log.i(TAG, "Existem beacons na região! :)");
}
}}
Qual o problema então se tudo está vindo de mão beijada? Pois bem, em 2017 tivemos umas mudanças de comportamento do Android 8.0 (Oreo) que algumas delas ficaram para as próximas versões. Uma dessas mudanças foi em relação aos serviços: agora temos limites de execução.
O monstro de 7 cabeças
Para melhorar o desempenho do dispositivo — principalmente em relação a bateria — , o sistema agora limita alguns comportamentos de aplicativos que não estejam em execução no primeiro plano, nesse caso, os aplicativos em execução em segundo plano agora têm limites quanto à liberdade de acesso aos serviços de segundo plano.
Vamos dar um exemplo prático para você entender a frustração do usuário se iniciarmos serviços em primeiro plano? Imagina que você acabou de comprar um smartwatch, instalou o aplicativo e está pronto para utilizar. Porém sempre é exibida uma notificação fixa informando que o serviço está ativo — vocês não tem ideia o quão aquilo pode irritar o seu usuário, porém a construção dessa notificação se tornou obrigatória como uma maneira de mostrar transparência ao usuário de que há um aplicativo consumindo sua bateria em background, tome como exemplo o Mi Fit que você tenta e tenta mas não consegue dar dismiss em sua notificação :
Em resumo, você não pode iniciar serviços como antes apenas com um startService() se a aplicação não estiver em foreground, caso contrário, receberá uma IllegalStateException. E ainda se chamar o método em foreground e logo após a aplicação ser movida para background, ainda há chance que com o tempo o serviço venha a ser destruído, principalmente se envolver requisições, ou seja, vai levar um tempo.
Pelo menos a Google disponibilizou algumas alternativas para que os desenvolvedores possam desenvolver serviços de outras maneiras, na biblioteca que utilizamos indicam o uso de Job Scheduler, não que ele não funcione mas os serviços, ou melhor, jobs, não são escalados quando queremos e nem de maneira contínua — cada execução possui intervalo de no mínimo 15 minutos, mesmo que você configure. Com isso, fomos além da documentação e testamos algo bem divertido, utilizamos AlarmManager e construímos uma arquitetura com os alarmes, vamos dar uma olhada no diagrama que fiz para entendermos o que iremos construir:
O que está acontecendo?
Com esse overview em mãos, vamos entender a fundo o que está acontecendo passo a passo:
- Assim que iniciamos o aplicativo (isso no exemplo, no projeto real acontece após o boot do device), registramos um Alarme. O método registrar nesse caso é responsável por, através de uma Intent, chamar a classe AlarmScan.
//Primeiro registramos o método na nossa activity
AlarmRegister.registrar(getApplicationContext());//Se liga o que acontece no método de registrar
public static void registrar(Context context){ //Intent para a classe de alarm responsável pelo scan
Intent it = new Intent(context, AlarmScan.class);
PendingIntent p = PendingIntent.getBroadcast( context, 101, it, PendingIntent.FLAG_UPDATE_CURRENT); //Assim que registrar esse método, registramos o tempo de 15 segundos - tempo esse que disparará o alarme
Calendar c = Calendar.getInstance();
c.setTimeInMillis(System.currentTimeMillis());
c.add(Calendar.SECOND, 15); //Instanciando o alarme
AlarmManager alarme = (AlarmManager) context.getSystemService(ALARM_SERVICE); //Convertendo o tempo para long
long time = c.getTimeInMillis(); //O alarme irá acionar/acordar (RTC_WAKEUP) o intent p dentro do time 15 segundos
alarme.setExact(AlarmManager.RTC_WAKEUP, time, p);}
2. A classe AlarmScan, que extende um BroadcastReceiver, por sua vez instancia o objeto de BeaconScanner — calma aí, já já chegamos nele — , caso o mesmo não tenha sido instanciado ainda, e bota em prática o método de Scan, que pelo nome, vemos que é escanea os beacons na região.
public class AlarmScan extends BroadcastReceiver { private static BeaconScanner bs;@RequiresApi(api = Build.VERSION_CODES.N)
@Override public void onReceive(Context context, Intent intent) { //Caso o objeto ainda não tenha sido instanciado, logo o instanciamos, fazemos isso pois você verá que na verdade criamos um loop
if (AlarmScan.bs == null) {
AlarmScan.bs = new BeaconScanner(context);
} //Independente se instanciamos ou não, colocamos o BeaconScanner para escanear a região
AlarmScan.bs.Scan(); //Por fim, registramos novamente o alarme anterior, criando um ciclo de alarmes
AlarmRegister.registrar(context);
}
}
3. Essa parte será um pouco mais extensa, vamos lá: no construtor da nossa classe BeaconScanner, vinculamos o serviço e registramos o protocolo de layout do beacon que estaremos scaneando.
public BeaconScanner(Context context) {
this.context = context;//Atente-se a registrar o protocolo através do setBeaconLayout, no caso estamos usando o protocolo AltBeacon
beaconManager = BeaconManager.getInstanceForApplication(context);
beaconManager.getBeaconParsers().add(new BeaconParser() .setBeaconLayout(“m:2–3=0215,i:4–19,i:20–21,i:22–23,p:24–24”));
beaconManager.bind(this);
}
4. Ainda na classe BeaconScanner, nosso método Scan indica que está em execução e executa o método startRangingBeaconsInRegion, lembrando que esse método encontra apenas os beacons identificados no protocolo registrado anteriormente.
public void Scan(){
try { //O id é apenas de identificação da região para o contexto, principalmente para você ter o controle disso
beaconManager.startRangingBeaconsInRegion(new Region(“IdRangingUnico”, null, null, null)); } catch (RemoteException e)
{ e.printStackTrace();}
}
5. Quando o serviço é iniciado com sucesso e está pronto para receber os beacons no protocolo designado, logo é chamado e assim nós sobrecarregamos o método de onBeaconServiceConnect. Essa parte pode soar maçante porém é aqui que a magia acontece! Para entender mais, vou contextualizá-los e assim vocês podem analisar o código.
5.1 Implementamos o ranging — lá embaixo em pontos a se destacar vamos esclarecer o que é — isto é, ao entrarmos em uma região com beacons.
5.2 Se de fato existem beacons na região, podemos fazer o que quiser com eles, no caso filtramos pela distância e sempre iniciamos uma activity — apenas como POC — com a lista dos filtrados passados através de uma intent. Mesmo com todos os processos encerrados, a activity ainda é instanciada! Que demais, não? É nessa hora que você pode montar suas requisições, notificações e assim por diante.
5.3 Por fim, encerramos o processo de ranging e atribuímos todo esse processo ao nosso beaconManager.
@Override public void onBeaconServiceConnect() {//O RangeNotifier é responsável por notificar se existem beacons na região na classe implementada
RangeNotifierEx rangeNotifier = new RangeNotifierEx(this) {@Override public void didRangeBeaconsInRegion(Collection<Beacon> beacons, Region region) { //Criamos um ArrayList que salvará o ID dos beacons encontrados
ArrayList<String> beaconsEncontrados = new ArrayList<String>(); //Apenas quando o tamanho dos beacons encontrados seja maior que 0, filtramos a distância, criamos um foreach para verificamos
if (beacons.size() > 0) {
for (Beacon beacon : beacons) {
//Sim! A distância é medida em metros, pode filtrar da distância que achar melhor.
if (beacon.getDistance() <= 15) {
//Adicionamos na nossa lista e assim por diante
beaconsEncontrados.add(beacon.getId1().toString().toLowerCase()); }
}
} try { //Independente do que faça com os beacons encontrados, sempre lembre de encerrar o processo de ranging! Lembre que estamos criando um loop, e não encerrar pode atropelar esse processo e causar n exceptions em relação ao processo. beaconManager.stopRangingBeaconsInRegion(region); } catch (RemoteException e) {
e.printStackTrace();
} //No caso, sempre que encontra beacons, inicio uma activity que exibe os beacons, isso apenas para a POC, você pode chamar uma outra classe e fazer uma requisição com base no ID do beacon encontrado e assim por diante.
Intent i = new Intent(context, POCActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.putStringArrayListExtra(“BeaconsEncontrados”, beaconsEncontrados);
context.startActivity(i);
}
}; //E por fim, informamos ao nosso gerenciador o rangeNotifier - isto é, o que ele fará ao ser notificado que entrou em uma região de beacons
beaconManager.addRangeNotifier(rangeNotifier); }
}
Pronto! Você conseguiu implementar todo o processo apenas com AlarmManagers. Mas lembre-se que você deve se atentar a alguns detalhes.
Detalhes a se pensar
- Hoje temos vários protocolos de beacon, AltBeacon, iBeacon, Eddystone… Mesmo você não tendo o dispositivo, você pode emular e assim estudar! Existem diversos apps que fazem isso, eu particularmente uso o BeaconSimulator, você pode configurar o beacon a ser simulado bem como o protocolo. Digo isso pois quando falamos em iOS, o protocolo padrão é o iBeacon, logo você terá dois beacons — um para cada sistema operacional — e o ideal, é você filtrar pelo beacon próprio. Nota: o protocolo Eddystone da Google dá suporte para ambos os sistemas operacionais porém com algumas limitações, vale estudar e analisar a viabilidade a fim de economizar na compra dos dispositivos.
- O Beacon possui dois momentos em uma região: o ranging e o monitoring. O monitoring de uma região permite que seu aplicativo saiba quando um dispositivo entra ou sai do intervalo de beacons definidos pela região. Imagine um museu com um aplicativo de guia em áudio e existem beacons instalados nas entradas. O aplicativo está monitorando uma região que abrange os esses beacons — pense em ‘região de todos os beacons de entrada’ — e é notificado sempre que o usuário entra no museu, lembrando sobre o recurso de guia de áudio. Enquanto o monitoring permite detectar o movimento dentro e fora da faixa dos beacons, o ranging é mais granular. Ele retorna uma lista de beacons ao alcance, juntamente com uma proximidade estimada para cada um deles. Voltando ao exemplo do museu: imagine que cada exposição tem um beacon próximo próprio. O aplicativo de guia de áudio pode procurar todos os beacons nesta região e depois verificar qual deles é o mais próximo. Como cada beacon está associado a uma exibição específica, o aplicativo reproduz uma descrição de uma obra de arte relevante para o contexto do usuário. Utilize isso ao seu favor e conforme o seu objetivo! ;)
- Serviços em background antes da atualização Oreo são mais próximos do comportamento que queremos, algo 100% contínuo. Algo que pode fazer é verificar a versão do Android — caso seja maior que a 8.0 — e assim decidir que tipo de serviço irá utilizar — sendo do tipo AlarmManager ou comum. Lembre-se que o AlarmManager que utilizamos só é possível em versões iguais ou acima da KitKat — API 19.
- Lembre-se que ao trabalhar com beacons seu Bluetooth — e a localização acima do Android 8.0 — precisam estar ativos! Caso contrário, não irá funcionar.
Apesar da complexidade, é uma maneira de você implementar soluções que farão total diferença na experiência do usuário.
Confira aqui o projeto completo no GitHub, por ser antigo não tá tão organizado sequer comentado, então utilize esse artigo como guia. ;)