Como Colocar Notificações Bolha em Seu Aplicativo Android

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog. Você receberá um email de confirmação. Somente depois de confirma-lo é que poderei lhe enviar os conteúdos exclusivos.

Email inválido.
Blog /Android /Como Colocar Notificações Bolha em Seu Aplicativo Android

Como Colocar Notificações Bolha em Seu Aplicativo Android

Vinícius Thiengo15/01/2017
(2553) (2) (202) (41)
Go-ahead
"Lembremo-nos de que nosso único limite é aquele que fixamos em nossa mente."
Napoleon Hill
Receitas Android
Capa do livro Receitas Para Desenvolvedores Android
TítuloReceitas Para Desenvolvedores Android
CategoriaDesenvolvimento Android
AutorVinícius Thiengo
Edição
Ano2017
Capítulos20
Páginas934
Acessar Livro
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
Acessar Livro
Conteúdo Exclusivo
Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Opa, blz?

Neste artigo vamos colocar, passo a passo, notificações bolha em um aplicativo Android, mais precisamente, colocar essas notificações em um aplicativo onde o domínio do problema é um game de Poker.

Notificações bolha ficaram famosas devido ao uso delas no aplicativo de mensagens do Facebook, o Facebook Messenger. Veja abaixo um comparativo do Facebook Messenger com o aplicativo que construiremos neste artigo:

Note que essa técnica utilizada para notificações bolha é totalmente independente do contexto de notificações, digo, a técnica de desenharmos Floating Windows em cima de qualquer outra view, mesmo quando não é a aplicação de origem que está no topo da pilha de atividades.

Aqui vou apresentar o "desenho de Floating Windows sobre qualquer aplicação" no contexto de notificação, que como já informado, é onde essa técnica mais ficou conhecida.

O artigo, como os anteriores, está completo, então mesmo se você já tiver visto o vídeo de implementação, que também está completo, não deixe de ler todo o conteúdo... e, obviamente, se inscrever na lista de emails do Blog, logo acima.

Abaixo os tópicos que serão abordados:

A técnica maliciosa, Tapjacking

Antes de apresentar toda a configuração e testes com a notificação bolha, vamos ao problema conhecido como Tapjacking. Problema que faz uso da mesma técnica de Floating Windows.

Esse problema é antigo e comumente utilizado para fazer com que o usuário ative, por exemplo, configurações de liberação de acesso root e liberação para instalação de recursos de fontes desconhecidas.

Com as permissões corretas e as intenções erradas, um atacker consegue com um simples aplicativo de game, por exemplo, fazer com que você pense estar jogando quando na verdade está com uma View sobreposta na aplicação de configurações, onde os botões estão estrategicamente posicionados.

Para melhor compreender o que foi dito anteriormente, veja o vídeo a seguir. Está em inglês, mas é possível entender o problema:

Essa é somente uma das possíveis tramas. Uma outra é a apresentação de um formulário similar ao de aplicativos famosos, solicitando que você entre com suas credenciais, login e senha.

Obviamente que existem muitos aplicativos que fazem o uso correto da técnica de Floating Windows, muitas já utilizam essa técnica bem antes do Facebook Messenger.

Sendo assim, apresentado um possível problema, você deve estar cheio de dúvidas, principalmente em relação ao que o Google fez para essa técnica maliciosa.

Vamos prosseguir, pois essa resposta vem com o estudo do histórico da permissão SYSTEM_ALERT_WINDOW.

Window, WindowManager e a permissão SYSTEM_ALERT_WINDOW

A classe Window é uma daquelas entidades onde a melhor maneira de explica-la é colocando-a em ação. Abaixo a imagem de uma aplicação aberta, mais precisamente, a atividade principal dela:

Agora, indo no Android Studio em Tools > Android > Android Device Monitor. Logo depois clicando na aba "Hierarchy Views" e em seguida clicando no package da aplicação aberta, temos: 

No meio e no lado direito da Hierarchy Views você já deve ter noção que é exatamente o que o nome indica: a hierarquia de Views da aplicação aberta, mais precisamente, do package selecionado no menu esquerdo, menu Windows.

Esse menu é o que nos importa aqui, ele permite que possamos ver todas as Windows presentes na tela do device. Ou seja, se temos algo sendo apresentado na tela, seguramente sabemos que esse "algo" está dentro de uma Window.

Esta última é nada mais nada menos que uma área retangular que contém uma série de entidades para que a visualização de elementos gráficos seja possível.

O status bar e o navigation bar nativos do device são outras duas entidades que na verdade estão sendo apresentadas em suas próprias Windows.

O menu esquerdo da imagem anterior está apresentando as Windows em ordem de relevância, as mais no topo é a mais relevante, e assim segue.

Agora, da aplicação de guia comercial apresentada anteriormente, vamos abrir uma outras Activity dela. Segue:

Veja como fica o menu de Windows no Hierarchy Views:

Uma nova Window é criada. Isso, pois cada Activity tem sua própria Window. Fique tranquilo quanto a complexidade de gerenciamento e criação de Windows no Android, pois essas são administradas pelo próprio Android.

Somente quando queremos trabalhar com Windows de maneira não convencional, como faremos neste artigo, é que devemos trabalhar de maneira explicita o código das Windows, mais precisamente, trabalhar via WindowManager.

Note que no decorrer do conteúdo vamos voltar a Hierarchy Views Tool para mostrar o que ocorre quando criamos notificações bolha com a técnica de Floating Window.

Com a explicação do que é uma Window, seguramente podemos afirmar que o WindowManager é um serviço do sistema Android que nos permite comunicação com Windows. O WindowManager é a interface de comunicação.

Para acessar o WindowManager devemos fazer como no código abaixo:

...
WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
...

 

Simples, certo? Principalmente se está familiarizado com códigos utilizando o LayoutInflater para inflar layouts.

Ok, e a permissão SYSTEM_ALERT_WINDOW? Mais precisamente: com os problemas citados o que o Google fez sobre eles?

A permissão SYSTEM_ALERT_WINDOW é uma permissão de sistema é nos garante a possibilidade de criarmos Floating Windows.

Por ser uma permissão de sistema, essa é mais crítica que as dangerous permissions discutidas no artigo, com vídeo, do link a seguir: Sistema de Permissões em Tempo de Execução, Android M.

Como assim: mais crítica?

A partir do Android Marshmallow, API 23, teríamos de apresentar um dialog de permissão para o usuário, isso se a SYSTEM_ALERT_WINDOW fosse uma dangerous permission.

Por ser uma permissão de sistema, o máximo que conseguimos fazer é encaminharmos o usuário para a área de liberação dessa permissão, área do sistema de configuração do próprio Android, onde o texto é apresentado com as palavras do próprio sistema.

Ou seja, o usuário pode não gostar do que pode acontecer, de acordo com o texto, e acabar por não liberar a permissão.

Note que antes da API 23 a permissão SYSTEM_ALERT_WINDOW somente precisa estar declarada no AndroidManifest.xml do projeto:

...
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
...

 

Assim ela será apresentada ao usuário antes da instalação do aplicativo. Se a instalação ocorrer a permissão estará concedida.

E, para a felicidade de alguns, o Google, mesmo nas versões a partir da API 23, libera, sem necessidade de solicitações extras, a permissão das Floating Windows. Isso somente com nós, developers, colocando a declaração dela no AndroidManifest.xml.

Mas por que isso?

Podemos tentar adivinhar, pois o motivo, até onde foram minhas pesquisas, não foi divulgado. Então vamos a "adivinhação".

Primeiro. Quão comum é o uso do Tapjacking? Mesmo sabendo do problema, eu, particularmente, somente o conheci depois de focar nos estudos do conteúdo deste artigo. Além de não lembrar de ter sido um "cliente" desse tipo de técnica.

Segundo. Qual a probabilidade de um usuário comum ativar uma configuração diretamente no aplicativo de configurações do Android depois de já ter seu aplicativo em uso?

Não faço a mínima ideia, mas vou "chutar" baseando-me na pesquisa de "campos de formulários" realizada por Tim Ash e publicada em Otimização da Página de Entrada.

... ou seja, a probabilidade é mínima, pois há ainda mais um passo para o usuário conceder a permissão, digo, um passo fora do aplicativo que ele abriu. Quanto mais passos, menor o número de conversões.

Em nosso caso a conversão é: o usuário ativar a permissão SYSTEM_ALERT_WINDOW direto do aplicativo nativo de configurações de sistema.

Terceiro. O IOS continua evoluindo, incluindo o antigo badge icon de contador de notificações que até hoje não conseguimos colocar de maneira trivial nos launcher icons do Android. E o Android, neste caso de Floating Windows, estaria limitando os desenvolvedores por causa de alguns, minoria, mal intencionados.

Enfim, foram chutes, mas podem ser os motivos da liberação do SYSTEM_ALERT_WINDOW, isso sem passos extras além da declaração no AndroidManifest.xml.

Porém, agora para a felicidade dos usuários, alguns aparelhos, muito provavelmente devido a modificação da configuração padrão do Android. Alguns bloqueiam certas tarefas do usuário, principalmente as que necessitam do toque, caso a configuração que permite Floating Windows esteja ativada.

Neste caso, o sistema solicita ao usuário que desative essa configuração para continuar com a tarefa bloqueada. De qualquer forma, vamos seguir no caminho correto da força, vamos colocar o código de ativação pelo usuário, isso quando a aplicação for compilada com a API 23 ou superior.

Assim podemos prosseguir com nosso projeto de exemplo.

Projeto Android de exemplo

Igualmente venho fazendo nos últimos artigos aqui do Blog, vamos trabalhar o conteúdo principal do artigo, Floating Window, em um aplicativo que simula um contexto real, digo, algo que poderia ser o seu aplicativo em produção, por exemplo.

O projeto completo, incluindo as imagens, você pode acessar diretamente no GitHub do código de exemplo em: https://github.com/viniciusthiengo/PockerHijack.

De início, crie um novo projeto Android com o nome Poker Hijack. Escolha um projeto inicial com a Activity que tenha o NavigationDrawer ja configurado.

Ao final da configuração padrão, que não faz parte do entendimento do Floating Window, vamos ter um layout similar ao abaixo:

Nossa configuração de pacotes será a seguinte:

Com isso podemos prosseguir com os códigos.

Configurações Gradle

Vou apresentar os dois Gradles do projeto, mas somente um passou por atualizações.

Segue código do Gradle Top Level, ou build.gradle (Project: PokerHijack):

buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
}
}

allprojects {
repositories {
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

 

Acima a configuração é padrão. A seguir o Gradle APP / Module Level, ou build.gradle (Module: app):

apply plugin: 'com.android.application'

android {
compileSdkVersion 25
buildToolsVersion "24.0.3"
defaultConfig {
applicationId "br.com.thiengo.pockerhijack"
manifestPlaceholders = [manifestApplicationId: "${applicationId}",
onesignal_app_id: "d65b9843-49ce-41b1-8c3b-eb6a8d59cc02",
onesignal_google_project_number: "113519449045"]
minSdkVersion 11
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.1.0'
compile 'com.android.support:design:25.1.0'
testCompile 'junit:junit:4.12'
compile 'com.android.support:cardview-v7:25.1.0' /* Para o CARDVIEW */
compile 'com.onesignal:OneSignal:3.+@aar' /* Para o ONESIGNAL */
compile 'com.google.android.gms:play-services-gcm:10.0.1' /* Para o ONESIGNAL */
compile 'com.mikhaellopez:circularimageview:3.0.2' /* Para o CIRCULARIMAGEVIEW */
compile 'com.squareup.picasso:picasso:2.5.2' /* Para o PICASSO */
}

 

Para utilizarmos o WindowManager e posteriormente criarmos nossas Floating Windows, não precisamos de libraries externas.

Porém, em nosso contexto de poker, para um projeto mais próximo de um aplicativo em produção, vamos utilizar algumas, vinculadas com as seguintes entidades: OneSignal; CardView; CircularImageView; e Picasso.

Configurações AndroidManifest

A seguir vamos a configuração inicial do AndroidManifest.xml, digo isso, pois vamos atualiza-lo ainda mais no decorrer do conteúdo. Segue:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.pockerhijack">

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

<application
android:name=".CustomApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

 

Escolhi já deixar a permissão SYSTEM_ALERT_WINDOW configurada, pois no caso de devices com a API abaixo da 23 essa permission no AndroidManifest.xml é o suficiente para a criação Floating Windows.

Configurações de estilo

Abaixo os arquivos padrões de um novo projeto no Android Studio, arquivos de estilo. Iniciando com /values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#9E9E9E</color>
<color name="colorPrimaryDark">#616161</color>
<color name="colorAccent">#FF5252</color>
</resources>

 

Caso ainda não conheça, eu frequentemente escolho as cores dos aplicativos dos artigos no Material Design Palette.

Prosseguindo, o XML de dimensões, /values/dimens.xml:

<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="nav_header_vertical_spacing">16dp</dimen>
<dimen name="nav_header_height">160dp</dimen>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources>

 

O XML de strings, /values/strings.xml:

<resources>
<string name="app_name">Poker Hijack</string>

<string name="navigation_drawer_open">Open navigation drawer</string>
<string name="navigation_drawer_close">Close navigation drawer</string>

<string name="action_settings">Settings</string>
<string name="title_activity_notification">Config. notificação</string>

<string name="notification_ok">"As notificações em bolha estão ativadas para esse aplicativo."</string>
<string name="notification_denied">"As notificações em bolha não estão ativadas ainda. Para obter mais desse aplicativo, ative-as clicando no botão abaixo e atualizando as configurações que serão apresentadas."</string>
</resources>

 

O XML de estilo, /values/styles.xml:

<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light">
<item name="android:windowBackground">@drawable/background</item>

<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>

<style name="AppTheme.NoActionBar">
<item name="android:windowBackground">@drawable/background</item>

<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>

<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

 

E por fim o XML de estilo para APIs a partir da versão 21, /values-v21/styles.xml:

<resources>
<style name="AppTheme.NoActionBar">
<item name="android:windowBackground">@drawable/background</item>

<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>

 

Lembrando que as imagens você acessa no GitHub do projeto.

Configurações do sistema de notificação OneSignal

Com o OneSignal não vou entrar nos detalhes de configuração do lado Web dele, na verdade vou somente colocar as entidades que utilizam classes e interfaces dessa API. Isso, pois tenho aqui no Blog tenho um artigo completo sobre o OneSignal: OneSignal Para Notificações em Massa no Android.

Caso não o conheça, não entre nele agora, depois deste artigo você pode estuda-lo, pois o entendimento é simples e o que mostrarei aqui é somente de suporte ao exemplo, nada complexo de entender somente observando o uso.

O OneSignal em si não tem relação direta com a criação de Floating Windows, digo, como a permissão SYSTEM_ALERT_WINDOW tem. Mas vai fazer com que nosso domínio do problema funcione.

Caso tenha algum outro sistema de notificações push você que queira utilizar, siga com o de sua preferência, não há problemas.

Vamos então a nossa CustomApplication:

public class CustomApplication extends Application
implements OneSignal.NotificationReceivedHandler {
@Override
public void onCreate() {
super.onCreate();
OneSignal
.startInit(this)
.setNotificationReceivedHandler(this)
.init();
}

@Override
public void notificationReceived(OSNotification notification) {
/* TODO */
}
}

 

Note que no decorrer do conteúdo vamos voltar a essa classe para colocar ainda mais código nela, código responsável por realizar o parser no objeto JSON retornado do sistema OneSignal e também pela ativação do serviço de notificação em bolha.

Se conhece os códigos do OneSignal você deve estar se perguntando porque que coloquei a implementação do OneSignal.NotificationReceivedHandler dentro da CustomApplication e não em uma classe separada.

Esta escolha é devido ao contexto da aplicação, Application, que precisaremos em códigos que ainda serão acrescentados.

Caso em uma classe não sendo um Service, Activity, Fragment ou Application não conseguiríamos de maneira trivial o acesso ao objeto de contexto.

Classes do domínio do problema

As classes de domínio do problema são quase todas bem simples. Segue código da classe Table:

public class Table {
private int image;
private String label;

public Table(int image, String label) {
this.image = image;
this.label = label;
}

public int getImage() {
return image;
}

public String getLabel() {
return label;
}
}

 

Essa será utilizada junto ao adapter do ListView de mesas de poker, ListView vinculado a atividade principal do projeto. Ainda sem relação com o sistema de criação de Floating Windows. Estas estão aqui somente para aprimorar o design do exemplo.

Assim prosseguirmos com a classe TableAdapter:

public class TableAdapter extends BaseAdapter {
private List<Table> tables;
private LayoutInflater inflater;

public TableAdapter(Context context, List<Table> tables){
inflater = LayoutInflater.from(context);
this.tables = tables;
}

@Override
public int getCount() {
return tables.size();
}

@Override
public Object getItem(int i) {
return tables.get( i );
}

@Override
public long getItemId(int i) {
return i;
}

@Override
public View getView(int i, View view, ViewGroup viewGroup) {
ViewHolder holder;

if( view == null ){
view = inflater.inflate(R.layout.item_table, null, false);
holder = new ViewHolder();
view.setTag( holder );
holder.setViews( view );
}
else{
holder = (ViewHolder) view.getTag();
}

holder.setData( tables.get( i ) );

return view;
}

private static class ViewHolder{
private ImageView ivTable;
private TextView tvNameTable;

private void setViews( View view ){
ivTable = (ImageView) view.findViewById(R.id.iv_table);
tvNameTable = (TextView) view.findViewById(R.id.tv_name_table);
}

private void setData( Table table ){
ivTable.setImageResource( table.getImage() );
tvNameTable.setText( table.getLabel() );
}
}
}

 

Uma simples classe trabalhando o BaseAdapter. Caso ainda não conheça essa entidade, depois de terminar este artigo, não deixe de estudar o seguinte: Utilizando BaseAdapter Para Personalização Completa da ListView.

Abaixo o XML do layout dos itens do ListView, /layout/item_table.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_gravity="center"
android:background="@android:color/white"
android:elevation="4dp"
card_view:cardCornerRadius="2dp">

<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="160dp">

<ImageView
android:id="@+id/iv_table"
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:contentDescription="Mesa em jogo"
android:scaleType="center" />

<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:background="@drawable/fade_black" />

<TextView
android:id="@+id/tv_name_table"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_gravity="left|bottom"
android:ellipsize="end"
android:maxLines="2"
android:padding="8dp"
android:textColor="@android:color/white"
android:textSize="21sp" />
</RelativeLayout>
</android.support.v7.widget.CardView>

 

Segue diagrama do layout anterior:

Por que o componente gráfico View está sendo utilizado? O ImageView e o TextView já seriam o suficiente para apresentar os dados, não?

Se notou, o View tem um drawable sendo utilizado como background dele. Recapitulando:

...
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:background="@drawable/fade_black" />
...

 

Segue código de /drawable/fade_black.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:centerColor="#66000000"
android:endColor="#00ffffff"
android:startColor="#bb000000" />

<corners android:radius="0dp" />
</shape>

 

Esse background com a configuração de dimensões do View, vai nos permitir colocar um gradiente escuro acima do ImageView e abaixo do título, facilitando a leitura deste último. Teremos como resultado algo como:

Antes de prosseguir para a próxima classe, caso ainda não conheça o CardView, como estudo para depois do fim deste artigo, siga para: Utilizando CardView, Material Design Android - Parte 4.

Assim podemos seguir com a apresentação da classe User:

public class User implements Parcelable {
private String image;
private int id;

public String getImage() {
return image;
}

public void setImage(String image) {
this.image = image;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}


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

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.image);
dest.writeInt(this.id);
}

public User() {
}

protected User(Parcel in) {
this.image = in.readString();
this.id = in.readInt();
}

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

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

 

E a classe Message:

public class Message implements Parcelable {
public static final String KEY = "message_key";

private User user;
private String message;

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

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

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(this.user, flags);
dest.writeString(this.message);
}

public Message() {
}

protected Message(Parcel in) {
this.user = in.readParcelable(User.class.getClassLoader());
this.message = in.readString();
}

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

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

 

Ambas implementam o Parcelable, pois estas já estão ligadas a lógica de negócio que vamos utilizar na ativação das notificações bola, mais precisamente, vamos utilizar instâncias destas classes para enviar os dados das notificações OneSignal para o serviço de criação / atualização das Floating Windows.

A outra classe de domínio do problema é a classe Notification. Essa tem o código principal que trabalha com o WindowManager e as Floating Windows.

Antes de chegarmos até ela temos de apresentar alguns outros conteúdos, assim ela será apresentada em uma seção posterior.

Pacote extras

Como classe auxiliar, ou extra, temos apenas uma, a classe Util:

public class Util {
@TargetApi(Build.VERSION_CODES.M)
public static boolean isSystemAlertPermissionGranted(Context context) {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays( context );
}

public static boolean isPlusEqualsApi13() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2;
}

public static int getDpsToPixels( int dp ){
dp = (int) (dp * Resources.getSystem().getDisplayMetrics().density);
return dp;
}

public static List<Table> getMockData(){
List<Table> list = new ArrayList<>();
list.add( new Table( R.drawable.pocker_01, "Casa Blanca (EUA) vs Nuria Muller (RUS)") );
list.add( new Table( R.drawable.pocker_02, "Hijacke Club House (BRA) vs Trariargos Porto (POR)") );
list.add( new Table( R.drawable.pocker_03, "Suicide Squad (NOR) vs Quiz Plus (ESP)") );
list.add( new Table( R.drawable.pocker_04, "Turtles Jordan (ESP) vs Palitos (EUA)") );
list.add( new Table( R.drawable.pocker_05, "Kiss 22 (AUS) vs Brazucas (BRA)") );
list.add( new Table( R.drawable.pocker_06, "London Square (ING) vs Deserted (SUE)") );

return list;
}
}

 

O método getMockData() será utilizado para preencher a lista que será vinculada ao adapter do ListView da atividade principal. Esse tipo de funcionalidade é muito comum em ambientes de desenvolvimento, onde ainda não há dados de produção ou uma base de dados preenchida para testes.

Os métodos getDpsToPixels() e isPlusEqualsApi13() são para, respectivamente, transformar um valor em densidade de pixels para pixels e informar se o Android do device em uso é da API 13 ou superior.

O método isSystemAlertPermissionGranted() é para verificar se a permissão SYSTEM_ALERT_WINDOW foi concedida. Porém o método Settings.canDrawOverlays() foi adicionado ao Android somente a partir da API 23, o que nos induz a utilizar o annotation @TargetApi.

Essa anotação é somente para informar ao code inspector, ou ao Lint, que independente da API mínima informada nas configurações do projeto, aquele método é somente para devices com API 23 ou superior.

Na execução do código essa anotação não tem efeito algum. E, independente de ter ou não ação em código em execução, na condicional a seguir:

...
Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Settings.canDrawOverlays( context )
...

 

A invocação do método Settings.canDrawOverlays() não ocorre em devices com Android API igual ou inferior a 22. Isso, pois a primeira verificação Build.VERSION.SDK_INT < Build.VERSION_CODES.M já é verdadeira.

Configuração da atividade principal

A atividade principal tem pouca ligação com a lógica de apresentação de notificações bolha, na verdade ela somente permite que seja invocada a Activity responsável por informar ao usuário sobre a necessidade de ativação, liberação, da permissão SYSTEM_ALERT_WINDOW.

Vamos iniciar com os arquivos XML dessa Activity. Segue código de /layout/content_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:id="@+id/content_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="br.com.thiengo.pockerhijack.MainActivity"
tools:showIn="@layout/app_bar_main">

<ListView
android:id="@+id/lv_tables"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:divider="#242525"
android:dividerHeight="8dp" />
</RelativeLayout>

 

Então o diagrama do layout anterior:

Agora o XML de /layout/app_bar_main.xml, o layout que, além de outras coisas, invoca o layout anterior: 

<?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:fitsSystemWindows="true"
tools:context="br.com.thiengo.pockerhijack.MainActivity">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

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

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

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

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

 

Segue diagrama de /layout/app_bar_main.xml:

Agora os dois arquivos XML vinculados ao DrawerLayout, root do XML da MainActivity. Começando pelo /layout/nav_header_main.xml, o cabeçalho do drawer:

<?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"
android:layout_width="match_parent"
android:layout_height="@dimen/nav_header_height"
android:background="@drawable/background_header"
android:gravity="bottom"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:theme="@style/ThemeOverlay.AppCompat.Dark">

<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
app:srcCompat="@android:drawable/sym_def_app_icon" />

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
android:text="Thiengo Calopsita"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="player@thiengo.com.br" />
</LinearLayout>

 

Agora o diagrama do layout /layout/nav_header_main.xml:

Você poderia seguramente utilizar um CircularImageView no lugar de somente ImageView, fica a seu critério. 

Então a segunda parte do drawer, a lista de itens, mais precisamente, /menu/activity_main_drawer.xml:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

<group android:checkableBehavior="single">
<item
android:id="@+id/nav_all_tables"
android:title="Mesas completas" />
<item
android:id="@+id/nav_table_5k"
android:title="Mesas de 5 mil" />
<item
android:id="@+id/nav_table_10k"
android:title="Mesas de 10 mil" />
<item
android:id="@+id/nav_table_15k"
android:title="Mesas de 15 mil" />
<item
android:id="@+id/nav_table_50k"
android:title="Mesas de 50 mil" />
<item
android:id="@+id/nav_table_sponsor"
android:title="Mesas patrocinadas" />
</group>

<item android:title="Configurações">
<menu>
<item
android:id="@+id/nav_notification"
android:icon="@drawable/ic_notification"
android:title="Notificação bolha" />
<item
android:id="@+id/nav_settings"
android:icon="@drawable/ic_person"
android:title="Perfil" />
</menu>
</item>
</menu>

 

Estamos com um menu para a lista de itens do drawer, pois essa é a melhor maneira, em meu ponto de vista, de implementa-lo, utilizando um NavigationView.

Segue diagrama de /menu/activity_main_drawer.xml:

Por fim o XML root da atividade principal, /layout/activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout 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:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#242525"
android:fitsSystemWindows="true"
tools:openDrawer="start">

<include
layout="@layout/app_bar_main"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@android:color/white"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer" />
</android.support.v4.widget.DrawerLayout>

 

O diagrama completo do layout /layout/activity_main.xml fica como abaixo:

Depois do código do Java API (posterior nesta seção) e com os layouts anteriores, teremos as seguintes telas no device:

Assim podemos prosseguir ao código da MainActivity. Segue:

public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener {

public static boolean isOpened = false;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
isOpened = true;

DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close);
drawer.addDrawerListener(toggle);
toggle.syncState();

NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);

ListView lvTables = (ListView) findViewById(R.id.lv_tables);
List<Table> tables = Util.getMockData();
TableAdapter tableAdapter = new TableAdapter(this, tables);
lvTables.setAdapter( tableAdapter );

OneSignal.clearOneSignalNotifications();
}

@Override
public void onBackPressed() {
DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START);
} else {
super.onBackPressed();
}
}

@SuppressWarnings("StatementWithEmptyBody")
@Override
public boolean onNavigationItemSelected(MenuItem item) {
int id = item.getItemId();

if (id == R.id.nav_notification) {
Intent intent = new Intent(this, NotificationActivity.class);
startActivity( intent );
return true;
}

DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout);
drawer.closeDrawer(GravityCompat.START);
return true;
}

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

 

Do código padrão criado pelo Android Studio, incluindo o código de inicialização do ListView, os trechos adicionados foram os seguintes:

public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener {

public static boolean isOpened = false;

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

OneSignal.clearOneSignalNotifications();
}
...

@SuppressWarnings("StatementWithEmptyBody")
@Override
public boolean onNavigationItemSelected(MenuItem item) {
...
if (id == R.id.nav_notification) {
Intent intent = new Intent(this, NotificationActivity.class);
startActivity( intent );
return true;
}
...
}

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

 

A variável isOpened será utilizada posteriormente na lógica de negócio de CustomApplication, para informar se a aplicação está ou não aberta.

A linha OneSignal.clearOneSignalNotifications() no onCreate() é para limpar todas as notificações OneSignal assim que a aplicação é aberta.

O código na lógica de clique do DrawerLayout é referente a abertura da Activity de configuração da permissão SYSTEM_ALERT_WINDOW. A NotificationActivity será construída na seção seguinte.

Voltaremos ao código da MainActivity quando finalizarmos a criação das entidades que trabalham as Floating Windows.

Configuração da atividade de notificação

Essa atividade é simples, vamos na verdade verificar se a permissão SYSTEM_ALERT_WINDOW foi concedida, caso não, vamos alterar a interface para permiti que o usuário dê essa permissão ao aplicativo.

Primeiro o código de layout. Segue /layout/content_notification.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:id="@+id/content_notification"
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"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="br.com.thiengo.pockerhijack.NotificationActivity"
tools:showIn="@layout/activity_notification">

<TextView
android:id="@+id/tv_notification_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:textSize="18sp" />

<Button
android:id="@+id/bt_notification"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:onClick="callAndroidSettings"
android:text="Atualizar notificação bolha"
android:textSize="18sp" />
</RelativeLayout>

 

Segue diagrama de /layout/content_notification.xml:

Agora o XML root, /layout/activity_notification.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"
android:fitsSystemWindows="true"
tools:context="br.com.thiengo.pockerhijack.NotificationActivity">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

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

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

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

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

 

Então o diagrama de /layout/activity_notification.xml:

Assim podemos prosseguir com o código Java API de NotificationActivity.

Primeiro a parte padrão de uma nova "Basic Activity" no Android Studio:

public class NotificationActivity extends AppCompatActivity {
...

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_notification);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);

if( getSupportActionBar() != null ){
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setDisplayShowHomeEnabled(true);
}

...
}
...

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

 

E assim o trecho adicional, trecho responsável pela verificação da permissão SYSTEM_ALERT_WINDOW e atualização do TextView e Button do layout:

public class NotificationActivity extends AppCompatActivity {

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

updateViews();
}

private void updateViews(){
TextView tvNotification = (TextView) findViewById(R.id.tv_notification_text);
Button btNotification = (Button) findViewById(R.id.bt_notification);

if( Util.isSystemAlertPermissionGranted( this ) ){
tvNotification.setText( getResources().getString( R.string.notification_ok ) );
btNotification.setVisibility(View.GONE);
}
else{
tvNotification.setText( getResources().getString( R.string.notification_danied ) );
btNotification.setVisibility(View.VISIBLE);
}
}
...

@TargetApi(Build.VERSION_CODES.M)
public void callAndroidSettings( View view ){
String packageName = getPackageName();
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + packageName));

startActivity(intent);
}
}

 

A constante Settings.ACTION_MANAGE_OVERLAY_PERMISSION foi adicionada somente a API 23, por isso novamente utilizamos a annotation @TargetApi, para informar ao Lint que não precisa marcar esse trecho como problemático.

Ainda falta a atualização do AndroidManifest.xml para também comportar a NotificationActivity. Segue:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.pockerhijack">
...

<application
android:name=".CustomApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...

<activity
android:name=".NotificationActivity"
android:label="@string/title_activity_notification"
android:theme="@style/AppTheme.NoActionBar" />
</application>
</manifest>

 

O Button que está vinculado ao método callAndroidSettings() somente é apresentado se a permissão ainda não foi concedida. Como na tela a seguir:

Assim, com o usuário clicando no Button, temos a seguinte tela de configuração do sistema:

Concedendo a permissão, o layout da NotificationActivity passa a ser apresentado como abaixo:

Devido a não possibilidade de trabalho com o método startActivityForResult(), pois a Activity da aplicação de configuração de permissão do Android não retorna nada depois da ação do usuário, vamos atualizar o código para a atualização das Views do layout de NotificationActivity ser efetiva na volta do usuário.

A invocação de updateViews() vai para o onResume():

public class NotificationActivity extends AppCompatActivity {
...

@Override
protected void onResume() {
super.onResume();
updateViews();
}
...
}

 

Agora podemos ir a parte crítica do sistema, onde se encontra a maior parte da lógica de negócio de criação e gerenciamento de Floating Windows, a classe Notification.

Classe Notification

O código da classe Notification é um pouco longo, logo, vamos em partes. Note que essa classe será responsável por toda a configuração de uma Floating Window, aqui uma notificação bola, em nosso sistema.

É nesta classe que você tem que mais ter atenção para entender a utilização do WindowManager e outras entidades para a criação de Floating Windows.

Vamos as nossas metas com a Notification class:

  • Permitir que as bubble notifications sejam apresentadas de maneira independente umas das outras;
  • Permitir que com o "toque e arrasta" o usuário consiga mover as bubble notifications pela tela do device;
  • Permitir que com somente o "toque e solta", simulando um clique, a aplicação seja aberta;
  • As notificações em bolha deverão ter um design similar ao do Facebook Messenger, mostrando a imagem de perfil e parte do texto da mensagem enviada;
  • As notificações deverão ser atualizáveis quando a mensagem vier de um usuário que já tem uma em Floating Window, referente a ele, na tela. Assim não será criada mais de uma notificação bolha de um mesmo usuário.

Vamos iniciar com os layouts da notificação.

Layouts? Será mais do que somente um?

Sim. Depois de vários testes descobri que a melhor maneira de permitir o posicionamento livre das notificações, por parte do usuário, é criando mais de um layout e inflando o correto de acordo com o novo posicionamento.

Prosseguindo com o primeiro XML, /layout/bubble_notification_left.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent">

<com.mikhaellopez.circularimageview.CircularImageView
android:id="@+id/cimv_profile"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
app:civ_border_width="0dp"
app:civ_shadow="false" />

<TextView
android:id="@+id/tv_message"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_toEndOf="@+id/cimv_profile"
android:layout_toRightOf="@+id/cimv_profile"
android:background="@drawable/circle_layout"
android:ellipsize="end"
android:gravity="center"
android:maxLines="2"
android:padding="5dp"
android:textColor="@android:color/white" />
</RelativeLayout>

Finalmente o CircularImageView sendo utilizado. Note os atributos de limitação no TextViewandroid:maxLines="2"android:ellipsize="end". Estes permitem que apresentemos corretamente apenas parte da mensagem completa enviada em notificação.

/layout/bubble_notification_right.xml é muito similar, mudando apenas os atributos das Views filhas. Segue:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent">

<com.mikhaellopez.circularimageview.CircularImageView
android:id="@+id/cimv_profile"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
app:civ_border_width="0dp"
app:civ_shadow="false" />

<TextView
android:id="@+id/tv_message"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_toLeftOf="@+id/cimv_profile"
android:layout_toStartOf="@+id/cimv_profile"
android:background="@drawable/circle_layout"
android:ellipsize="end"
android:gravity="center"
android:maxLines="2"
android:padding="5dp"
android:textColor="@android:color/white" />
</RelativeLayout>

 

Ambos os layouts apresentados têm o seguinte diagrama:

Assim o layout root que contém algum dos dois layouts anteriores, /layout/bubble_notification.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent">

<include layout="@layout/bubble_notification_left" />
</RelativeLayout>

 

Segue diagrama de /layout/bubble_notification.xml:

Note que no TextView de ambos os layouts que são apresentados em /layout/bubble_notification.xml têm um background que permite bordas circulares. O XML /drawable/circle_layout permite esse design:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/colorAccent"/>
<stroke android:width="0dip" android:color="@color/colorAccent" />
<corners android:radius="4dip"/>
<padding android:left="0dip" android:top="0dip" android:right="0dip" android:bottom="0dip" />
</shape>

 

Optamos pela cor vermelha, pois essa á a Accent Color do projeto.

Assim podemos prosseguir com o código Java API de Notification. Vamos primeiro a declarações de variáveis e alguns métodos simples, incluindo o construtor:

public class Notification implements View.OnTouchListener {
private RelativeLayout bubble;
private WindowManager windowManager;
private WindowManager.LayoutParams params;


public Notification( WindowManager windowManager, RelativeLayout layout ){
this.windowManager = windowManager;
this.setBubble( layout );
this.setParams();
}

public RelativeLayout getBubble() {
return bubble;
}

private void setBubble(RelativeLayout bubble) {
this.bubble = bubble;
this.bubble.setOnTouchListener(this);
}

public WindowManager.LayoutParams getParams() {
return params;
}

private void setParams() {
params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);

params.gravity = Gravity.TOP | Gravity.LEFT;
params.x = 0;
params.y = 100;
}

@Override
public boolean onTouch(View v, MotionEvent event) {
/* TODO */
return false;
}
}

 

O WindowManager será injetado pelo construtor, incluindo um RelativeLayout, a instância de /layout/bubble_notification.xml. Lembrando que as instâncias de Notification serão administradas em um Service, criado na próxima seção.

Veja que em setParams() precisamos de uma configuração extensa. Isso para que a Window criada somente ocupe o espaço necessário a ela.

O passo "...a Window criada" acontece de maneira implícita. A criação de uma nova Window acontece quando adicionamos uma nova View ao WindowManager. Esse trecho de código, em nosso projeto, estará no Service que administrará as instâncias de Notification.

As duas primeiras constantes em new WindowManager.LayoutParams() são, respectivamente, referentes a largura e altura da Window.

A constante WindowManager.LayoutParams.TYPE_SYSTEM_ALERT permite que a Window que será criada fique acima de qualquer outra aplicação.

Essa constante, TYPE_SYSTEM_ALERT, pode ser substituída por inúmeras outras caso queira um outro tipo de comportamento por parte da Window. Veja outras constantes possíveis no link a seguir: Documentação WindowManager.LayoutParams.

A constante WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE faz com que a nova Window não obtenha o foco de entrada que há em outras aplicações já abertas ou que serão ainda acionadas.

E por fim, a constante PixelFormat.TRANSLUCENT permite que a aplicação abaixo da nova Window seja visível através de qualquer parte transparente desta nova Window.

O código:

...
params.gravity = Gravity.TOP | Gravity.LEFT;
params.x = 0;
params.y = 100;
...

 

É para definir um posicionamento inicial para a nova Window. Digo inicial, pois o usuário poderá realizar o drag.

Os métodos getter e setter presentes vão permitir acesso as variáveis. Acesso por parte do Service que gerenciará as bubble notifications. Essa parte tem relação com o domínio do problema de nosso sistema, pois não é obrigatório quando se trabalhando com criação de Floating Windows.

Precisamos de um método que permita a entrada de uma instância do tipo Message para que os dados sejam colocados nas Views da Window. Segue método updateBubbleView():

public class Notification implements View.OnTouchListener {
...

public void updateBubbleView( Message message ){
((TextView) bubble.findViewById(R.id.tv_message)).setText( message.getMessage() );

Picasso.with( bubble.getContext() )
.load( message.getUser().getImage() )
.into( ((CircularImageView) bubble.findViewById(R.id.cimv_profile)) );
}
}

 

Estamos enfim utilizando a API Picasso para que seja possível o carregamento de uma imagem remota. A uri da imagem será enviada via notificação OneSignal.

Agora já podemos trabalhar o código do método onTouch(). Estaremos respondendo a três eventos dentro desse método, são eles: MotionEvent.ACTION_DOWNMotionEvent.ACTION_UPMotionEvent.ACTION_MOVE.

Esses eventos são acessados pelo parâmetro de entrada do tipo MotionEvent. Segue código atualizado de onTouch():

public class Notification implements View.OnTouchListener {
...

@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
/* TODO */
return true;
case MotionEvent.ACTION_UP:
/* TODO */
return true;
case MotionEvent.ACTION_MOVE:
/* TODO */
return true;
}
return false;
}
...
}

 

O retorno true em onTouch() indica que o sistema não deve fazer nada, pois nós já processamos a entrada do evento. O false, o oposto.

Lembrando que vamos trabalhar com o drag na Window criada, logo, em MotionEvent.ACTION_DOWN precisamos das posições iniciais da Floating Window e do touch do usuário. Segue:

public class Notification implements View.OnTouchListener {
...
private int initialX;
private int initialY;
private float initialTouchX;
private float initialTouchY;

...
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
actionDownUpdate( event );
return true;
case MotionEvent.ACTION_UP:
/* TODO */
return true;
case MotionEvent.ACTION_MOVE:
/* TODO */
return true;
}
return false;
}

private void actionDownUpdate( MotionEvent event ){
initialX = params.x;
initialY = params.y;
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
}
...
}

 

Agora vamos direto a lógica de MotionEvent.ACTION_MOVE, pois, depois do toque, a possível próxima ação é o drag. Segue código:

public class Notification implements View.OnTouchListener {
private boolean isInRightSide = false;
...

@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
actionDownUpdate( event );
return true;
case MotionEvent.ACTION_UP:
/* TODO */
return true;
case MotionEvent.ACTION_MOVE:
actionMoveUpdate( event );
return true;
}
return false;
}
...

private void actionMoveUpdate( MotionEvent event ){
int extraVal = isInRightSide ? bubble.getWidth() * -1 : 0;

params.x = initialX + extraVal + (int) (event.getRawX() - initialTouchX);
params.y = initialY + (int) (event.getRawY() - initialTouchY);
windowManager.updateViewLayout(bubble, params);
}
...
}

 

Com a ação de arrastar aplicada pelo usuário nós teremos as novas coordenadas, partindo da coordenada inicial.

Porém, caso não trabalhemos corretamente com o tamanho da Window junto a atualização de posicionamento, quando esta estiver no final da abcissa (eixo x), aqui o final é a borda direita da tela, o arrastar somente será visível depois que todo o tamanho da Window já tiver sido percorrido pelo dedo do usuário.

Veja o diagrama abaixo representando o arrastar vindo da esquerda, início da abcissa:

O reposicionamento da Window é refletido assim que o drag se inicia.

Agora o diagrama do arrastar da direita para a esquerda caso não estivéssemos com a lógica de negócio de adicionar o tamanho da Window ao valor de x de params:

A variável isInRightSide é importante para evitar o problema acima. Ela inicia com o valor false, pois nosso posicionamento inicial para novas Floating Windows é na esquerda.

O valor de isInRightSide é atualizado na lógica de negócio que responde ao evento MotionEvent.ACTION_MOVE. Segue:

public class Notification implements View.OnTouchListener {
...
private int width;

public Notification( WindowManager windowManager, RelativeLayout layout ){
this.windowManager = windowManager;
this.setBubble( layout );
this.setWidth();
this.setParams();
}

private void setWidth() {
Display display = windowManager.getDefaultDisplay();

if( Util.isPlusEqualsApi13() ){
Point size = new Point();
display.getSize(size);
width = size.x;
}
else{
width = display.getWidth();
}
}
...

@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
actionDownUpdate( event );
return true;
case MotionEvent.ACTION_UP:
actionUpUpdate( event );
return true;
case MotionEvent.ACTION_MOVE:
actionMoveUpdate( event );
return true;
}
return false;
}
...

private void actionUpUpdate( MotionEvent event ){
int desiredPosition;
int posX = params.x + bubble.getWidth() / 2;

if( posX > width / 2 ){
desiredPosition = width;
isInRightSide = true;
}
else{
desiredPosition = 0;
isInRightSide = false;
}
slowDrawBubbleMove( desiredPosition );
updateWindowViews();
}
...
}

 

Primeiro, adicionamos uma variável width, pois ela será necessária na lógica de posicionamento da Window. Note que no método setWidth() temos código para versões anteriores a API 13 e iguais e posteriores a esta API.

O método actionUpUpdate() tem toda a lógica de negócio para o correto reposicionamento da Window depois que o usuário "para o touch".

Antes de chegar a invocação do método slowDrawBubbleMove() estamos verificando se o local onde o usuário liberou a Window está mais para a direita ou para a esquerda.

Caso mais para a direita, coloque o posicionamento de borda da Window para ficar junto a borda direita da tela. Caso contrário, à borda esquerda. Veja que a variável isInRightSide é também atualizada.

Essa lógica é necessária para que o usuário não deixe a Window no meio da tela, atrapalhando o trabalho com outras aplicações.

O método slowDrawBubbleMove() permite que apliquemos a correção de posicionamento a ponto de os olhos do usuário conseguirem visualizar a animação, sem um reposicionamento bruto.

Segue código deste método:

public class Notification implements View.OnTouchListener {
...
private void slowDrawBubbleMove( int desiredPosition ){
int incDec = params.x < desiredPosition ? 1 : -1;

while( params.x < desiredPosition || params.x > desiredPosition ){

params.x += incDec;
windowManager.updateViewLayout(bubble, params);
}
}
...
}

 

No método acima, se a posição atual é menor que a desejada, desiredPosition, incrementamos params.x até atingir o valor igual a desiredPosition. Sempre atualizando as coordenadas da Window.

Agora podemos ir ao método que atualiza as Views da Window de acordo com o lado que ela deve ficar, o método updateWindowViews(). Segue:

public class Notification implements View.OnTouchListener {
...

private void updateWindowViews(){
/* PARTE 1 */
Bitmap bitmap = ((BitmapDrawable) ((ImageView)bubble.findViewById(R.id.cimv_profile))
.getDrawable())
.getBitmap();

String text = ((TextView) bubble.findViewById(R.id.tv_message))
.getText()
.toString();

/* PARTE 2 */
Context context = bubble.getContext();
int layout = isInRightSide ? R.layout.bubble_notification_right : R.layout.bubble_notification_left;
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
Util.getDpsToPixels( 166 ),
RelativeLayout.LayoutParams.WRAP_CONTENT );

/* PARTE 3 */
bubble.removeAllViews();

/* PARTE 4 */
RelativeLayout view = (RelativeLayout) LayoutInflater
.from(context)
.inflate( layout, null );

((ImageView)view.findViewById(R.id.cimv_profile)).setImageBitmap( bitmap );
((TextView)view.findViewById(R.id.tv_message)).setText( text );

view.setLayoutParams(lp);
bubble.addView( view );
}
...
}

 

Na Parte 1 devemos obter os dados que chegaram a Window via notificação OneSignal. Temos esses dados somente nas Views atuais da Floating Window, logo, acessamos estes dados por meio destas Views para que seja possível coloca-los no novo layout inflado.

Na Parte 2 obtemos alguns dados que serão necessários para inflar o novo layout. Primeiro o contexto, depois o identificador inteiro do layout de acordo com o novo posicionamento da Window. E, por fim, uma nova instância de LayoutParams de RelativeLayout.

Note que esse novo LayoutParams é do RelativeLayout, pois o layout root do layout que será inflado é um RelativeLayout e não porque ele, o layout inflado, é também um RelativeLayout.

Na Parte 3 já podemos seguramente remover todas as Views filhas do RelativeLayout root na Window.

Na Parte 4 inflamos o novo layout container das Views de dados e em seguida colocamos os dados nessas Views. Finalizando com a configuração do LayoutParams no novo RelativeLayout e a colocação dele, o RelativeLayout, como filho do RelativeLayout root da Window.

Note que o valor de 166 DPs para a largura do layout é uma escolha pessoal minha de acordo com os testes que fiz, você poderia colocar o valor que quisesse.

Com isso somente precisamos atualizar a lógica de Notfication para também ter o código que permite a abertura da aplicação. Segue:

public class Notification implements View.OnTouchListener {
...
private boolean isClicked = false;
...

private void actionDownUpdate( MotionEvent event ){
initialX = params.x;
initialY = params.y;
initialTouchX = event.getRawX();
initialTouchY = event.getRawY();
isClicked = true;
}

private void actionUpUpdate( MotionEvent event ){
int desiredPosition;
int posX = params.x + bubble.getWidth() / 2;

if( posX > width / 2 ){
desiredPosition = width;
isInRightSide = true;
}
else{
desiredPosition = 0;
isInRightSide = false;
}
slowDrawBubbleMove( desiredPosition );
updateWindowViews();
callActivityIfClicked();
}

private void actionMoveUpdate( MotionEvent event ){
int extraVal = isInRightSide ? bubble.getWidth() * -1 : 0;
isClicked = false;

params.x = initialX + extraVal + (int) (event.getRawX() - initialTouchX);
params.y = initialY + (int) (event.getRawY() - initialTouchY);
windowManager.updateViewLayout(bubble, params);
}

private void callActivityIfClicked(){
if( isClicked ){
Intent intent = new Intent( bubble.getContext(), MainActivity.class);
intent.setFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
bubble.getContext().startActivity(intent);
}
}
...
}

 

Com a variável isClicked sendo atualizada nos locais corretos conseguimos garantir que a MainActivity de nossa aplicação somente será acionada caso o usuário clique, ativação do método actionDownUpdate(), e solte o touch, ativação do método actionUpUpdate().

Ou seja, sem o uso do actionMoveUpdate(). Caso haja a ação de mover, não é um clique, devemos somente atualizar a posição da Floating Window em foco.

Note que para ser possível abrir a aplicação partindo do Service que gerencia as bubble notifications, temos de utilizar a flag Intent.FLAG_ACTIVITY_NEW_TASK na Intent.

Com isso atingimos todas as metas para a classe Notification e podemos prosseguir com a construção do serviço de gerenciamento de Floating Windows. Em nosso domínio do problema estas são nossas notificações bolha.

Criando o serviço de gerenciamento de notificação bolha

Com a classe NotificationService nossas metas são:

  • Obter do sistema Android a instância do WindowManager;
  • Iniciar e gerenciar uma lista de notificações bolha;
  • Obter ou criar novas Floating Windows.

Primeiro vamos iniciar com a configuração inicial de um Service e a declaração de variáveis de acordo com nossas metas:

public class NotificationService extends Service {
private WindowManager windowManager;
private List<Notification> bubbles;

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();

bubbles = new ArrayList<>();
windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() {
super.onDestroy();
}
}

 

Todos os métodos do ciclo de vida de um Service, os que foram adicionados ao código anterior, serão úteis em alguma parte do algoritmo que será desenvolvido. Somente o método onBind() que não, a implementação dele é obrigatória.

Agora vamos ao código que nos permitirá acessar uma notificação bolha da lista de notificações, isso para atualizar os dados dela quando há uma nova mensagem do mesmo usuário.

Ou criar uma nova notificação, Floating Window, para a mensagem de um usuário que ainda não havia enviado alguma no período em que a aplicação estava fechada.

Segue algoritmo:

public class NotificationService extends Service {
...

private Notification getBubble( User user ){
Notification bubble = getBubbleFromList( user );
if( bubble == null ){
bubble = getNewBubble( user );
}

return bubble;
}

private Notification getBubbleFromList( User user ){
for( Notification n : bubbles ){
if( n.getBubble().getId() == user.getId() ){
return n;
}
}

return null;
}

private Notification getNewBubble( User user ){
RelativeLayout layout = (RelativeLayout) LayoutInflater.from(this).inflate( R.layout.bubble_notification, null, false );
layout.setId( user.getId() );

Notification bubble = new Notification( windowManager, layout );

bubbles.add( bubble );
windowManager.addView( bubble.getBubble(), bubble.getParams() );

return bubble;
}
...
}

 

Em getBubble() primeiro verificamos, via getBubbleFromList(), se já existe a notificação bolha referente ao usuário atual em mensagem. Note que o ID da notificação, no caso o ID do RelativeLayout, é equivalente ao ID do usuário no backend Web da aplicação.

Em nosso domínio do problema o ID do usuário, juntamente a outros dados, será enviado via notificação OneSignal.

O método getNewBubble() infla um novo layout com o ID do usuário atual em mensagem, logo em seguida adiciona esse novo layout a uma instância de Notification. E então o mesmo layout e os parâmetros de tela dele são adicionados ao WindowManager.

Esse ato de adicionar uma nova View ao WindowManager, na verdade aciona a criação de uma nova Window em tela.

Agora vamos a atualização do método que utiliza o método getBubble(). Segue código de onStartCommand():

public class NotificationService extends Service {
...

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Message message = intent.getParcelableExtra( Message.KEY );
if( message != null ){
Notification bubble = getBubble( message.getUser() );
bubble.updateBubbleView( message );
}

return super.onStartCommand(intent, flags, startId);
}
...
}

 

Por segurança verificamos se há dados no Intent enviado, isso verificando se a variável message não é nula. Em seguida obtemos a notificação bolha, nova ou já existente, e então atualizamos os dados nela.

Para finalizar o Service, precisamos do código de destruição de Views do WindowManager, esse vem no onDestroy():

public class NotificationService extends Service {
...

@Override
public void onDestroy() {
super.onDestroy();
for( Notification bubble : bubbles ){
windowManager.removeView( bubble.getBubble() );
}
}
}

 

O código acima é autocomentado, estamos removendo as Floating Windows da tela. Com isso somente temos de declarar o Service no AndroidManifest.xml para que ele já possa ser acionado:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.pockerhijack">
...

<application
android:name=".CustomApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...

<service
android:name=".service.NotificationService"
android:enabled="true"
android:exported="false" />
</application>

</manifest>

 

Assim vamos as atualizações em CustomApplication e na MainActivity para corretamente trabalharmos com as notificações.

Atualização da CustomApplication e da atividade principal

Primeiro a MainActivity, pois a atualização é bem simples:

public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener {
...

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

Intent intent = new Intent( this, NotificationService.class);
stopService(intent);
}
...
}

 

Trabalhamos o stopService() para garantir que quando a aplicação for aberta as notificações bolhas serão removidas. Devido ao código que colocamos no onDestroy() do NotificationService, o stopService() fará com que as notificações sejam seguramente removidas.

Agora o código atualizado de CustomApplication:

public class CustomApplication extends Application
implements OneSignal.NotificationReceivedHandler {
...

@Override
public void notificationReceived(OSNotification notification) {

if( !MainActivity.isOpened
&& Util.isSystemAlertPermissionGranted(this) ){

Message message = getMessage( notification );

Intent intent = new Intent( this, NotificationService.class);
intent.putExtra( Message.KEY, message );
startService(intent);
}
}

private Message getMessage( OSNotification notification ){
Message message = new Message();

try{
JSONObject jsonObject = notification.payload.additionalData;
User user = new User();
user.setImage( jsonObject.getString("user_image") );
user.setId( jsonObject.getInt("user_id") );

message.setMessage( notification.payload.body );
message.setUser( user );
}
catch( JSONException e){}

return message;
}
}

 

Primeiro verificamos se a permissão que libera a criação de Floating Windows foi concedida. Caso sim, realizamos o parser nos dados que estão na instância de OSNotification, criamos o objeto do tipo Message por completo e enviamos este para o NotificationService.

Caso este Service já esteja em execução, a invocação do método startService() acionará somente o método onStartCommand(), onde tem nossa lógica de negócio para atualização das Floating Windows.

Assim podemos prosseguir para os testes.

Testes e resultados

Assumindo que você já configurou seu OneSignal ou o sistema de notificação de sua preferência, neste último caso você terá de adaptar um pouco o código da CustomApplication.

Assumindo isso, vamos ao dashboard do OneSignal e criar uma nova mensagem com as seguintes configurações:

Enviando a mensagem, temos como resultado a seguinte tela:

Arrastando a notificação bolha, temos:

Clicando nela, sem arrastar, temos a abertura da aplicação:

Agora um teste com o envio de um usuário de ID 1:

Agora o envio de uma nova notificação do mesmo usuário, com o mesmo ID, temos:

E por fim o envio de uma notificação de um usuário com ID 2. Assim temos:

Abaixo o printscreen do Hierarchy Views depois de termos duas notificações bolha em tela. Print comprovando que cada nova View adicionada ao WindowManager gera, na verdade, uma nova Window:

As duas Windows na lista de Windows no menu esquerdo, digo, as duas sem nomes, estas são na verdade as Windows das duas notificações bolha em tela.

Aqui finalizamos nosso projeto de exemplo e você já deve saber como trabalhar com Floating Windows em suas aplicações Android.

Não esqueça de se inscrever no canal e assinar a lista de emails do Blog para receber conteúdos exclusivos e em primeira mão.

Vídeo com implementação passo a passo do código de notificação bolha

Abaixo o vídeo com a implementação passo a passo do projeto deste artigo:

Para acesso ao conteúdo completo do projeto, para download, entre no GitHub dele em: https://github.com/viniciusthiengo/PockerHijack.

Conclusão

Utilizada corretamente, as Floating Windows podem colocar ainda mais poder de interação entre seu aplicativo Android e o usuário.

Apesar de a configuração de trabalho com a Floating Window não ser trivial, principalmente devido as várias possibilidades de configurações do LayoutParams, quando encapsulado corretamente, digo, os código obrigatórios (boilerplate code) bem distribuídos, o trabalho é muito facilitado e vale o esforço para acrescentar funcionalidades a sua APP.

Lembre-se de sempre verificar se a permissão SYSTEM_ALERT_WINDOW foi concedida. Pois mesmo sabendo que o Google já a deixa liberada, caso declarada no AndroidManifest.xml, há o bloqueio de alguns devices para a segurança dos usuários.

O lado negativo de trabalho com Floating Windows, na verdade, trabalho com o WindowManager, é que as Windows são criadas de maneira implícita, o programador não fica ciente, em código, desta criação, somente se debugar com o Hierarchy Views.

Finalizando a conclusão, para melhor simular a bubble notification do Facebook, implemente a funcionalidade de remover as notificações assim que o usuário dá o touch em alguma delas.

Aqui finalizamos o artigo, não esqueça de compartilhar e se inscrever na lista de emails do Blog, logo abaixo. Qualquer dúvida ou sugestão, deixe nos comentários.

Fontes

Chathead Basics

How Tapjacking Made a Return with Android Marshmallow — and Nobody Noticed

Documentação Window Android

Documentação WindowManager Android

Stackoverflow: What is an Android window?

Stackoverflow: What is windowManager in android?

Vlw.

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Relacionado

Estudando Android - Lista de Conteúdos do BlogEstudando Android - Lista de Conteúdos do BlogAndroid
AndroidAnnotations, Entendendo e UtilizandoAndroidAnnotations, Entendendo e UtilizandoAndroid
Top 10 leituras de 2016 que são boas pedidas para 2017Top 10 leituras de 2016 que são boas pedidas para 2017Empreendedorismo
API de Endereços Para Pré-Cadastro em APPs Android - Parte 1API de Endereços Para Pré-Cadastro em APPs Android - Parte 1Android

Compartilhar

Comentários Facebook

Comentários Blog (2)

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...
20/01/2017
Overlay Notification, pretty awsome.
Congratulations once again.
Responder
Leandro (3) (0)
18/01/2017
Muito bom mesmo este conteúdo, me ajudou muito....valeu thiengo
Responder