FCM Android - Notificação Personalizada com NotificationCompat [Parte 3]

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

Email inválido.
Blog /Android /FCM Android - Notificação Personalizada com NotificationCompat [Parte 3]

FCM Android - Notificação Personalizada com NotificationCompat [Parte 3]

Vinícius Thiengo
(9712) (10)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloManual de DevOps: como obter agilidade, confiabilidade e segurança em organizações tecnológicas
CategoriaEngenharia de Software
Autor(es)Gene Kim, Jez Humble, John Willis, Patrick Debois
EditoraAlta Books
Edição1ª
Ano2018
Páginas464
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

Opa, blz?

Neste artigo continuaremos a série sobre a API FCM no Android, mais precisamente o FCM junto a um servidor de aplicativos.

Aqui vamos trabalhar com a construção dos objetos de notificação diretamente em nossos códigos Android, sem uso do FCM para isso. Assim conseguiremos trabalhar com algumas características que não são possíveis quando utilizando o atributo notification em nossa push message.

Antes de prosseguir, abaixo deixo os links dos conteúdos anteriores desta série:

Para esta parte três, caso prefira ir direto ao vídeo, siga para a seção Vídeo com a implementação da atualização do projeto de exemplo.

A seguir os tópicos que estaremos estudando:

NotificationCompat, visão geral

A classe NotificationCompat veio para permitir que Android APIs sem suporte a características como BigPicture, BigText e Action Buttons pudessem continuar tendo as notificações criadas sem precisar de código de identificação de versão de sistema operacional em tempo de execução.

As funcionalidades não suportadas não terão efeito, serão ignoradas, mas a notificação ainda será criada, mesmo que com certas limitações. Isso quando utilizando a API NotificationCompat.

Aqui no Blog já falamos sobre a NotificationCompat, mais precisamente no conteúdo sobre push message via GCM API: Notificações com NotificationCompat. Push Message Android - Parte 2.

A partir da próxima seção, antes de apresentar a aplicação da NotificationCompat em nosso projeto de Blog, vamos a uma visão geral sobre algumas das possibilidades quando trabalhando com essa API.

Lembrando que quando utilizando a NotificationCompat nós não mais devemos utilizar o atributo notification da FCM push message. Isso, pois essa API passará para nós, desenvolvedores, toda a responsabilidade de gerência na criação e customização das notificações quando elas já tiverem sido entregues pelos servidores do FCM.

NotificationManager

A instância desta classe, NotificationManager, é que deve ser utilizada para informar ao usuário sobre a notificação.

As configurações são colocadas em uma instância de NotificationCompat.Builder, porém a notificação somente é gerada na área de notificações do device depois que acessamos a instância de NotificationManager e utilizamos a sobrecarga correta do método notify().

Abaixo o código de exemplo de como acessar essa instância e assim enviar uma notificação ao local correto no device:

...
NotificationManager notifyManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notifyManager.notify( id, builder.build() );
...

 

Caso o id seja o mesmo de uma notificação já ativa na área de notificações do device, neste caso a notificação ativa terá uma nova configuração, a mesma da que foi recentemente criada e lançada pela instância de NotificationManager.

É com a instância desta classe que também podemos cancelar notificações. Por exemplo: caso o domínio do problema de sua aplicação permita que mais de uma notificação seja apresentada na barra de status do aparelho, você pode facilmente remover todas elas assim que o usuário clique em apenas uma ou quando ele abre a aplicação pelo launcher icon:

...
NotificationManager notifyManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notifyManager.cancelAll();
...

 

A outra opção simples é cancel() onde você fornece como parâmetro o id da notificação ou a tag e o id. Isso, pois é possível criar uma notificação também com tag e id além de somente com id.

Na documentação de NotificationManager você ainda tem outras opções de métodos e constantes, mas o que vimos anteriormente é o que frequentemente utilizamos desta classe.

Notificação simples

A seguir vamos a criação de uma notificação simples, somente com título, mensagem de corpo e um simples ícone, incluindo a configuração de auto-cancelamento:

...
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon( R.drawable.ic_notificacao_app )
.setTicker( "Título ticker de teste" )
.setContentTitle( "Título de teste" )
.setContentText( "Texto de corpo - teste" )
.setAutoCancel( true );

int id = 1;

NotificationManager notifyManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notifyManager.notify( id, builder.build() );
...

 

Executando o código anterior, nós temos:

O método setTicker(), a princípio, está depreciado desde o Android Lollipop ou Android 5. O que ele faz é apresentar o texto, rapidamente, antes mesmo de o usuário visualizar as notificações por completo na status bar. 

O método setAutoCancel() permite que a notificação seja cancelada assim que acionada, mas em nosso caso, como não criamos uma PendingIntent nada acontecerá. Vamos adicionar o código dessa classe ao script anterior:

...
Intent intent = new Intent(this, MainActivity.class);

PendingIntent pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
);

NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon( R.drawable.ic_notificacao_app )
.setTicker( "Título ticker de teste" )
.setContentTitle( "Título de teste" )
.setContentText( "Texto de corpo - teste" )
.setAutoCancel( true )
.setContentIntent( pendingIntent );

int id = 1;

NotificationManager notifyManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notifyManager.notify( id, builder.build() );
...

 

Com uma PendingIntent podemos não somente ativar uma atividade como também enviar dados por ela, ou seja, toda a configuração enviada na notificação que chegou ao seu aplicativo você pode seguramente coloca-la na Intent que será colocada na intenção pendente da notificação.

Resumidamente uma PendingIntent é uma entidade que nos permiti colocarmos a execução de uma Intent em um tempo futuro.

O método getActivity() de PendingIntent tem como parâmetros:

  • 1º - O componente de contexto ao qual a atividade será iniciada;
  • 2º - O código de requisição privado para a entidade que envia ativa a intenção pendente;
  • 3º - A Intent que tem a configuração da atividade que deverá ser acionada e também alguns dados extras opcionais;
  • 4º - Alguma flag entre: FLAG_ONE_SHOT, FLAG_NO_CREATE, FLAG_CANCEL_CURRENT, FLAG_UPDATE_CURRENT ou qualquer flag suportada em Intent.fillIn. Está flag definirá como será o comportamento de criação ou reutilização de uma PendingIntent.

Há algumas regras de negócio na criação de uma PendingIntent, a principal delas é que está pode ser reutilizada caso os dados fornecidos na configuração de uma nova PendingIntent, incluindo a intenção fornecida como parâmetro, caso esses não se diferenciem o suficiente de alguma PendingIntent já existente.

Mais precisamente: não adianta modificar somente os dados extras da Intent da PendingIntent, isso fará com que a mesma PendingIntent já utilizada seja reutilizada caso a flag definida não seja uma que requisite a criação de uma nova PendingIntent.

Caso a configuração da PendingIntent tenha de ser a mesma, porém uma nova precise ser criada sem que uma já existente tenha de ser removida. Neste caso, na Intent utilizada como argumento, coloque valores diferentes para os campos: action, data, type, class ou categories. Esses são os campos utilizados na comparação em Intent.filterEquals().

Uma outra maneira de forçar a criação de uma nova PendingIntent é utilizar um valor de requestCode diferente dos já utilizados em PendingIntents existentes.

Mas não há uma flag que possa ser utilizada indicando a criação de uma nova PendingIntent?

Na verdade não há uma que somente faça isso. O que temos mais próximo é a solicitação de criação de uma nova PendingIntent, porém com a remoção de outra existente que tenha a configuração similar, tendo no maximo os dados extras da Intent sendo diferentes.

Estou ressaltando essa regra de negócio, pois pode ser que somente os usuários de sua aplicação percebam o impacto dela. Ou seja, a possibilidade de mais de uma notificação sendo apresentada, porém qualquer uma, quando acionada, somente tem o conteúdo da última notificação recebida, isso devido ao reaproveitamento da PendingIntent.

Além do método getActivity() ainda temos: getActivities(), getBroadcast() e getService().

ID de notificação

Se você retornar aos códigos anteriores notará que sempre utilizamos um mesmo id de notificação, um com o valor 1.

Utilizando um ID já existente, o que temos é a atualização da notificação já apresentada na área de notificações. Todos os dados serão referentes a nova notificação criada.

Caso você precise de mais de uma notificação em amostra, isso, por exemplo, devido a categoria a qual ela pertence em seu domínio do problema. Caso precise disso, terá de ter IDs distintos.

O código a seguir tem chances mínimas de gerar uma atualização de notificação:

...
int id = (int) (System.currentTimeMillis() / 1000);

PendingIntent pendingIntent = PendingIntent.getActivity(
this,
id,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
);
...

NotificationManager notifyManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notifyManager.notify( id, builder.build() );
...

 

Utilizei um cast para int, pois long não é aceito e é o tipo retornado por System.currentTimeMillis().

Note que também estamos utilizando o id como o requestCode da PendingIntent, isso para garantirmos que uma nova PendingIntent é que será utilizada e não uma reaproveitada, algo que faria as notificações todas terem os mesmo valores.

Notificação com BigText

Caso queira que o texto do corpo de sua notificação apareça por completo, há a opção de estilo BigTextStyle. Mesmo utilizando esse style não deixe de fornecer a mensagem também em setContentText(), pois algumas versões antigas do Android não têm suporte a BigViews, mais precisamente as versões anteriores ao Android 4.1, Jelly Bean.

Veja o código a seguir:

...
String bigText = "Nessa primeira parte de estudo do Firebase " +
"Cloud Messaging com servidor de aplicativos, vamos " +
"utilizar notificações push partindo de um server PHP " +
"e MySQL.";

NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon( R.drawable.ic_notificacao_app )
.setContentTitle( "Título de teste" )
.setContentText( bigText )
.setContentIntent( pendingIntent )
.setStyle( new NotificationCompat.BigTextStyle().bigText( bigText ) );
...

 

Quando acionando uma notificação com o código anterior, temos algo como:

Notificação com BigPicture e LargeIcon

Um dos motivos de nós estarmos atualizando o projeto de Blog é justamente devido a possibilidade de trabalho com imagens grandes na notificação. Isso, pois imagem traz mais engajamento dos usuários com nossos aplicativos.

Vídeos trazem ainda mais engajamento, porém ainda não é possível reproduzir vídeos na notificação, não até o momento de criação deste artigo.

Podemos utilizar o LargeIcon e / ou o BigPicture. Vamos iniciar com o LargeIcon que diferente do BigPicture não é um style, é apenas uma opção a mais em NotificationCompat.Builder:

...
String url = "https://www.thiengo.com.br/img/post/facebook/650-366/7go5uuk478u4m2us9shqoeg3k0a45ffac74a32604bf42ddc4307f928f0.png";
Bitmap bitmap = null;
try {
bitmap = Picasso.with( this )
.load( url )
.get();
}
catch (IOException e) {
e.printStackTrace();
}

NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon( R.drawable.ic_notificacao_app )
.setContentText( "Texto corpo - teste" )
.setContentTitle( "Título de teste" )
.setContentIntent( pendingIntent )
.setLargeIcon( bitmap );
...

 

Note que diferente de setSmallIcon(), em setLargeIcon() nós temos de fornecer um Bitmap, local ou remoto. Em nosso caso estamos utilizando em conjunto a API Picasso para download da imagem, pois ela está em uma servidor remoto.

Estamos com o modelo de download síncrono de imagem, pois frequentemente as push messages para criação de objetos de notificação são entregues em um Service que não está rodando na Thread principal, logo, não temos o que temer em relação a geração de uma Exception devido a trabalho de alta carga nesta Thread.

Acionando uma notificação com o algoritmo anterior, temos:

A princípio nada demais, mas já não temos de utilizar somente o ícone. Com o BigPicture style fica ainda melhor.

Segue código com o LargeIcon e o BigPicture sendo utilizados:

...
String url = "https://www.thiengo.com.br/img/post/facebook/650-366/7go5uuk478u4m2us9shqoeg3k0a45ffac74a32604bf42ddc4307f928f0.png";
Bitmap bitmap = null;
try {
bitmap = Picasso.with( this )
.load( url )
.get();
}
catch (IOException e) {
e.printStackTrace();
}

NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon( R.drawable.ic_notificacao_app )
.setContentTitle( "Título de teste" )
.setContentText( "Texto corpo - teste" )
.setContentIntent( pendingIntent )
.setLargeIcon( bitmap )
.setStyle( new NotificationCompat.BigPictureStyle().bigPicture( bitmap ) );
...

 

Estou utilizando o mesmo objeto Bitmap, mas você poderia utilizar um para o LargeIcon e outro para o BigPicture, não há problemas quanto a isso.

Criando um objeto de notificação com o script anterior, temos:

Na primeira tela, lock screen, somente a imagem do LargeIcon é apresentada junto ao título e ao texto de corpo da notificação. Na segunda tela ambas as imagens são apresentadas junto ao título da notificação.

Se estiver trabalhando somente com textos e seu aplicativo não é um de mensagens, teste o trabalho com BigPicture (essa também funciona sem o LargeIcon) para ver se consegue aumentar ainda mais o engajamento dos usuários em seu aplicativo.

Notificação com botões de ação

Apesar de ser algo muito específico ao domínio do problema do aplicativo, é possível, e viável em alguns casos, que o usuário consiga diretamente da notificação acionar alguma ação dentro do app.

Para isso temos a possibilidade de colocar botões na notification. Podemos colocar vários, mas em meus testes somente três foram apresentados.

Veja o código a seguir:

...
/* BUTTON - OK, ENTENDI */
Intent okEntendiIntent = new Intent(this, MainActivity.class);
okEntendiIntent.setAction( "action-ok-entendi" );
PendingIntent okEntendiPI = PendingIntent.getActivity(
this,
0,
okEntendiIntent,
0);

NotificationCompat.Action okEntendiAc = new NotificationCompat.Action(
R.drawable.ic_ok_entendi,
"Ok, entendi",
okEntendiPI);

/* BUTTON - ENTRAR */
Intent entrarIntent = new Intent(this, MainActivity.class);
entrarIntent.setAction( "action-entrar" );
PendingIntent entrarPI = PendingIntent.getActivity(
this,
0,
entrarIntent,
0);

NotificationCompat.Action entrarAc = new NotificationCompat.Action(
R.drawable.ic_entrar,
"Entrar",
entrarPI);

NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon( R.drawable.ic_notificacao_app )
.setContentTitle( "Título de teste" )
.setContentText( "Texto corpo - teste" )
.setLargeIcon( bitmap )
.setStyle( new NotificationCompat.BigTextStyle().bigText( bigText ) )
.addAction( okEntendiAc )
.addAction( entrarAc );
...

 

Note que apesar de eu estar utilizando o getActivity() do PendingIntent, é comum acionar componentes que têm processamento no background, isso, pois a princípio os botões de uma notificação têm como função evitar que o usuário tenha de entrar no aplicativo para trabalhar uma configuração que rapidamente pode ser feita por meio desses Buttons.

Aqui utilizei o getActivity() somente como exemplo. As outras opções que permitiriam um processamento no background, sem abrir o app, seriam: getService() e getBroadcast().

No construtor de NotificationCompat.Action temos como argumento:

  • O ID do ícone que deve ser apresentado junto ao rótulo do botão;
  • O rótulo do Button;
  • A PendingIntent que será acionada com o clique no Button.

O método addAction() tem uma sobrecarga na qual podemos adicionar os argumentos de um construtor de NotificationCompat.Action diretamente nele e assim o objeto é criado internamente em NotificationCompat.Builder.

Caso você não queira um ícone no botão, apenas coloque um valor inteiro que não representa um identificador no /res de seu projeto, por exemplo: o valor 0.

Criando um objeto de notificação com o algoritmo anterior, temos:

Notificação com animação de progresso

Temos dois tipos de notificação com progresso:

  • Determinada: onde sabemos o tempo que leva até o término da operação;
  • Indeterminada: onde não temos o dado de tempo de processamento.

A principal diferença está na animação apresentada. Vamos ao primeiro código, com um algoritmo com o tempo de processamento conhecido:

...
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon( R.drawable.ic_notificacao_app )
.setContentTitle( "Título de teste" )
.setContentText( "Texto corpo - teste" );

final int id = (int) (System.currentTimeMillis() / 1000);
final NotificationManager notifyManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

new Thread(
new Runnable() {
@Override
public void run() {
int incr;

for (incr = 0; incr <= 100; incr += 10) {
/*
* COLOCANDO O VALOR MÁXIMO DO PROGRESS NO
* PRIMEIRO PARÂMETRO E ENTÃO O VALOR ATUAL A
* SER REFERENCIADO NA ANIMAÇÃO, ESSE NO
* SEGUNDO PARÂMETRO. COM O TERCEIRO PARÂMETRO
* EM FALSE ESTAMOS INFORMANDO QUE SABEMOS O
* TEMPO E O PROGRESSO DO PROCESSO, OU SEJA:
* ELE É DETERMINÁVEL.
* */
builder.setProgress(100, incr, false);
/* APRESENTA O PROGRESS */
notifyManager.notify(id, builder.build());
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {}
}
/*
* ATUALIZA O TEXTO DA NOTIFICAÇÃO PROGRESSE REMOVE
* A ANIMAÇÃO DE PROGRESS */
builder.setContentText("Download complete").setProgress(0,0,false);
notifyManager.notify(id, builder.build());
}
}
).start();
...

 

O método setProgress() tem como argumentos:

  • O valor máximo possível no progress, comumente o valor é 100 para facilitar o cálculo do dado do segundo argumento;
  • O valor atual do progress, o que aparecerá preenchido;
  • Se o progress deve ser um de animação com tempo determinado ou indeterminado. Quando false indica determinado, quando true indeterminado.

O código notifyManager.notify() por estar utilizando sempre o mesmo ID, apenas atualiza a notificação presente, aqui atualizando a UI do progress.

Quando colocamos setProgress(0, 0, false), ou seja, os dois primeiros parâmetros iguais a 0 e o último como false, estamos na verdade removendo o progress, deixando explícito ao usuário que o processamento foi finalizado com sucesso ou interrompido por problemas.

Criando um objetos de notificação com o algoritmo anterior, temos:

Caso queira manter a barra cheia, simplesmente não invoque o método setProgress(0, 0, false), mude o texto, mas não invoque o código de remoção de progress.

Agora para um processamento não determinado, apenas altere o código anterior no seguinte trecho:

...
builder.setProgress(100, incr, false);
...

 

Altere para:

...
builder.setProgress(100, incr, true);
...

 

Os dois primeiros parâmetros são ignorados quando o terceiro é true. Criando novamente um objeto de notificação, agora com o algoritmo atualizado, temos:

Note que para remover o progress o processo é o mesmo: invocar setProgress(0, 0, false).

Preservando a navegação

Algumas vezes é necessário que o conteúdo referenciado pela notificação seja apresentado em uma atividade específica, até mesmo um Fragment ou Dialog específicos caso você construa a lógica de negócio correta para isso.

Lógica de negócio correta?

Sim. Condicionais que respondem aos dados na Intent da notificação, por exemplo. Assim os componentes corretos serão inicializados.

Em termos de Activity, onde realmente temos um componente que está fortemente ligado a navegação do usuário, diferente de um Service ou de um BroadcastReceiver. Quando se falando de atividade nós podemos:

  • Abrir uma diretamente, sem se importar com a navegação dela. Esse modelo é o que utilizamos até aqui nos exemplos, porém acionando a atividade principal. Note que dessa forma nem sempre temos a navegação preservada, somente quando é a atividade de lançamento que será aberta;
  • Abrir uma diretamente, porém criando as invocações corretas de acordo com os dados da Intent recebida, isso para que a atividade correta possa ser invocada posteriormente. Dessa forma temos sempre a navegação preservada, mas com bastante código envolvido;
  • Abrir a correta diretamente, porém com a pilha de atividades para navegação já definida com a entidade TaskStackBuilder. Dessa forma preservamos a navegação, porém com pouco código.

Os dois primeiros modelos são simples de ao menos imaginar, o primeiro nós até mesmo já utilizamos aqui. Logo, vamos prosseguir com o terceiro. Veja o trecho de código a seguir de um AndroidManifest.xml:

...
<activity
android:name=".view.LoginActivity"
android:label="@string/app_name">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".view.PostContentActivity"
android:parentActivityName=".view.LoginActivity">

<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".view.LoginActivity"/>
</activity>
...

 

Com o código anterior utilizando o atributo android:parentActivityName na tag <activity> de PostContentActivity e assumindo que em nosso código Java estaremos utilizando uma instância de TaskStackBuilder, assumindo isso o que estamos fazendo é permitindo que a ação de back no device quando a atividade aberta for a PostContentActivity, que essa ação acione a atividade em parentActivityName, aqui: LoginActivity.

Lembrando que a atividade referenciada em parentActivityName e em <meta-data> deve estar definida no AndroidManifest.xml na própria tag <activity> dela.

Podemos utilizar uma outra API Java para fazer funcionar a configuração de pai lógico no AndroidManifest.xml, a que temos utilizando o atributo parentActivityName e a tag <meta-data>. Mas aqui vamos prosseguir com a API TaskStackBuilder, pois ela permite a construção de uma pilha sintética, que é o que utilizaremos também em nosso projeto de exemplo.

A tag <meta-data> está ali somente para permitir que aparelhos com Android a partir da versão 4 tenham suporte para essa característica de navegação, pois o atributo parentActivityName foi adicionado somente a partir da API 16.

Agora no código Java temos de realizar a seguinte atualização:

...
Intent intent = new Intent(this, PostContentActivity.class)
;

TaskStackBuilder pilha = TaskStackBuilder.create( this );
pilha.addParentStack( PostContentActivity.class );
pilha.addNextIntent( intent );

...
PendingIntent pendingIntent = pilha.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon( R.drawable.ic_notificacao_app )
.setContentTitle( "Título de teste" )
.setContentText( "Texto corpo - teste" )
.setContentIntent( pendingIntent );
...

 

Clicando na notificação a atividade PostContentActivity será aberta diretamente, mas quando clicado o back button (ou up button), a LoginActivity é que será acionada e não o fechamento do aplicativo.

Note que a ordem de invocação em:

...
pilha.addParentStack( PostContentActivity.class );
pilha.addNextIntent( intent );
...

 

Essa ordem de métodos deve ser obdecida, caso contrário o comportamento posterior ao acionamento da notificação não será como esperado.

E se eu tiver ainda uma outra atividade nessa pilha?

Não há problemas, é possível utilizar esse mesmo formato de construção pilha. Veja o exemplo a seguir no AndroidManifest.xml:

...
<activity
android:name=".view.LoginActivity"
android:label="@string/app_name">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".view.PostContentMoreDetailsActivity"
android:parentActivityName=".view.LoginActivity">

<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".view.LoginActivity"/>
</activity>

<activity
android:name=".view.PostContentActivity"
android:parentActivityName=".view.PostContentMoreDetailsActivity">

<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".view.PostContentMoreDetailsActivity"/>
</activity>
...

 

Acredite, o código Java é exatamente o mesmo do último apresentado, não precisamos de referências diretas a PostContentMoreDetailsActivity no código Java.

Com a notificação criada com a configuração do AndrioidManifest.xml anterior, a atividade PostContentActivity será aberta diretamente, mas o up ou back button acionará primeiro a PostContentMoreDetailsActivity e quando novamente pressionado será acionada a LoginActivity.

Regras de negócio e outros pontos

Como informado anteriormente, essas seções iniciais são somente uma visão geral do que é possível fazer com a API NotificationCompat. Até mesmo o áudio da notificação você pode estar personalizando.

Vale o estudo da API por completo caso queira incrementar ainda mais as notificações de seus aplicativos, e lembre que não é preciso um único modelo de apresentação de notificação e então sempre utiliza-lo até a próxima atualização do App. É possível trabalhar com algoritmos, condicionais, por exemplo, para sempre criar a configuração ideal para a push message entregue.

Fique ciente que mesmo trabalhando a definição de prioridade com NotificationCompat, mesmo colocando uma prioridade alta, o modo soneca, Doze Mode, não é afetado a partir da API 23, Android Marshmallow. A seguir um código de exemplo de definição de prioridade:

...
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setPriority(NotificationCompat.PRIORITY_HIGH)
...

 

Assim podemos prosseguir com as atualizações de nosso projeto de Blog que, acredite, são bem menores que as já aplicadas nas partes um e dois desta série.

Atualização do projeto Web

No lado Web do projeto pouca coisa será alterada, mais precisamente somente um método será atualizado, isso, pois nós não mais trabalharemos com o atributo notification de nosso objeto de push message.

Para ter acesso completo a parte Web do projeto de Blog, entre no GitHub a seguir: https://github.com/viniciusthiengo/blog-android-app-web.

Atualizando a classe AplNotificação

Na classe AplNotificação, classe do pacote /apl, acesse o método getNotificacaoObj().

Se está seguindo a série o código deve estar como a seguir:

...
private function getNotificacaoObj( Post $post )
{
$obj = new stdClass();
$obj->delay_while_idle = true;

$obj->notification = new stdClass();
$obj->notification->title = $post->titulo;
$obj->notification->body = $post->sumario;
$obj->notification->color = '#9E9E9E';
$obj->notification->icon = $post->categoria->getMobIcon();

$obj->data = new stdClass();
$obj->data->post = $post;
$obj->data->categoria = $post->categoria->id;

return $obj;
}
...

 

Sabemos que quando há o atributo notification no objeto de push message a regra de negócio para construção da notificação no Android é à seguir:

  • Caso o aplicativo esteja fechado, o FCM se encarrega de construir o objeto de notificação e envia-lo a bandeja de notificações. Caso o aplicativo esteja aberto, o método onMessageReceived() recebe o conteúdo.

O que queremos é que sempre o método onMessageReceived() receba o conteúdo, pois assim poderemos construir nossa própria configuração de notificação.

Logo, atualize o código de getNotificacaoObj() para:

...
private function getNotificacaoObj( Post $post )
{
$obj = new stdClass();
$obj->delay_while_idle = true;

$obj->data = new stdClass();
$obj->data->post = $post;
$obj->data->categoria = $post->categoria->id;

return $obj;
}
...

 

Agora somente trabalhamos com o atributo de carga útil data. Atributos de notification que não mais estaremos utilizando foram removidos também. São eles: title, body, color e icon.

Isso, pois o objeto do tipo Post que é enviado como carga útil já contém todos os dados necessários as nossas configurações em NotificationCompat no lado Android.

Ok, mas porque manter o atributo categoria, mesmo sabendo que ele também faz parte do objeto do tipo Post?

Nesse caso, para brevidade nas atualizações do código Android. Pois nele não temos a classe Categoria e facilmente conseguiremos manter muito do código já utilizado com este atributo.

Assim podemos partir para os algoritmos no lado Android.

Atualização do projeto Android

Aqui vamos trabalhar duas atualizações, uma onde somente criaremos a notificação com o NotificationCompat e outra onde teremos a abertura da atividade de detalhes de conteúdo do post.

Essa última configuração terá a navegação preservada, onde o acionamento do back button (ou up button) fará com que o usuário navegue para a atividade correta.

Para acesso completo ao projeto do lado Android, entre no GitHub a seguir: https://github.com/viniciusthiengo/AndroidBlogApp.

Atualizando o serviço CustomFirebaseMessagingService

Note que agora temos os dados de título e corpo de mensagem dentro do objeto Post que estará no formato JSON na push message enviada ao Android.

Recapitulando o código atual de onMessageReceived() em CustomFirebaseMessagingService, temos que quando o aplicativo está aberto é utilizada a API Gson para realizar o parser JSON:

...
public void onMessageReceived(RemoteMessage remoteMessage) {
if( PostsActivity.isOpened
&& remoteMessage.getData().size() > 0
&& remoteMessage.getData().containsKey(Post.POST_KEY ) ){

Gson gson = new Gson();
Post p = gson.fromJson( remoteMessage.getData().get(Post.POST_KEY ), Post.class );

PresenterPosts presenter = PresenterPosts.getInstance(this);
presenter.updateListaRecycler( p );
}
}
...

 

O trecho:

...
Gson gson = new Gson();
Post p = gson.fromJson( remoteMessage.getData().get(Post.POST_KEY ), Post.class );
...

 

Agora também é importante ao código que adicionaremos, pois os dados úteis a notificação, mesmo quando o aplicativo estiver fechado, estarão no objeto post, assim, atualizando o código, temos:

...
public void onMessageReceived(RemoteMessage remoteMessage) {
Gson gson = new Gson();
Post p = gson.fromJson( remoteMessage.getData().get(Post.POST_KEY ), Post.class );

if( PostsActivity.isOpened ){
PresenterPosts presenter = PresenterPosts.getInstance(this);
presenter.updateListaRecycler( p );
}
}
...

 

Lembrando que sempre teremos o objeto do tipo Post na push message entregue, devido a isso não precisamos de verificação para saber se há ou não esse objeto na mensagem.

Assim podemos seguramente listar o que nossa notificação deve ter como conteúdo quando o aplicativo estiver fechado:

  • O título do post como o title e o ticker text da notificação;
  • O sumário do post como sendo o body text da notificação;
  • O mesmo ID para notificações de mesma categoria, ou seja, no máximo cinco notificações poderão estar na área de notificações do device;
  • A imagem principal do post sendo apresentada como um LargeIcon e como uma BigPicture (aqui não trabalharemos com BigText).

Segue trecho else do algoritmo de onMessageReceived():

...
public void onMessageReceived(RemoteMessage remoteMessage) {
...
else{
Bitmap bitmap = null;
try {
bitmap = Picasso.with( this )
.load( p.getUriImagem() )
.get();
}
catch (IOException e){}

int idCategoria = Integer.parseInt( remoteMessage.getData().get(Post.CATEGORIA_KEY) );

Intent intent = new Intent(this, LoginActivity.class);
intent.putExtra( Post.CATEGORIA_KEY, String.valueOf(idCategoria) );
intent.putExtra( Post.POST_KEY, p );

PendingIntent pendingIntent = PendingIntent.getActivity(
this,
idCategoria,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
);

NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon( p.getIcon(idCategoria) )
.setTicker( p.getTitulo() )
.setContentTitle( p.getTitulo() )
.setContentText( p.getSumario() )
.setLargeIcon( bitmap )
.setStyle( new NotificationCompat.BigPictureStyle().bigPicture( bitmap ) )
.setContentIntent( pendingIntent );

NotificationManager notifyManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notifyManager.notify( idCategoria, builder.build() );
}
}
...

 

Da maneira que colocamos a carga útil na notificação, somente precisamos agora de colocar estrategicamente o código de anulação de notificação quando o aplicativo for inicializado. Isso, pois somente o uso de setAutoCancel() não é o suficiente, sabendo que podemos ter até cinco notificações ativas.

Note que estamos utilizando uma objeto da classe Post como se ele implementasse um Parcelable e também estamos utilizando um novo método ainda não discutido, getIcon(). Isso trabalharemos na próxima seção.

Antes de prosseguirmos, lembre que o método onMessageReceived() é sempre invocado fora da Thread principal, por isso seguramente podemos realizar o download síncrono de uma imagem nesse método.

Importante ressaltar que em nosso domínio do problema uma imagem sempre deve ser fornecida, por isso não fizemos alguma verificação para saber se ela está ou não presente. Caso em seu domínio a imagem seja opcional, coloque um algoritmo de verificação, pois como construímos nosso código aqui, se uma URL válida de imagem não for fornecida, haverá um erro fatal e a notificação não será criada.

Atualizando a classe Post

Na classe Post do projeto, implemente a Interface Parcelable e adicione o método getIcon() como a seguir:

public class Post implements Parcelable {
...

public int getIcon( int idCategoria ) {
switch( idCategoria ){
case 1:
return R.drawable.ic_categoria_1;
case 2:
return R.drawable.ic_categoria_2;
case 3:
return R.drawable.ic_categoria_3;
case 4:
return R.drawable.ic_categoria_4;
default:
return R.drawable.ic_categoria_5;
}
}

@Override
public int describeContents() {
return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(this.id);
dest.writeString(this.titulo);
dest.writeString(this.uriImagem);
dest.writeString(this.sumario);
}

public Post() {}

protected Post(Parcel in) {
this.id = in.readLong();
this.titulo = in.readString();
this.uriImagem = in.readString();
this.sumario = in.readString();
}

public static final Parcelable.Creator<Post> CREATOR = new Parcelable.Creator<Post>() {
@Override
public Post createFromParcel(Parcel source) {
return new Post(source);
}

@Override
public Post[] newArray(int size) {
return new Post[size];
}
};
}

 

No código PHP até podemos remover o método getMobIcon() da classe Categoria, pois essa tarefa nós transferimos para o lado Android.

Colocando o código de remoção de notificação

Como comentado anteriormente, ainda precisamos remover as notificações assim que o aplicativo é acionado, isso, pois não faz sentido em nossa lógica de negócio manter as notificações presentes quando o app for acionado.

Nosso melhor local para adicionar o algoritmo de remoção de notificação é a classe LoginActivity, pois ela sempre será a primeira a ser aberta. Isso até o momento que mantivermos o trabalho sem a abertura direta de uma atividade de detalhes de post, tema de seções futuras deste artigo.

Então em LoginActivity, mais precisamente no método onCreate(), adicione o código a seguir em destaque:

...
protected void onCreate(Bundle savedInstanceState) {
...

NotificationManager notifyManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notifyManager.cancelAll();
}
...

 

Assim podemos partir para os testes. 

Testes e resultados da primeira atualização

No Android Studio, vá em "Build", logo depois clique em "Rebuild project" e assim execute a aplicação no seu device / emulador de testes. Feche a aplicação ao final.

No dashboard Web coloque dados para um novo post, como a seguir:

Não esqueça de utilizar uma categoria ao qual você esteja cadastrado, no app Android, para receber notificações. Exatamente o que discutimos na parte dois desta série.

Clicando em "Criar post", temos:

A última imagem já representa o aplicativo depois do toque na notificação.

Se você prosseguir com os testes e criar um post quando o aplicativo estiver aberto notará que o comportamento será como esperado, esse novo post será adicionado ao topo da lista de posts.

Criando a atividade de detalhes

O que é mais comum do que somente abrir a atividade principal do projeto é abrirmos uma atividade específica para apresentar diretamente o conteúdo da notificação.

Em nosso domínio do problema poderíamos sim ter esse tipo de regra de negócio, mas preservando a navegação. Vamos então a criação dessa atividade.

Primeiro o arquivo XML de conteúdo. Segue /res/layout/content_post_content.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="br.com.thiengo.androidblogapp.view.PostContentActivity">

<ImageView
android:id="@+id/iv_banner"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop" />

<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp" />
</LinearLayout>

 

A seguir o diagrama do layout anterior:

Então o XML principal da atividade, o responsável por carregar o content_post_content.xml. Segue /res/layout/activity_post_content.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
tools:context="br.com.thiengo.androidblogapp.view.PostContentActivity">

<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:fitsSystemWindows="true"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>

<include layout="@layout/content_post_content" />

</android.support.design.widget.CoordinatorLayout>

 

A seguir o diagrama do layout anterior:

Assim podemos partir para o código Java que por sinal é simples e contém códigos de atribuição de valor as Views do layout e também de remoção de notificação. 

Remoção de notificação?

Sim, pois a partir desta atualização a nova atividade, PostContentActivity, também poderá ser aberta diretamente, podendo existir mais do que uma notificação de nosso aplicativo na área de notificações do device. Assim será preciso o código do NotificationManager de remoção delas.

Segue classe PostContentActivity:

public class PostContentActivity extends AppCompatActivity {
private Toolbar toolbar;
private Post post;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_post_content);

toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
toolbarFontFamily( toolbar );
if( getSupportActionBar() != null ){
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}

NotificationManager notifyManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notifyManager.cancelAll();

post = getIntent().getParcelableExtra(Post.POST_KEY);
initContent();
}

private void toolbarFontFamily(Toolbar toolbar ){
TextView tv = (TextView) toolbar.getChildAt(0);
Typeface font = Typeface.createFromAsset( getAssets(), "Timmana.ttf" );
tv.setTypeface( font );
}

private void initContent(){
TextView tvContent = (TextView) findViewById(R.id.tv_content);
tvContent.setText( post.getSumario() );

ImageView ivBanner = (ImageView) findViewById(R.id.iv_banner);
Picasso.with( this )
.load( post.getUriImagem() )
.into( ivBanner );
}

@Override
protected void onResume() {
super.onResume();
toolbar.setTitle( post.getTitulo() );
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if(id == android.R.id.home) {
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
}

 

Note que utilizei o onResume(), pois o toolbar, com atualização de título direto no método onCreate() não estava tendo efeito, o título não era atualizado como esperado.

Ainda precisamos adicionar essa nova atividade ao AndroidManifest.xml do sistema, incluindo as características que permitiram preservar a navegação. Segue atualização:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="br.com.thiengo.androidblogapp">
...

<application
android:name=".presenter.App"
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...

<activity
android:name=".view.PostContentActivity"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.NoActionBar"
android:parentActivityName=".view.LoginActivity">

<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".view.LoginActivity"/>
</activity>
...
</application>
</manifest>

 

Por que a atividade que será utilizada como atividade pai a PostContentActivity é a LoginActivity e não a PostsActivity?

Isso porque em nosso aplicativo o usuário precisa estar logado para que seja possível acessar a PostsActivity, digo, os dados de usuário dele devem estar já carregados em um objeto do tipo User.

Como sabemos que ele somente receberá notificações depois do primeiro login, podemos seguramente colocar a volta a LoginActivity, pois assim os dados dele serão carregados do backend Web e em seguida a PostsActivity será acionada.

Para o usuário parecerá que ele está voltando diretamente para a PostsActivity.

Atualização do código de notificação, pilha com TaskStackBuilder

Ainda precisamos utilizar um objeto do tipo TaskStackBuilder para conseguirmos construir a pilha de atividades que deverá ser seguida assim que a notificação for acionada pelo usuário.

No serviço CustomFirebaseMessagingService, mais precisamente no método onMessageReceived(), adicione o código em destaque:

public void onMessageReceived(RemoteMessage remoteMessage) {
...
else{
Bitmap bitmap = null;
try {
bitmap = Picasso.with( this )
.load( p.getUriImagem() )
.get();
} catch (IOException e) {
e.printStackTrace();
}

int idCategoria = Integer.parseInt( remoteMessage.getData().get(Post.CATEGORIA_KEY) );

Intent intent = new Intent(this, PostContentActivity.class);
intent.putExtra( Post.CATEGORIA_KEY, String.valueOf(idCategoria) );
intent.putExtra( Post.POST_KEY, p );

TaskStackBuilder stack = TaskStackBuilder.create( this );
stack.addParentStack( PostContentActivity.class );
stack.addNextIntent( intent );

PendingIntent pendingIntent = stack.getPendingIntent( idCategoria, PendingIntent.FLAG_UPDATE_CURRENT );

NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon( p.getIconByCategoria(idCategoria) )
.setTicker( p.getTitulo() )
.setContentTitle( p.getTitulo() )
.setContentText( p.getSumario() )
.setLargeIcon( bitmap )
.setStyle( new NotificationCompat.BigPictureStyle().bigPicture( bitmap ) )
.setContentIntent( pendingIntent );

NotificationManager notifyManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notifyManager.notify( idCategoria, builder.build() );
}
}

 

Com isso podemos partir a última atualização.

Atualização PostsAdapter

Para que seja possível acessar a atividade de detalhes também quando houver um clique em algum item da lista de posts, temos de atualizar a classe adaptadora, PostsAdapter. Segue:

class PostsAdapter extends RecyclerView.Adapter<PostsAdapter.ViewHolder> {
...

class ViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener {
...

ViewHolder(View itemView) {
...

itemView.setOnClickListener( this );
}
...

@Override
public void onClick(View view) {
Intent intent = new Intent(context, PostContentActivity.class);
intent.putExtra( Post.POST_KEY, posts.get(getAdapterPosition()) );
context.startActivity( intent );
}
}
...
}

 

Assim podemos ir aos testes.

Testes e resultados da segunda atualização

Como realizado na primeira bateria de testes: entre em "Build", clique em "Rebuild project" e logo depois execute o aplicativo em seu emulador ou device de testes.

Não esqueça de aceitar todas as categorias de notificações em NotificacaoActivity para que seus testes não tenham atrasos caso você selecione, no dashboard Web, uma categoria não marcada no device de testes.

No dashboard Web crie um novo post como a seguir:

Feche o aplicativo no device de testes e então clique em "Criar post", assim teremos:

A primeira imagem é a notificação sendo apresentada. A segunda é o aplicativo já aberto depois do acionamento da notificação. Por fim, a terceira imagem é já com o back ou up button acionado. Notará que na terceira imagem, apesar de termos a lista de posts a LoginActivity foi invocada primeiro, terá um delay de login assim que o back button for acionado.

Corrigindo um erro lógico do projeto

Note os condicionais do método onMessageReceived():

...
public void onMessageReceived(RemoteMessage remoteMessage) {
...

if( PostsActivity.isOpened ){
...
}
else {
...
}
}
...

 

O que temos é que caso a LoginActivity ou a PostContentActivity esteja aberta quando uma nova push message de nossa app for entregue, essa se tornará uma notificação na área de notificações do device, mesmo sabendo que isso não deveria acontecer, pois o aplicativo está aberto.

O que vamos fazer é adicionar alguns códigos a essas atividades para assim atualizar o else do trecho lógico de onMessageReceived().

Primeiro o adicione em LoginActivity o código em destaque:

public class LoginActivity extends AppCompatActivity {
...
public static boolean isOpened;
...

@Override
protected void onStart() {
super.onStart();
isOpened = true;
}

@Override
protected void onStop() {
super.onStop();
isOpened = false;
}
}

 

Como sabemos que o valor inicial de um boolean é false, não precisamos pré-definir esse valor em nosso isOpened.

Agora o código de PostContentActivity, que por sinal é o mesmo:

public class PostContentActivity extends AppCompatActivity {
...
public static boolean isOpened;
...

@Override
protected void onStart() {
super.onStart();
isOpened = true;
}

@Override
protected void onStop() {
super.onStop();
isOpened = false;
}
}

 

Por fim o novo código condicional de onMessageReceived() em CustomFirebaseMessagingService:

...
public void onMessageReceived(RemoteMessage remoteMessage) {
...
if( PostsActivity.isOpened ){
...
}
else if( !LoginActivity.isOpened && !PostContentActivity.isOpened ){
...
}
}
...

 

Com isso a criação de objetos de notificação em momentos indevidos não mais correrá.

Assim finalizamos a parte três de nossa série. Vamos sim prosseguir com ela, pois ainda faltam alguns bons conteúdos a serem cobertos.

Para receber em primeira mão os novos artigos e vídeos do Blog, se inscreva na lista de emails logo ao lado, não envio spams a ti, somente conteúdos relacionados com o Blog.

Não deixe de se inscrever também no canal do Blog: Thiengo [Calopsita].

Vídeo com a implementação da atualização do projeto de exemplo

A seguir o vídeo com o código de atualização do projeto Android e Web com notificação push FCM, atualização para uso da API NotificationCompat:

Para acesso aos conteúdos completos do projeto de Blog, incluindo o backend Web, entre nos GitHub a seguir:

Conclusão

Delegar o trabalho de criação de objeto de notificação no Android para a API do FCM é algo desejável, tendo em mente que muito da codificação será trabalhada internamente por essa API. Mas em alguns casos, até mesmo para testes de engajamento, nós precisamos de características não atendidas pelo atributo notification de nossa push message junto a API FCM.

Para isso utilizamos a API NotificationCompat junto a outras classes para termos a apresentação de banners, textos de resumo completos, mensagens aninhadas de notificações em uma única notificação, botões de resposta rápida, ...

O controle de configuração da notificação fica completamente no código construído por nós, podemos até mesmo aplicar um delay da apresentação da notificação.

Mesmo que em seu aplicativo a criação de objetos de notificação pela API do FCM seja o suficiente, como notificações também estão fortemente ligadas a maior engajamento, vale o teste com elas contendo banners, botões ou textos grandes, isso para saber se o engajamento aumenta.

Caso você tenha dúvidas ou sugestões, não deixe de comentar logo abaixo. Não se esqueça também de se inscrever na lista de emails do Blog logo ao lado ou abaixo do artigo.

Abraço.

Fontes

Documentação Android: NotificationCompat

Documentação Android: NotificationCompat.Builder

Building a Notification

Preserving Navigation when Starting an Activity

Using Big View Styles

Displaying Progress in a Notification

Documentação Android: tag <activity>

Providing Up Navigation

Documentação Android: PendingIntent

What is an Android PendingIntent? Resposta: Shakeeb Ayaz

Documentação Android: TaskStackBuilder

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
Email inválido

Relacionado

Como Construir Aplicativos Android Com HTML e JSOUPComo Construir Aplicativos Android Com HTML e JSOUPAndroid
Construindo a Política de Privacidade de Seu Aplicativo Android [Agora Obrigatório]Construindo a Política de Privacidade de Seu Aplicativo Android [Agora Obrigatório]Android
FCM Android - Domínio do Problema, Implementação e Testes Com Servidor de Aplicativo [Parte 1]FCM Android - Domínio do Problema, Implementação e Testes Com Servidor de Aplicativo [Parte 1]Android
FCM Android - Relatório e Notificação Por Tópicos [Parte 2]FCM Android - Relatório e Notificação Por Tópicos [Parte 2]Android

Compartilhar

Comentários Facebook

Comentários Blog (10)

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...
Ari melo (1) (0)
21/08/2019
Opa, e ai. Tenho um app funcionando perfeitamente e o push notification também, minha dúvida é: No app ele tem o activity principal o MainActivity, se eu mandar o push para abrir uma activity ele vai abrir o MainActivity numa boa. Como eu verifico no MainActivity que ele foi aberto por um push Notification?
Responder
Vinícius Thiengo (0) (0)
24/08/2019
Ari, tudo bem?

Você consegue realizar essa verificação enviando, junto a notificação, algum dado que poderá ser capturado na MainActivity por meio do método getIntent().

Se me lembro bem eu faço algo similar a isso na segunda aula desta série sobre o FCM: https://www.thiengo.com.br/fcm-android-relatorio-e-notificacao-por-topicos-parte-2

Abraço.
Responder
Ari melo (1) (0)
26/08/2019
Olá, tudo sim.

Massa, Consegui resolver meu problema, vlw.
Responder
Victor (0) (0)
10/06/2019
Acompanhei e fiz exatamente igual nesse artigo e vídeo. A notificação simplesmente não aparece qnd o app está aberto. Passa pelos códigos da notificação e não exibe ela. Testei fora do firebase tb n funciona. Qual seria meu erro? Fiz com notification tb n funcionou. Qnd o app está fechado ele exibe sempre a notificação padrão vinda do FIREBASE, estou fazendo algo de errado? Antes estava funcioando normal, só q estava usando a versão do SDK 22 qnd subi pra 27 começou a dar esse problema.

public class CustomFirebaseMessagingService extends FirebaseMessagingService{
    /**
     * 0 - Tarefas.<p>
     * 1 - Autorização.</p>
     * 2 - Vistoria.<p>
     * 3 - Avaliação.</p>
     * 99 - Notificacao Geral.
     */
    private String modulo;
    private String codigoRegistro;
    private Bitmap imagem;

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        modulo = (remoteMessage.getData().get("modulo"));
        codigoRegistro = (remoteMessage.getData().get("codigoRegistro"));
        ParametroSingleton.ID_TABELA = Integer.parseInt(codigoRegistro);

        showNotification(remoteMessage.getNotification().getTitle(), remoteMessage.getNotification().getBody());
        //Log.d(TAG, "From: " + remoteMessage.getFrom());
        //Log.d(TAG, "Notification Message Body: " + remoteMessage.getNotification().getTitle());
        //Log.d(TAG, "Notification Message Body: " + remoteMessage.getNotification().getBody());
    }

    private void showNotification(String titulo, String mensagem) {
        Intent i = null;

        ParametroSingleton.CONTROLE_NOTIFICACAO = true;

        imagem = null;
        if(ParametroSingleton.ID_TABELA > 0){
            switch (Integer.parseInt(modulo)){
                case 0: //Tarefas.
                    imagem = BitmapFactory.decodeResource(getResources(),
                            R.mipmap.ic_servico);
                    i = new Intent(this, Tela_OrdemServico_Editar.class);
                    i.putExtra("ID", Integer.parseInt(codigoRegistro));
                    break;
                case 1: //Autorização.
                    imagem = BitmapFactory.decodeResource(getResources(),
                            R.mipmap.ic_autorizacao);
                    i = new Intent(this, Tela_Autorizacao_Editar.class);
                    i.putExtra("ID", Integer.parseInt(codigoRegistro));
                    break;
                case 2: //Vistoria.
                    imagem = BitmapFactory.decodeResource(getResources(),
                            R.mipmap.ic_vistoria);
                    i = new Intent(this, Tela_Vistoria_Editar.class);
                    i.putExtra("ID", Integer.parseInt(codigoRegistro));
                    break;
                case 3: //Avaliação.
                    imagem = BitmapFactory.decodeResource(getResources(),
                            R.mipmap.ic_avaliacao);
                    i = new Intent(this, Tela_Avaliacao.class);
                    i.putExtra("ID", Integer.parseInt(codigoRegistro));
                    break;
                case 99:

                    break;
                default:
                    i = new Intent(this, Tela_OrdemServico_Editar.class);
                    i.putExtra("ID", Integer.parseInt(codigoRegistro));
                    break;
            }
        }else{
            if(Integer.parseInt(modulo) != 99){
                i = new Intent(this, Tela_OrdemServico_Editar.class);
            }
        }

        if(Integer.parseInt(modulo) != 99){
            i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
            Notification.Builder builder = null;
            builder = new Notification.Builder(this)
                    .setAutoCancel(true)
                    .setContentTitle(titulo)
                    .setContentText(mensagem)
                    .setStyle(new Notification.BigTextStyle()
                            .bigText(mensagem))
                    .setSmallIcon(R.mipmap.ic_concluida)
                    .setLargeIcon(imagem)
                    //.setNumber(2)
                    .setDefaults(Notification.DEFAULT_ALL)
                    //.addAction(R.mipmap.ic_concluida, "Visualizar", pendingIntent)
                    .setContentIntent(pendingIntent);

            NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            manager.notify(0, builder.build());
        }else{
            Notification.Builder builder = null;
            builder = new Notification.Builder(this)
                    .setAutoCancel(true)
                    .setContentTitle(titulo)
                    .setContentText(mensagem)
                    .setStyle(new Notification.BigTextStyle()
                            .bigText(mensagem))
                    //.setNumber(3)
                    .setSmallIcon(R.mipmap.ic_concluida)
                    .setDefaults(Notification.DEFAULT_ALL);

            NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            manager.notify(0, builder.build());
        }
        if(imagem != null){
            imagem.recycle();
        }
    }
}
Responder
Vinícius Thiengo (0) (0)
16/08/2019
Victor, tudo bem?

Na época em que eu criei os conteúdos sobre o Firebase Cloud Messaging (FCM) não havia regras de negócio para a entidade NotificationChannel, alias, essa classe nem mesmo existia.

Aparentemente, a partir do Android 8, Oreo ou Android API 26, é necessário ao menos uma definição de NotificationChannel. Além de ser permitido somente o payload data{}, nada de notification{} mais.

Veja essa discussão: https://stackoverflow.com/a/51517343/2578331

Abraço.
Responder
Douglas (1) (0)
06/05/2019
Olá vinicius,

Estou a algum tempo procurando uma forma de enviar notificações, porem essas notificações serão disparadas com uma condição realizada pelo usuário.

como posso fazer isso  ? uso Firebase   , e estou tentando com Firebase Cloud Messaging
Responder
Vinícius Thiengo (1) (0)
14/05/2019
Douglas, tudo bem?

Você já está com a API correta, Firebase Cloud Messaging (FCM), agora é trabalhar a lógica de negócio em seu back-end para conseguir vincular o disparo de notificação somente quando uma situação XYPZ ocorrer.

No projeto deste artigo eu tenho algo que certamente vai lhe ajudar, ao menos ajudar a pensar em como funcionaria em seu caso.

O artigo acima é a terceira (e última) aula deste projeto. Nele eu desenvolvo um app Android que recebe notificações de acordo com novas postagens criadas, postagens que são criadas em um dashboard Web.

Ou seja, há uma lógica de negócio para que os disparos de notificações ocorram.

Assim, recomendo que você estude todas as aulas deste projeto (pelo artigo e pelos vídeos) para entender como tudo funciona e então partir para a atualização de seu app.

Seguem as aulas:

-> FCM Android - Domínio do Problema, Implementação e Testes Com Servidor de Aplicativo [Parte 1]: https://www.thiengo.com.br/fcm-android-dominio-do-problema-implementacao-e-testes-com-servidor-de-aplicativo-parte-1

-> FCM Android - Relatório e Notificação Por Tópicos [Parte 2]: https://www.thiengo.com.br/fcm-android-relatorio-e-notificacao-por-topicos-parte-2

-> FCM Android - Notificação Personalizada com NotificationCompat [Parte 3]: https://www.thiengo.com.br/fcm-android-notificacao-personalizada-com-notificationcompat-parte-3

Surgindo dúvidas, pode perguntar.

Douglas, não deixe de também acompanhar a nova série gratuita do Blog e canal. Nela estamos desenvolvendo, do zero, um aplicativo Android de mobile-commerce.

Muitas dicas e dúvidas serão tratadas, mesmo para aqueles que estão iniciando do completo zero no Android.

A última aula liberada até então é a aula 11, do link a seguir:

-> Como Criar a UI de Configurações de Conta de Usuário - Android M-Commerce: https://www.thiengo.com.br/como-criar-a-ui-de-configuracoes-de-conta-de-usuario-android-m-commerce

Abraço.
Responder
Leandro Mariano (1) (0)
05/09/2018
Ótimo artigo. Parabéns e sucesso.
Responder
marcos vinicius (1) (0)
20/03/2018
Olá vinicius, beleza?

Cara eu to tentando há um tempo, colocar um valor (url) no meu Data, e abrir ele externamente, tipo, colocar um link do youtube, e ele abrir o link no browser ou no youtube, eu vi em vários lugares, o tratamento, mas ainda não consegui fazer, o último que tentei foi esse código.


public class MyFirebaseMessagingService extends FirebaseMessagingService {

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {

            String link=remoteMessage.getData().get("link");


        sendNotification(link);
    }

    private void sendNotification(String link) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(Uri.parse(link));
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        startActivity(intent);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
                PendingIntent.FLAG_ONE_SHOT);
        Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
                .setAutoCancel(true)
                .setContentIntent(pendingIntent)
                .setSound(defaultSoundUri)
                .setContentText(link);
        NotificationManager notificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());

    }
}
Responder
Vinícius Thiengo (1) (0)
25/03/2018
Marcos Vinicius, tudo bem aqui.

A configuração de seu onMessageReceived() está errada, até mesmo startActivity() há nela. Tente algo similar ao algoritmo do link a seguir, digo, como conteúdo do método onMessageReceived():

https://pastebin.com/ym3pCZef

Para a abertura do URL no YouTube ou navegador mobile, coloque no onCreate(), da atividade de abertura via notificação, o algoritmo do link a seguir:

https://pastebin.com/wNv2B7hd

Abraço.
Responder