
GCMNetworkManager Para Execução de Tarefas no Background Android
(5421) (19)

CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim

CategoriaEngenharia de Software
Autor(es)Eric Evans
EditoraAlta Books
Edição3ª
Ano2016
Páginas528
Tudo bem?
Neste artigo vamos construir, passo a passo, um aplicativo Android de GPS Tracking, isso com o objetivo de mostrar uma maneira eficiente de trabalhar com tarefas em background, utilizando o GCMNetworkManager API.
No decorrer do conteúdo, além de outros assuntos, também serão abordadas as opções ante ao uso do GCMNetworkManager (AlarmManager, JobScheduler e SyncAdapter), incluindo as vantagens desse, o GCMNetworkManager, sobre essas.
Antes de prosseguir, não esqueça de se inscrever 📫 na lista de e-mails do Blog para receber todas as atualizações do desenvolvimento Android, em primeira mão.
Abaixo os tópicos apresentados no artigo:
- Entendendo o Doze Mode;
- AlarmManager x JobScheduler x GCMNetworkManager x SyncAdapter;
- GCMNetworkManager, visão geral;
- Projeto de exemplo - parte Web:
- Projeto de exemplo - parte Android:
- Vídeo com implementação passo a passo do exemplo;
- Conclusão;
- Fontes.
Entendendo o Doze Mode
O Doze Mode foi incluído no Android a partir da API 23, Android 6 ou Android Marshmallow. O objetivo dessa entidade é aumentar o tempo de vida da bateria do device.
Não é de hoje que os smartphones, não somente Android, têm problemas com o tempo de vida de suas baterias.
Dê uma olhada na figura abaixo para entender melhor o Doze Mode:
Acima a representação do Doze Mode em ação. As barras laranjas são as várias aplicações do device realizando suas sincronizações com suas APIs Web, requisições Web.
Não sei se já está ciente do problema, mas o procedimento de ativar as tecnologias de radio do device para permitir a conexão com a Internet é um procedimento desgastante e que consome muito da bateria.
É exatamente nesse tempo que mais se consome a carga dela, isso quando no contexto: conexão com a Internet.
Para minimizar esse problema o Doze Mode entra em ação assim que o Android OS percebe que o device já está a algum tempo sem a iteração do usuário: tela em off por um longo tempo e device não ligado ao carregador.
As faixas verdes da figura anterior representam os momentos onde o device para com as execuções em background, incluindo as conexões com a Internet.
As barras laranjas que se espaçam cada vez mais são as janelas de manutenção (maintenance window). Nesses momentos os aplicativos rapidamente voltam a executar em background, podendo utilizar também a conexão com a Internet.
Logo depois, à volta ao Doze Mode, até o usuário entrar em ação no device ou alguma notificação de alta prioridade ser recebida.
Na figura anterior, a versão do Doze Mode apresentada é a mais crítica, Deep-Doze, com ela, a cada nova entrada em Doze Mode o espaço para a próxima janela de manutenção aumenta ainda mais.
Existe outro modo?
Sim, esse foi adicionado somente no Android 7 (ou Android Nougat). É o Light-Doze:
Com as mesmas condições: usuário sem iterar com o device por um longo tempo e o aparelho não está carregando.
A partir do Android 7 o device entra primeiro em Light-Doze, com a principal diferença sendo o não espaçamento entre as janelas de manutenção.
Porém fique ciente que o Light-Doze é apenas o pré-requisito para a entrada em Deep-Doze, pois esse ainda existe no Android 7 e é iniciado depois de um certo tempo em Light-Doze.
Ok, mas por que a explicação do Doze Mode nesse conteúdo sobre GCMNetworkManager?
Simples. O GCMNetworkManager é diretamente afetado pelo Doze Mode. Não somente esse, mas o JobScheduler, SyncAdapter e AlarmManager também.
Imagine que você precise que seu aplicativo envie as coordenadas do usuário para sua aplicação Web de dez em dez minutos, sendo que é aceitável uma variação nesse intervalo, não tão grande, mas aceitável.
O GCMNetworkManager se encaixa perfeitamente como uma solução. Porém, em seus testes, você percebe que ao dormir (tem developer que faz isso, não vejo problemas), o número de envios de dados na madrugada (enquanto você e o device dormiam) caiu consideravelmente.
Logo, você corre para vários fóruns para tentar descobrir qual é o problema... e ... não é um problema, é uma característica dos devices Android, Doze Mode em ação.
Sua lógica de negócio precisa ser redesenhada ou você terá de utilizar outras tecnologias para conseguir esse feito: as coordenadas GPS do device a cada dez minutos.
Mas há solução para isso? Eu tenho um aplicativo que precisa de tarefas no background executando mesmo quando o usuário não está a algum tempo utilizando o device?
Se suas tarefas não envolvem conexão com a Internet, você pode utilizar o AlarmManager, pois segundo a documentação, o AlarmManager executa mesmo quando em Doze Mode.
Isso, pois seria muito "estranho" seu aplicativo de agendamento para tomar remédios, digo, agendamento de horários para tomar remédios, não funcionar devido ao usuário estar dormindo e não ter interagido com o device. Muito estranho!
Porém, se seu app precisa de conexão com a Internet, o AlarmManager não ajudará, pois nem mesmo ele conseguirá forçar a conexão com a Internet quando o device estiver em Doze Mode.
Nesse caso, a solução mais evidente pede um pouco mais de código.
Sua aplicação terá de ter um servidor Web onde rodará um crontab. Esse crontab acionará de tempos em tempos (de um em um minuto, por exemplo) um script back-end de seu projeto.
Esse script acessará sua base de dados de agendamentos. E então obterá todos os agendamentos para aquelas hora e minuto, enviará uma push message (notificação) com prioridade alta para os devices que precisam executar alguma tarefa relacionada ao agendamento de seus usuários.
Devido ao uso de prioridade alta em seus algoritmos de notificação (Google Cloud Messaging - GCM - ou Firebase Cloud Messaging - FCM) o Doze Mode não tem poder para bloquear essas notificações, fazendo com que a conexão com a Internet seja criada e que as tarefas de seu aplicativo possam ser executadas.
Quer um exemplo simples de aplicativos que utilizam notificações com alta prioridade? Aplicativos de mensagens.
Note que apesar da complexidade da solução anterior fazer com que ela pareça definitiva, na verdade ela não é.
Lembre que nós não temos controle sobre a latência da rede e nem sobre a situação dos servidores de push message do Google Android.
Ou seja, pode haver delay na entrega das notificações... perdeu-se o horário para tomar o remédio!
Com isso, a melhor solução é adaptar sua lógica de negócio. Na próxima seção há uma discussão falando sobre quando como utilizar as tecnologias de tarefas de background disponíveis no Android.
Importante: não deixe de ler a documentação sobre o Doze Mode, ela está em português, Otimização para soneca e aplicativo em espera.
Nesse conteúdo há muito mais detalhes, incluindo algumas restrições que são aplicadas até mesmo ao AlarmManager e a aplicações que estão na lista de permissão para não entrarem na lista de aplicativos que devem respeitar o estado Doze Mode estritamente.
AlarmManager x JobScheduler x GCMNetworkManager x SyncAdapter
AlarmManager: a tecnologia de agendamento de tarefas de background mais simples de utilizar. O principal indício de que o AlarmManager é a escolha ideal é quando sua lógica de negócio precisa de exatidão na ativação da tarefa, ou seja, o que foi colocado na configuração, o tempo definido, deve ser respeitado.
Aplicativos de agendamento e de calendário de consultas são exemplos de apps que têm lógicas de negócio que não podem esperar condições ideais do device para terem suas tarefas executadas.
Porém, se seu aplicativo precisa de exatidão e ao mesmo tempo de conexão com a Internet, o recomendado é alterar o modelo de trabalho do app, pois com o que o Android lhe prover atualmente não será possível, sem a intervenção do usuário, garantir essas duas características ao mesmo tempo.
Sem a intervenção do usuário?
Sim, ele pode aumentar as prioridades de seu aplicativo nas configurações do device, mais precisamente, seu app pode avisa-lo sobre a necessidade atual utilizando a permissão REQUEST_IGNORE_BATTERY_OPTIMIZATIONS.
Mas não conte com esse comportamento, a não ser que seu aplicativo seja para os funcionários de sua empresa, onde você poderá também, pessoalmente, informa-los sobre a configuração requerida no device.
JobScheduler: se seu app não trabalha com devices com a API abaixo da 21, Lollipop, e também não precisa de exatidão na ativação das tarefas, apenas uma janela onde a execução é aceitável naquele tempo. Nessas condições o JobScheduler é uma excelente opção.
Aplicativos modernos que trabalham com GPS tracking se beneficiam com o uso do JobScheduler. Porém o principal ponto negativo é poder trabalhar apenas com devices que estejam com a API 21 ou superior.
SyncAdapter: uma excelente opção caso você não queira que seu aplicativo se preocupe com a utilização ou não do Google Play Services, além de dar suporte a partir da API 7. O SyncAdapter é antigo no Android e não muito famoso, digo, não entre os tutoriais gratuitos.
Por que isso?
O SyncAdapter não tem uma implementação trivial como as outras opções dessa seção. Para utiliza-lo você também tem que configurar um ContentProvider e o AccountManager, mesmo que de maneira fake, mock.
GCMNetworkManager: com características muito similares ao JobScheduler, o GCMNetworkManager se destaca em poder trabalhar também com APIs abaixo da 21. Além de já entrar em uma Thread de background quando invoca o método que deve acionar as tarefas que serão executadas, algo que o JobScheduler não faz.
Para APIs abaixo da 21 o GCMNetworkManager utiliza uma tecnologia proprietária do Google para manter o funcionamento sem diferença das APIs iguais ou maiores que a 21.
Para as APIs a partir da 21 o GCMNetworkManager utiliza o JobScheduler no background. A partir da API 9 você já consegue utilizar o GCMNetworkManager.
Com exceção do caso de necessidade de exatidão na ativação da tarefa, para todos os outros o GCMNetworkManager se encaixa muito bem.
Com as informações apresentadas até aqui você consegue definir o que utilizar. Não deixe de sempre priorizar a experiência do usuário de seu app, esqueça as dicas do Google se os números de seu analytics mostram que há mais iteração e retenção quando as tarefas executam com maior frequência. Continue assim, e vice-versa.
GCMNetworkManager, visão geral
Como já explicado anteriormente, o GCMNetworkManager não é exato quanto o tempo da execução da tarefa, definimos uma janela possível para execução, porém temos um ganho na otimização do uso da bateria e um código muito simples de se trabalhar.
Para utilizar o GCMNetworkManager você precisa primeiro adicionar a seguinte referência em seu Gradle App Level ou build.gradle (Module: app):
dependencies {
compile 'com.google.android.gms:play-services-gcm:10.0.0'
}
Mas o GCM não está para ser removido?
Na verdade não, não exatamente. Na documentação do GCM "push message" tem as recomendações para utilizar o FCM, pois esse último é mais atual e robusto.
De qualquer forma, o GCMNetworkManager é uma opção que, a princípio, não tem substituto no conjunto de tecnologias Firebase, logo as recomendações nos topos das páginas da documentação do GCM não valem para ele.
Continuando com a configuração padrão do GCMNetworkManager, é preciso um Service, mais precisamente um Service que herde de GcmTaskService:
public class CustomService extends GcmTaskService {
@Override
public void onInitializeTasks() {
super.onInitializeTasks();
}
@Override
public int onRunTask( TaskParams taskParams ) {
return GcmNetworkManager.RESULT_SUCCESS;
}
}
O método onInitializeTasks() é ativado quando o app é atualizado ou reinstalado. Nesse caso, quando esse método é invocado isso indica que todas as tarefas do aplicativo que tinham sido criadas e ainda não executadas foram destruídas.
É nesse método que seus algoritmos de lógica de negócio devem acessar sua base de dados local (ou remota) que tenha todas as configurações das tarefas que tinham sido agendadas e então reconstruí-las novamente.
Note que onInitializeTasks() roda na Thread principal enquanto onRunTask() roda em uma Thread de background.
Esse último, onRunTask(), é onde suas tarefas deverão ser executadas. Elas têm no máximo três minutos para serem executadas, depois disso a Thread é descartada.
Se os três minutos não forem o suficiente, parta para a velha e conhecida solução de: utilize um AlarmManager para criar um Service que no background realizará essa longa execução.
Voltando ao onRunTask(), nele você deve retornar algum dos seguintes três valores:
- GcmNetworkManager.RESULT_SUCCESS: informa que a execução foi sem problemas e que essa tarefa atual não precisa ser executada novamente;
- GcmNetworkManager.RESULT_FAILURE: informa que a tarefa falhou, mas que não precisa ser executada uma outra vez;
- GcmNetworkManager.RESULT_RESCHEDULE: informa que houve falha na execução da tarefa e que essa deve ser reagendada para um nova execução.
Uma coisa importante: você vai ver que com o GCMNetworkManager nós podemos criar acionadores de tarefas (ou apenas tarefas, são sinônimos aqui) de um único trigger ou de triggers repetitivos.
Para os repetitivos, não confundam o retorno GcmNetworkManager.RESULT_RESCHEDULE como sendo necessário.
O retorno de onRunTask() é referente a tarefa atual em execução, nada tem relação com as tarefas posteriores de um trigger repetitivo.
Com isso podemos mostrar as configurações necessárias no AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.gpstrackinggcmnetworkmanager">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<service
android:name=".service.CustomService"
android:exported="true"
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
<intent-filter>
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
</intent-filter>
</service>
</application>
</manifest>
Ok, a tag <service> já uma conhecida antiga minha, mas e essas configurações?
As tarefas que você criar com o GCMNetworkManager, digo, esses triggers (ou acionadores), são controlados pelo Google Play Services, ou seja, ele é que vai invocar o Service de sua aplicação.
O action com.google.android.gms.gcm.ACTION_TASK_READY é que vai permitir que o Service de sua aplicação responda ao trigger de tarefa do Google Play Services.
O atributo android:exported="true" permite que outra aplicação acione o Service de seu aplicativo, nesse caso a outra aplicação é o Google Play Services.
A permission com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE é a que permite que somente o Google Play Services seja a outra aplicação que pode acionar o service de seu aplicativo.
A permissão android.permission.RECEIVE_BOOT_COMPLETED é para que seja possível persistir com as tarefas já agendadas para seu app. Persistir depois de o device ter sido reiniciado.
Com o AlarmManager temos a dor de cabeça de termos de trabalhar com um BroadcastReciever para remontar todos os agendamentos.
Agora podemos passar para a criação da tarefa (ou, trigger de tarefa). Na MainActivity, por exemplo, você pode colocar o seguinte código:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate( Bundle savedInstanceState ) {
super.onCreate( savedInstanceState );
setContentView( R.layout.activity_main );
OneoffTask task = new OneoffTask.Builder()
.setTag( "periodic" )
.setService( CustomService.class )
.setExecutionWindow( 5, 30 )
.setRequiredNetwork( Task.NETWORK_STATE_CONNECTED )
.setRequiresCharging( false )
.setUpdateCurrent( true )
.setPersisted( true )
.build();
GcmNetworkManager
.getInstance( this )
.schedule( task );
}
}
OneoffTask é a tarefa sem periodicidade, ela é chamada apenas uma vez. O schedule() do GcmNetworkManager é que agenda a tarefa. Quanto aos métodos:
- setTag(): é o onde se defini o nome da tarefa, essa é a maneira de identifica-la;
- setService(): aqui se defini o Service que executará a tarefa;
- setExecutionWindow(): o primeiro parâmetro informa a partir de que tempo, em segundos, a tarefa já pode ser executada. O segundo parâmetro informa o segundo máximo, depois de a tarefa ter sido agendada, que ela já deverá ter entrado em execução;
- setRequiredNetwork(): defini a condição requerida quanto a Internet no device, para poder executar:
- Task.NETWORK_STATE_CONNECTED: deve haver conexão;
- Task.NETWORK_STATE_ANY: qualquer estado de Internet é o suficiente;
- Task.NETWORK_STATE_UNMETERED: que a tarefa só será executada se houver uma conexão de rede sem medição.
- setRequiresCharging(): se é necessário o device estar sendo carregado (true) ou não (false) para que a tarefa seja acionada;
- setUpdateCurrent(): caso a tarefa atual sendo criada tenha o mesmo tag name de uma outra tarefa na fila para execução, com esse método sendo true, a tarefa antiga na fila será descartada, caso contrário ambas vão ficar na fila aguardando a execução. Tarefas em execução não são canceladas;
- setPersisted(): quando true e com a permissão android.permission.RECEIVE_BOOT_COMPLETED definida no AndroidManifest, faz com que todas as tarefas agendadas persistam depois da reinicialização do device;
- build(): constrói a tarefa com as configurações informadas.
Para a execução de tarefas de forma repetitiva, você utiliza o PeriodicTask:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate( Bundle savedInstanceState ) {
super.onCreate( savedInstanceState );
setContentView( R.layout.activity_main );
Bundle bundle = new Bundle();
bundle.putString( "key", "valor de teste" );
PeriodicTask task = new PeriodicTask.Builder()
.setTag( "periodic" )
.setService( CustomService.class )
.setPeriod( 20 )
.setFlex( 5 )
.setRequiredNetwork( Task.NETWORK_STATE_UNMETERED )
.setRequiresCharging( false )
.setUpdateCurrent( true )
.setPersisted( true )
.setExtras( bundle )
.build();
GcmNetworkManager
.getInstance( this )
.schedule( task );
}
}
Abaixo vamos a explicação dos método ainda não falados sobre. Segue:
- setPeriod(): definimos, em segundos, o intervalo onde queremos que seja executada nossa tarefa, no máximo, uma vez. Esse período pode não ser respeitado algumas vezes;
- setFlex(): faz com que seja criada uma janela de execução para a tarefa. Ou seja, com setPeriod(20) e setFlex(5) estamos informando ao Google Play Services que a tarefa deve ser invocada, preferencialmente, entre os segundo 15 e 20 do intervalo informado;
- setExtras(): também disponível para o OneoffTask, nele podemos enviar dados que serão acessados por meio do parâmetro TaskParams de onRunTask() do GcmTaskService.
Para cancelar todas as tarefas criadas temos a seguinte opção:
...
GcmNetworkManager
.getInstance( this )
.cancelAllTasks( CustomService.class );
...
cancelAllTasks() recebe como parâmetro o Service que terá as tarefas a ele vinculadas removidas da fila para execução.
A outra opção é cancelTask():
...
GcmNetworkManager
.getInstance( this )
.cancelTask( "tag-name", CustomService.class );
...
Junto a tag name da tarefa, definindo o Service, é possível desativar uma task especifica.
Com isso cobrimos o "como" utilizar o GCMNetworkManager. Note que o Google Play Services vai acionar as tarefas em momentos oportunos, quando um app já estiver utilizando a conexão com a Internet, por exemplo, e se essa configuração de Internet estiver nas configurações de sua tarefa.
Quanto a configuração e momento oportuno, vale o mesmo para quando o device estiver carregando.
Com isso podemos passar ao exemplo, o aplicativo de GPS Tracking.
Projeto de exemplo - parte Web
Esse projeto Android tem uma simples parte Web onde as coordenadas são salvas em um arquivo .txt para simular um banco de dados relacional.
Para baixar esse projeto Web completo, acesse o GitHub dele em: https://github.com/viniciusthiengo/gps-tracking-gcm-network-manager-web.
Configuração e estrutura
O back-end Web do projeto roda em um servidor PHP, mais precisamente, no pacote MAMP, com configurações:
- Apache 2.2.29;
- PHP 5.6.2;
- MySQL 5.5.38.
A estrutura do projeto está como segue:
Na lógica de negócio utilizada, os arquivos tracking dentro do diretório /data têm como sufixo o id do usuário da aplicação e dentro de cada um desses arquivos contém as coordenadas de seus respectivos usuários.
Para desenvolver o projeto Web utilizei o PHPStorm, mas qualquer editor de código ou IDE, como o Eclipse, é o bastante.
Domínio do problema
Temos apenas duas classes de domínio do problema na versão Web do projeto. Segue o código de User:
class User
{
private $id;
private $tracking;
public function setByPOST()
{
$this->id = $_POST['user_id'];
$this->tracking = new Tracking();
$this->tracking->setByPOST();
}
public function save()
{
$file = fopen( '../../data/tracking-'.$this->id.'.txt', 'a' );
fwrite( $file, $this->tracking->getLatitude() );
fwrite( $file, Tracking::SEPARATOR );
fwrite( $file, $this->tracking->getLongitude()."\n" );
fclose( $file );
}
}
E então o código de Tracking:
class Tracking
{
const SEPARATOR = '__sdsdvsvs__';
const SUCCESS = '0';
const FAILURE = '2';
private $latitude;
private $longitude;
public function setByPOST()
{
$this->latitude = $_POST['latitude'];
$this->longitude = $_POST['longitude'];
}
public function getLatitude()
{
return $this->latitude;
}
public function getLongitude()
{
return $this->longitude;
}
}
Ambas são bem simples, adicionadas somente para manter o projeto Web funcional.
Controlador
O controlador do projeto Web é opcional, mas passa a possibilidade de adição de ainda mais lógica de negócio na versão Web. Esse controlador é responsável por direcionar a execução correta de acordo com a entrada de dados pela superglobal $_POST.
Segue código de CtrlUser:
<?php
include_once '../../domain/User.php';
include_once '../../domain/Tracking.php';
if( strcasecmp($_POST['method'], 'user-tracking') == 0 ){
$user = new User();
$user->setByPOST();
$user->save();
echo Tracking::SUCCESS;
exit;
}
echo Tracking::FAILURE;
Note que CtrlUser é um arquivo, puro, PHP e não uma classe PHP.
Projeto de exemplo - parte Android
Aqui vamos seguir a configuração da versão Android. Como para a seção anterior, configuração da versão Web, vamos colocar a maior parte do código aqui, digo, o suficiente para você já colocar em um novo projeto no Android Studio.
Mas caso queira o acesso até mesmo aos arquivos de mídia e de configuração de uma projeto no Android Studio, você pode estar acessando o projeto, versão Android, em: https://github.com/viniciusthiengo/gps-tracking-gcm-network-manager.
Para seguir com o exemplo, crie um novo projeto (empty activity) no Android Studio, de preferência com o nome "GPS Tracking GCMNetworkManager".
Configurações Gradle
Abaixo a configuração do Gradle Top Level ou build.gradle (Project: GPSTrackingGCMNetworkManager):
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.2'
}
}
allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Note que foi adicionado o repositório maven { url "https://jitpack.io" }, isso devido ao uso da library Simplify-Permissions que vamos utilizar para permissão em tempo de execução. Mais abaixo vamos apresenta-la melhor.
Agora a configuração do Gradle App Level ou build.gradle (Module: app):
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "24.0.3"
defaultConfig {
applicationId "br.com.thiengo.gpstrackinggcmnetworkmanager"
minSdkVersion 16
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.0.1'
testCompile 'junit:junit:4.12'
/* Permission System Library */
compile 'com.github.anshulagarwal06:Simplify-Permissions:v1'
/* LocationManager */
compile 'com.github.zellius:rxlocationmanager:0.1.1'
/* Retrofit */
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.google.code.gson:gson:2.7'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.google.android.gms:play-services-gcm:10.0.0' /* GCMNetworkManager */
}
Como informado anteriormente, a partir da API 9 já é possível utilizar o GCMNetworkManager. O minSdkVersion 16 é devido a library de permissão em tempo de execução, Simplify-Permissions, que está sendo utilizada.
De qualquer forma, a RxLocationManager utiliza como API mínima a API 14, esse seria nosso "piso" sem a library de permissions.
Configurações AndroidManifest
Abaixo as configurações do AndroidManifest.xml do projeto:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.gpstrackinggcmnetworkmanager">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".service.CustomService"
android:exported="true"
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
<intent-filter>
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
</intent-filter>
</service>
</application>
</manifest>
Note que além da já explicada permissão RECEIVE_BOOT_COMPLETED temos outras quatro. Isso, pois precisamos nesse projeto de acesso a Internet (Retrofit) e de obtenção de coordenadas (RxLocationManager).
Atividade principal (MainActivity)
Aqui vou começar com o layout da activity, mais precisamente o arquivo activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="br.com.thiengo.gpstrackinggcmnetworkmanager.MainActivity">
<TextView
android:layout_marginTop="25dp"
android:id="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/colorPrimaryDark"
android:textSize="38sp"
android:text="GCM Network Manager" />
<Button
android:textSize="18sp"
android:id="@+id/bt_tracking"
android:text="Iniciar tracking"
android:layout_centerHorizontal="true"
android:onClick="tracking"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorAccent"/>
</RelativeLayout>
Bem simples, direto de uma empty activity. Esse layout vai permitir termos a seguinte tela:
Ok, já lhe entendi, o background, certo?
Esse é colocado via /values/styles.xml, a linha destacada abaixo:
<resources>
<style name="AppTheme" parent="Theme.AppCompat">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowBackground">@drawable/background</item>
</style>
</resources>
Agora, antes de prosseguir com o código da MainActivity, veja o código da classe Util:
public class Util {
private static final String SP = "SP";
public static final String TRACKING_STATUS = "tracking_status";
public static void saveSP(
Context context,
String key,
boolean value ){
SharedPreferences sp = context.getSharedPreferences( SP, Context.MODE_PRIVATE );
sp.edit().putBoolean( key, value ).apply();
}
public static boolean retrieveSP(
Context context,
String key ){
SharedPreferences sp = context.getSharedPreferences( SP, Context.MODE_PRIVATE );
return sp.getBoolean( key, false );
}
}
Em nosso projeto de exemplo precisamos de alguma maneira saber qual o estado atual da execução do tracking, isso para que não realizemos uma ativação quando esse já está ativado.
Com a classe Util conseguimos encapsular o acesso ao SharedPreferences, uma entidade de persistência simples que vai nos permitir isso, controlar o estado do tracking.
Com isso, de forma parcial, vamos acessar a primeira parte do código da MainActivity:
public class MainActivity extends MarshmallowSupportActivity {
@Override
protected void onCreate( Bundle savedInstanceState ) {
super.onCreate( savedInstanceState );
setContentView( R.layout.activity_main );
changeButtonLabel();
}
private void changeTrackingStatus(){
boolean status = Util.retrieveSP( this, Util.TRACKING_STATUS );
Util.saveSP( this, Util.TRACKING_STATUS, !status );
}
private void changeButtonLabel(){
Button bt = (Button) findViewById( R.id.bt_tracking );
if( Util.retrieveSP( this, Util.TRACKING_STATUS ) ){
bt.setText( "Parar tracking" );
}
else{
bt.setText( "Iniciar tracking" );
}
}
...
}
O método changeTrackingStatus() permite que facilmente alteremos o estado atual do tracking do aplicativo.
O método changeButtonLabel() permite que alteremos o rótulo do Button de activity_main.xml de acordo com o estado do tracking do app, isso para o usuário saber quando iniciar e quando parar o tracking.
A herança MarshmallowSupportActivity é devido a library Simplify-Permissions que vamos apresentar na seção logo abaixo.
Configuração da library Simplify-Permissions
Essa library permite que utilizemos de forma simples e segura o script de solicitação de permissão em tempo de execução.
Segue a página dela no GitHub: https://github.com/anshulagarwal06/Simplify-Permissions.
Então a configuração da Simplify-Permissions na MainActivity:
public class MainActivity extends MarshmallowSupportActivity {
...
public void tracking( View view ){
String[] LOCATION_PERMISSIONS = {
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
};
Permission.PermissionBuilder permissionBuilder = new Permission
.PermissionBuilder(
LOCATION_PERMISSIONS,
0,
new InternPermissionRequest()
);
requestAppPermissions( permissionBuilder.build() );
}
private class InternPermissionRequest implements Permission.PermissionCallback{
@Override
public void onPermissionGranted( int i ) {
setTrackingTask();
}
@Override
public void onPermissionDenied( int i ) {}
@Override
public void onPermissionAccessRemoved( int i ) {}
}
}
O método tracking() está vinculado ao listener de clique do Button do activity_main.xml.
Note que sem uma instância que implementa a interface PermissionCallback, terceiro parâmetro em PermissionBuilder, a library retorna uma exceção.
O segundo parâmetro dePermissionBuilder, 0, seria um requestCode para ser utilizado no onActivityResult() se estivéssemos acessando a Camera2 API, por exemplo.
Na implementação de PermissionCallback, a classe interna InternPermissionRequest, somente o método onPermissionGranted() é útil para nosso exemplo de aplicativo de GPS Tracking.
Isso, pois é apenas um exemplo. Para projetos que vão para produção, você trabalharia todos os métodos para dar um feedback válido ao usuário.
Se você está se perguntando o porquê de a classe interna não ser estática.
Saiba que essa classe nunca terá o ciclo de vida maior que a classe pai dela, a MainActivity, logo, não temos a necessidade de remover a referência implícita que há para a MainActivity, quando a classe interna não é static.
O método setTrackingTask() é referente as configurações do GCMNetworkManager. Esse método é explicado logo abaixo.
Configuração do GCMNetworkManager
Começando pelas configurações de criação de tarefa:
public class MainActivity extends MarshmallowSupportActivity {
...
private void setTrackingTask(){
GcmNetworkManager gnm = GcmNetworkManager.getInstance( this );
changeTrackingStatus();
changeButtonLabel();
if( !Util.retrieveSP( this, Util.TRACKING_STATUS ) ){
gnm.cancelAllTasks( CustomService.class );
return;
}
PeriodicTask task = new PeriodicTask.Builder()
.setTag( "periodic" )
.setService( CustomService.class )
.setPeriod( 20 )
.setFlex( 10 )
.setRequiredNetwork( Task.NETWORK_STATE_UNMETERED )
.setRequiresCharging( false )
.setUpdateCurrent( true )
.setPersisted( true )
.build();
gnm.schedule( task );
}
...
}
Note que não tivemos a necessidade de termos o GcmNetworkManager como variável de instância, ele é utilizado somente no método setTrackingTask().
No único condicional do método verificamos se o tracking já está em execução, caso sim, alteramos o label do Button, cancelamos todas as tarefas (sempre será somente uma) e saímos do método sem criar uma nova task.
Agora a parte da configuração do Service CustomService que herda de GcmTaskService:
public class CustomService extends GcmTaskService {
private static final String TAG = "log";
private Location location;
@Override
public int onRunTask( TaskParams taskParams ) {
retrieveCoordinate();
lockThreadUntilLocationNotNull();
sendCoordinate();
return GcmNetworkManager.RESULT_SUCCESS;
}
...
}
Nas configurações que serão apresentadas nas seções a seguir você entenderá o porquê dos métodos retrieveCoordinate(), lockThreadUntilLocationNotNull() e sendCoordinate(), além do entendimento da existência da variável de instância location.
Note que depois do processamento dos métodos dentro de onRunTask() sempre estaremos retornando que a execução foi realizada com sucesso.
Em nossa lógica de negócio não há problemas com isso, mas em seu aplicativo você deve avaliar para retornar a opção correta (SUCCESS, FAILURE ou RESCHEDULE).
Configuração da library RxLocationManager
Essa library nos permite acesso simples a coordenadas disponíveis no device, caso não haja uma, podemos facilmente configurar qual será o provider que solicitará uma nova coordenada.
Para acessar a página dessa library no GitHub entre em: https://github.com/Zellius/RxLocationManager.
Utilizamos essa library em CustomService, segue:
public class CustomService extends GcmTaskService {
private static final String TAG = "log";
private Location location;
@Override
public int onRunTask( TaskParams taskParams ) {
retrieveCoordinate();
lockThreadUntilLocationNotNull();
sendCoordinate();
return GcmNetworkManager.RESULT_SUCCESS;
}
private void retrieveCoordinate(){
LocationRequestBuilder locationRequestBuilder = new LocationRequestBuilder(
getApplicationContext()
);
locationRequestBuilder
.addLastLocation(
LocationManager.NETWORK_PROVIDER,
new LocationTime( 30, TimeUnit.SECONDS ),
false
)
.addRequestLocation(
LocationManager.GPS_PROVIDER,
new LocationTime( 10, TimeUnit.SECONDS )
)
.setDefaultLocation(
new Location( LocationManager.PASSIVE_PROVIDER )
)
.create()
.subscribe( new Subscriber<Location>() {
@Override
public void onCompleted(){}
@Override
public void onError( Throwable e ){}
@Override
public void onNext( Location l ) {
location = l;
}
});
}
private void lockThreadUntilLocationNotNull(){
while( location == null ){
SystemClock.sleep( 1000 );
}
}
...
}
Depois de criarmos a instância de LocationRequestBuilder trabalhamos a configuração de como será feita a busca de coordenada no device.
O método addLastLocation() aceita, respectivamente, os dados de configuração: provider utilizado para buscar a uma coordenada; tempo máximo de vida aceito para a coordenada buscada e; se o valor null pode ser retornado, se é válido para a lógica de negócio trabalhada (em nosso exemplo, não).
O método addRequestLocation() aceita como configuração: o provider que será utilizado para a busca de uma nova coordenada e; o tempo limite de espera por essa nova coordenada, o timeout.
Em caso de falha nos dois métodos anteriores, o método setDefaultLocation() retorna uma coordenada padrão do device.
O método create() ativa a instância para buscar a coordenada e o método subscriber() trabalha como a parte subscriber do padrão de projeto Observer, inscreve uma entidade para que possa ouvir quando houver alguma coordenada disponível.
Em nosso caso criamos uma instância anônima de Subscriber<Location>. Utilizamos o método onNext() para obtermos as coordenadas e então coloca-las na variável de instância location.
Ok, mas por que essa lógica de negócio "estranha"?
Isso, pois a execução de LocationRequestBuilder é assíncrona, mesmo sabendo que ela iniciou o trabalho em uma Thread de background.
Juntando que o retorno em onNext() acontece na Thread principal. Porém, em nosso projeto, esses dados, digo, as coordenadas, terão ainda de ser enviados via conexão com a Internet, fora da Thread principal.
Logo, obtendo a referência no onNext() e então prosseguindo com a execução em onRunTask() nós ainda continuamos na Thread de background.
A, então é por isso que você colocou aquela "gambiarra" no método lockThreadUntilLocationNotNull()?
Sim, com aquele "loop de trava" (sinônimo para gambiarra), nós podemos aguardar a resposta do LocationRequestBuilder sem terminar o processamento, mesmo sendo assíncrona a execução de LocationRequestBuilder.
Lembrando que não precisamos nos preocupar com o tempo de execução desse loop. Mesmo que dê algum problema e onNext() nunca seja invocado, a aplicação continuará em execução por no máximo três minutos.
Configuração do Retrofit
Se já acompanha o Blog e Canal a algum tempo o Retrofit não será figura nova para ti, já o estou utilizando a várias aulas aqui.
Caso tenha alguma dúvida sobre como ele funciona, veja o artigo que tenho sobre ele aqui no Blog: Library Retrofit 2 no Android.
Com isso vamos começar com a definição da Interface que servirá para permitir a comunicação do Retrofit com nossa API Web. Segue código de UserTracking:
public interface UserTracking {
@FormUrlEncoded
@POST( "package/ctrl/CtrlUser.php" )
public Call<String> sendCoordinates(
@Field("method") String method,
@Field("user_id") String id,
@Field("latitude") String value,
@Field("longitude") String token
);
}
E então o código do método sendCoordinate() em CustomService:
public class CustomService extends GcmTaskService {
...
private void sendCoordinate(){
Retrofit retrofit = new Retrofit.Builder()
.baseUrl( "http://192.168.25.221:8888/gps-tracking-gcm-network-manager/" )
.addConverterFactory( GsonConverterFactory.create() )
.build();
UserTracking userTracking = retrofit.create( UserTracking.class );
Call<String> request = userTracking.sendCoordinates(
"user-tracking",
"564s6d54s6d",
String.valueOf( location.getLatitude() ),
String.valueOf( location.getLongitude() )
);
String answer = null;
try{
answer = request.execute().body();
}
catch( Exception e ){}
Log.i(TAG, "-----> "+answer);
}
}
Nada de novo do que já explicamos aqui no blog sobre o Retrofit. Somente note que por já estarmos dentro de uma Thread de background, nós optamos por utilizar o Retrofit de modo síncrono, sem callback.
Como forma de debug, apenas vamos printar nos logs do Android Studio o retorno da API Web.
Testes e resultados
Crie um emulador que tenha as Google APIs (ou execute o exemplo em um device real). Logo em seguida rode o emulador e então instale o aplicativo nele. Clique no botão "Iniciar Tracking" e terá como resultado algo como:
Para simular coordenadas no emulador, clique em "..." na barra direita de botões que aparece ao lado dele. Então clique em "Location", coloque as coordenadas e clique em "send":
Pode seguramente sair do aplicativo, até mesmo removê-lo do background.
Com isso, passados alguns segundos (de acordo com o definido em setPeriod() e setFlex()) temos os seguintes resultados no back-end Web:
Assim finalizamos a configuração de nosso simples aplicativo Android GPS Tracking.
Antes de prosseguir, não esqueça de se inscrever na 📫 lista de e-mails do Blog para receber, em primeira mão, os conteúdos Android de desenvolvimento.
Se inscreva também no canal do Blog em YouTube Thiengo.
Vídeo com implementação passo a passo do exemplo
No vídeo a seguir é apresentado o passo a passo da implementação do aplicativo de tracking do artigo:
Para acessar a versão Android do Projeto, entre no GitHub a seguir: https://github.com/viniciusthiengo/gps-tracking-gcm-network-manager
Para acessar a versão Web, entre no seguinte GitHub: https://github.com/viniciusthiengo/gps-tracking-gcm-network-manager-web
Conclusão
Sem sombra de dúvidas que há muitos benefícios no uso do GCMNetworkManager, mas infelizmente não temos a opção de controla-lo para a exata execução, digo, no tempo determinado.
Com isso devemos primeiro estudar as opções que temos para execução de tarefas em background e então, baseando-se nas informações coletadas, escolher a melhor opção para o projeto Android.
Na seção AlarmManager x JobScheduler x GCMNetworkManager x SyncAdapter discutimos isso bem. Porém os links de Fontes vão lhe ajudar nas dúvidas caso ainda existam.
Não esqueça do Doze Mode, o que você pode achar como sendo um bug, na verdade é uma característica do Android para economia de bateria.
O Evernote (você muito provavelmente conhece esse software) tem uma library que facilita ainda mais o trabalho com essas entidades de execução de tarefa em background. Acesse ela em: https://github.com/evernote/android-job.
Curtiu o conteúdo? Não esqueça de compartilha-lo. E, por fim, de se inscrever na 📩 lista de e-mails, respondo às suas dúvidas também por lá.
Abraço.
Fontes
Implementing GCM Network Manager on Android
Doc reference - GcmNetworkManager
Doc reference - OneoffTask.Builder
Doc reference - PeriodicTask.Builder
Doc reference - GcmTaskService
GcmNetworkManager example - Veja as FAQ desse link
Optimize Battery Life with Android's GCM Network Manager
Choosing the Right Background Scheduler in Android
Diving into Doze Mode for Developers
How Google Cloud Messaging handles Doze in Android 6.0 Marshmallow
Simplify-Permissions Library - Utilizada no exemplo do artigo
RxLocationManager Library - Utilizada no exemplo do artigo
Relacionado
Comentários Blog (19)



















Comentários Facebook