GCMNetworkManager Para Execução de Tarefas no Background Android

Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação. Você receberá um email de confirmação. Somente depois de confirmar é que poderei lhe enviar o conteúdo exclusivo por email.

Email inválido.
Blog /Android /GCMNetworkManager Para Execução de Tarefas no Background Android

GCMNetworkManager Para Execução de Tarefas no Background Android

Vinícius Thiengo27/11/2016
(1130) (4) (38)
Go-ahead
"Não compare você mesmo com outros, pois é ai que começa a perder confiança em si próprio."
Will Smith
Código limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Ano2017
Capítulos46
Páginas598
Comprar Livro
Conteúdo Exclusivo
Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação.
Email inválido

Opa, blz?

Nesse artigo vamos construir, passo a passo, uma APP de GPS Tracking, isso com o objetivo de mostrar uma maneira eficiente de trabalhar com tarefas de background, utilizando o GCMNetworkManager.

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 Sync Adapter), incluindo as vantagens desse, o GCMNetworkManager, sobre estas.

Abaixo os tópicos apresentados no artigo:

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 as APPs 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 Ligh-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, SyncAdapterAlarmManager também.

Imagine que você precise que sua APP 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 uma APP 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" sua APP 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 sua 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 backend 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 sua APP possam ser executadas.

Quer um exemplo simples de APPs que utilizam notificações com alta prioridade? APPs 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 APPs 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.

APPs 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 sua APP precisa de exatidão e ao mesmo tempo de conexão com a Internet, o recomendado é alterar o modelo de trabalho da 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 sua APP nas configurações do device, mais precisamente, sua 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 sua APP 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 sua 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.

APPs modernas 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 sua APP 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 sua 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 a APP é atualizada ou reinstalada. Nesse caso, quando esse método é invocado isso indica que todas as tarefas da APP 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 sua APP, 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 sua APP.

A permissão android.permission.RECEIVE_BOOT_COMPLETED é para que seja possível persistir com as tarefas já agendadas para sua 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 vincualdas 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 uma 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 APP 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 backend 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 da APP.

O método changeButtonLabel() permite que alteremos o rótulo do Button de activity_main.xml de acordo com o estado do tracking da 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 APP 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 sua APP 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 a APP nele. Clique no Button "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 da APP, até mesmo removê-la do background.

Com isso, passados alguns segundos (de acordo com o definido em setPeriod() e setFlex()) temos os seguintes resultados no backend Web:

Assim finalizamos a configuração de nosso simples Android APP GPS Tracking.

Vídeo com implementação passo a passo do exemplo

No vídeo a seguir é apresentado o passo a passo da implementação da APP 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.

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

Evernote Android Job Library

Simplify-Permissions Library - Utilizada no exemplo do artigo

RxLocationManager Library - Utilizada no exemplo do artigo

Vlw.

Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação.
Email inválido

Relacionado

ConstraintLayout, Melhor Performance no AndroidConstraintLayout, Melhor Performance no AndroidAndroid
Checkout Transparente da Web no AndroidCheckout Transparente da Web no AndroidAndroid
Proguard AndroidProguard AndroidAndroid
Input File no WebView AndroidInput File no WebView AndroidAndroid

Compartilhar

Comentários Facebook

Comentários Blog (4)

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...
Ruan Alves (1) (0)
05/04/2017
Opa, Blz? A um tempo estou utilizando essa opção, pra min resolveu os meus problemas com facilidade, so que surgiu uma breve dúvida: supondo que eu necessite em 3 em 3 minutos pegar a localização do usuário, irei usar o GCM, so que no caso como você mesmo disse ele tem um tempo limite, as vezes para pegar a localização demora, pois usa o GPS sem rede, no caso tem como colocar um delimitador de tempo, donde se ultrapassar o tempo proposto ele cancela a operação? .... vlw ...
Responder
Vinícius Thiengo (0) (0)
08/04/2017
Ruan, tudo bem?

O delimitador de tempo que vejo seria uma Thread secundária aberta com um código de cronometro, SystemClock.slee(), onde quando atingido o tempo de 3 minutos, seria verificado se já há uma nova coordenada, caso não, desativa a conexão do listener de coordenadas.

Pode até mesmo utilizar uma flag para apontar que se a coordenada for entregue ela deve ser descartada. Uma flag que terá o valor alterado no algoritmo de cronometro.

Veja se consegue atualizar as regras de negócios de seu sistema para que mesmo extrapolando os 3 minutos seja possível aceitar as coordenadas retornadas. Para mim essa é a melhor opção. Abraço.
Responder
Ruan Alves (1) (0)
08/04/2017
Opa blz ? ... Entendi, só que no meu caso por exemplo as vezes as coordenadas vem em atraso, mas vem, na verdade não sei o por que a demora .... por isso pensei na ideia do limitador de tempo ...
Responder
Ruan Alves (1) (0)
30/11/2016
Esse método de escrever um artigo detalhado e depois o vídeo, é simplesmente sensacional, no caso já vamos pro vídeo (se precisar) com muita coisa engatinhada ... Showww
Responder