GitHub Login, Firebase Android - Parte 9
(4633) (8)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Opa, blz?
Nesse post damos continuidade a série Firebase Android, abordando o último login social restante, GitHub login.
Mesmo não tendo classes que facilitam o processo de login, com o GitHub a funcionalidade de login é tão simples de vincular quanto os logins sociais já apresentados nessa série. Porém teremos de utilizar WebView para login fluir sem problemas.
Antes prosseguir, lembre que o projeto está presente no GitHub: https://github.com/viniciusthiengo/nosso-chate
Agora, aos códigos. Nossa primeira atualização é no gradle APP level, build.gradle (Module: app), vamos atualizar a referência ao Firebase UI e adicionar o compile do GitHub API:
...
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
compile 'com.google.firebase:firebase-database:9.0.0'
compile 'com.google.firebase:firebase-auth:9.0.0'
compile 'com.firebaseui:firebase-ui:0.4.0' /* ATUALIZE ESSA */
compile 'com.facebook.android:facebook-android-sdk:[4,5)'
compile 'com.google.android.gms:play-services-auth:9.0.0'
compile('com.twitter.sdk.android:twitter:1.13.1@aar') {
transitive = true;
}
compile 'com.github.alorma:github-sdk:3.2.5' /* ADICIONE ESSA LINHA */
}
...
Logo depois dessa atualização, se está acompanhando o código da série desde o início, vamos ter problemas de incompatibilidade de temas entre a library Firebase UI é o Facebook Android SDK. A solução é a atualização da tag Activity do Facebook no Android Manifest, como segue:
...
<application
android:name=".CustomApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
xmlns:tools="http://schemas.android.com/tools">
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
<activity android:name="com.facebook.FacebookActivity"
android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:label="@string/app_name"
tools:replace="android:theme"/>
...
</application>
...
Adicione o atributo xmlns:tools a tag <application> e logo depois adicione o atributo tools:replace a tag <activity> do Facebook.
Feito isso, nosso próximo passo é atualizar a classe adapter UserRecyclerAdapter, pois logo após a atualização da library do Firebase (no post anterior) ela ficou comentada devido ao não suporte ao novo Firebase, porém com a nova versão do Firebase UI esse problema foi solucionado. Segue código completo da classe:
package br.com.thiengo.thiengocalopsitafbexample.adapter;
import com.firebase.ui.database.FirebaseRecyclerAdapter;
import com.google.firebase.database.Query;
import br.com.thiengo.thiengocalopsitafbexample.domain.User;
public class UserRecyclerAdapter extends FirebaseRecyclerAdapter<User, UserViewHolder> {
public UserRecyclerAdapter(
Class<User> modelClass,
int modelLayout,
Class<UserViewHolder> viewHolderClass,
Query ref ){
super( modelClass, modelLayout, viewHolderClass, ref );
}
@Override
protected void populateViewHolder(
UserViewHolder userViewHolder,
User user, int i) {
userViewHolder.text1.setText( user.getName() );
userViewHolder.text2.setText( user.getEmail() );
}
}
Com isso, para a lista de usuários presentes no chat voltar a funcionar devemos também atualizar a MainActivity:
public class MainActivity extends AppCompatActivity {
private DatabaseReference databaseReference; /* ADICIONE ESSA LINHA */
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
authStateListener = new FirebaseAuth.AuthStateListener() {
@Override
public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
if( firebaseAuth.getCurrentUser() == null ){
Intent intent = new Intent( MainActivity.this, LoginActivity.class );
startActivity( intent );
finish();
}
}
};
mAuth = FirebaseAuth.getInstance();
mAuth.addAuthStateListener( authStateListener );
databaseReference = LibraryClass.getFirebase(); /* ADICIONE ESSA LINHA */
}
...
private void init(){ /* DESCOMENTE TODAS AS LINHAS DESSE MÉTODO */
RecyclerView rvUsers = (RecyclerView) findViewById(R.id.rv_users);
rvUsers.setHasFixedSize( true );
rvUsers.setLayoutManager( new LinearLayoutManager(this));
adapter = new UserRecyclerAdapter(
User.class,
android.R.layout.two_line_list_item,
UserViewHolder.class,
databaseReference.child("users") /* ATUALIZE ESSE ARGUMENTO */
);
rvUsers.setAdapter(adapter);
}
@Override
protected void onDestroy() {
super.onDestroy();
adapter.cleanup(); /* DESCOMENTE ESSA LINHA */
if( authStateListener != null ){
mAuth.removeAuthStateListener( authStateListener );
}
}
...
}
Depois dessa última atualização na MainActivity o RecyclerView presente no layout dela deve estar sendo preenchido.
Ok, agora podemos partir para a vinculação do GitHub login a nossa APP de chat.
Nosso primeiro passo é acessar nossa conta no GitHub. E então, no menu superior direito clicar em Settings, logo depois no menu lateral esquerdo clicar em OAuth applications, e então clique na aba Developer applications. Agora clique em Register a new application.
Preencha todo o formulário, exceto o campo Authorization callback URL. Note que o campo Homepage URL pode ser preenchido com qualquer url, de preferência a que leva à página de download da APP, como estamos ainda em desenvolvimento coloquei a url do blog:
Não clique ainda em Register application. Antes vamos acessar o nosso Firebase dashboard. Já no dashboard, no menu lateral esquerdo clique em Auth, logo depois clique na aba MÉTODOS DE LOGIN. E então, na lista de providers que aparecerá abaixo clique em GitHub:
Veja que logo acima dos botões CANCELAR e SALVAR tem uma url, copie ela e volte ao formulário de inscrição de aplicação no GitHub.
No campo Authorization callback URL cole essa url, logo depois clique em Register a new application. Você terá um tela similar a abaixo:
Copie as chaves referentes a Client ID e a Client Secret. Volte ao dashboard do Firebase, mais precisamente onde estavámos, no formulário de liberação do GitHub provider. Cole as chaves do passo anterior em seus respectivos campos nesse formulário. No topo direito do formulário ative o provider e logo depois clique em SALVAR:
O resultado deve ser o GitHub provider ativado na lista de providers:
Nosso próximo passo é realizar algumas atualizações em nosso projeto. Começando por adicionar algumas variáveis String a /res/values/strings.xml:
<resources>
...
<string name="action_sign_in_github">Sign in com GitHub</string>
<string name="github_app_id">b34116e18d2421649752</string>
<string name="github_app_secret">a620999ca02a16a7215aaf6fc82d65ddf38cfc57</string>
<string name="github_app_url">https://nosso-chat-firebase-972f1.firebaseapp.com/__/auth/handler</string>
</resources>
Note que adicionamos o label do botão que ainda temos de adicionar ao layout de LoginActivity, content_login.xml, "Sign in com GitHub".
Logo depois colocamos as chaves Client ID e Client Secret de nossa APP GitHub. E Então colocamos a url de redirecionamento, a mesma que colocamos no campo Authorization callback URL no formulário de aplicação do GitHub. Vamos precisar desses valores em nossa aplicação.
Nosso próximo passo é adicionar o botão de login com GitHub ao layout content_login.xml. Adicione esse botão logo abaixo do Twitter Button:
...
<Button
android:onClick="sendLoginTwitterData"
android:id="@+id/email_sign_in_twitter_button"
style="?android:textAppearanceSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/action_sign_in_twitter"
android:textStyle="bold" />
<!-- ADICIONE O BUTTON ABAIXO -->
<Button
android:onClick="sendLoginGithubData"
android:id="@+id/email_sign_in_github_button"
style="?android:textAppearanceSmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/action_sign_in_github"
android:textStyle="bold" />
...
Agora em LoginActivity vamos criar o método sendLoginGithubData() que está vinculado ao listener de clique do GitHub button em content_login.xml. Vamos criá-lo logo abaixo do método sendLoginTwitterData():
...
public void sendLoginGithubData( View view ){
/* PARTE 1 */
Uri uri = new Uri.Builder()
.scheme("https")
.authority("github.com")
.appendPath("login")
.appendPath("oauth")
.appendPath("authorize")
.appendQueryParameter("client_id", getString(R.string.github_app_id))
.appendQueryParameter("scope", "user,user:email")
.build();
/* PARTE 2 */
WebView webView = new WebView(this);
webView.loadUrl( uri.toString() );
webView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri uri = Uri.parse(url);
if( uri.getQueryParameter("code") != null
&& uri.getScheme() != null
&& uri.getScheme().equalsIgnoreCase("https") ){
requestGitHubUserAccessToken( uri.getQueryParameter("code") );
}
return super.shouldOverrideUrlLoading(view, url);
}
});
/* PARTE 3 */
AlertDialog.Builder alert = new AlertDialog.Builder(this);
alert.setTitle("Login GitHub");
alert.setView(webView);
alert.show();
}
...
Note que primeiro, PARTE 1, criamos a url via Uri.Builder, passo a passo. Esse procedimento é descrito na documentação do GitHub API https://developer.github.com/v3/oauth/, mais precisamente na seção Web Application Flow. Nesse ponto do código montamos a url https://github.com/login/oauth/authorize e adicionamos os parâmetros GET cliente_id e scope. O primeiro é o nosso Client ID do GitHub APP, o segundo é referente aos dados que precisamos do usuário GitHub, no caso "user,user:email" é referente aos dados de perfil e ao email.
Ainda na PARTE 1, essa url montada terá a seguinte aparência: https://github.com/login/oauth/authorize?client_id=b34116e18d2421649752&scope=user,user:email. Ela será útil apenas para obtermos a página de login do GitHub no WebView e consequentemente, com o consentimento do usuário, vamos obter um código temporário que vai nos permitir solicitar, em uma nova requisição aos servidores do GitHub, o token de acesso, ou access token.
No código abaixo relembramos a PARTE 2:
...
/* PARTE 2 */
WebView webView = new WebView(this);
webView.loadUrl( uri.toString() );
webView.setWebViewClient(new WebViewClient(){
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri uri = Uri.parse(url);
if( uri.getQueryParameter("code") != null
&& uri.getScheme() != null
&& uri.getScheme().equalsIgnoreCase("https") ){
requestGitHubUserAccessToken( uri.getQueryParameter("code") );
}
return super.shouldOverrideUrlLoading(view, url);
}
});
...
Nessa parte estamos utilizando um WebView. Por que se a documentação diz algo sobre um IntentFilter?
Realmente, a documentação fala sobre o IntentFilter, pois a princípio é mais fácil chamar o Browser APP do device via Intent com nossa url construída na PARTE 1 do que utilizar um WebView. O problema depois está em fechar o Browser APP, pois essa APP não faz parte de sua APP de Chat, e mesmo se fosse um passo simples, você ainda teria de colocar o IntentFilter no AndroidManifest.xml, passo que a utilização de um WebView dispensa.
Logo, colocamos toda a codificação necessária para trabalharmos com o WebView, incluindo o setWebViewClient() que além de nos permitir verificar a url da página a cada novo carregamento (de suma importância em no login com GitHub) nos permite também evitar a chamada nativa ao Browser APP, já que estamos carregando o conteúdo Web em nossa própria WebView.
Ainda na PARTE 2, note que transformamos a url de parâmetro do método shouldOverrideUrlLoading() em uma Uri, isso para que possamos acessar as partes da url. Por que?
Lembra da url de callback que fornecemos no campo Authorization callback URL do formulário de criação de APP no GitHub? Então, logo depois de o usuário permitir o acesso de nossa APP aos dados dele no GitHub, o GitHub vai carregar exatamente essa url de callback, porém vai ser enviado também um parâmetro GET code. Esse code contém um código único e temporário que nos permite realizarmos uma nova requisição aos servidores do GitHub para podermos obter o access token.
Por que todos esses passos? Não podemos já utilizar esse código único para vincular o usuário ao Firebase? Não, o Firebase está esperando o token válido do GitHub, ele não vai funcionar com o código único (eu testei).
Então voltando ao condicional dentro do método shouldOverrideUrlLoading() de nosso WebView. Verificamos se é a url com o código único e protocolo https que estamos aguardando, caso sim chamamos o método requestGitHubUserAccessToken().
Observação: precisamos dessa verificação pois nosso WebView pode ter até três novos carregamentos de página e um mínimo de dois carregamentos de página, sendo que o que nos interessa é sempre o último carregamento.
Ok, e o método requestGitHubUserAccessToken(), para que temos ele?
Esse é responsável por obter o nosso access token. Antes de prosseguir para esse método vamos comentar a PARTE 3 do método sendLoginGitHubData(), obviamente relembrando o código dessa parte:
...
/* PARTE 3 */
AlertDialog.Builder alert = new AlertDialog.Builder(this);
alert.setTitle("Login GitHub");
alert.setView(webView);
alert.show();
...
Essa parte é útil para que não seja necessária abertura do Browser APP do Android, logo não temos de nos preocupar com um Activity na pilha de Activities que não seja de nosso controle, ou seja, parte de nossa APP. Além de esse AlertDialog nos permitir abrir o conteúdo ainda dentro de nossa Actiivty de login.
Agora seguimos para o método requestGitHubUserAccessToken():
private void requestGitHubUserAccessToken( String code ){
RequestTokenClient requestTokenClient = new RequestTokenClient(
code,
getString(R.string.github_app_id),
getString(R.string.github_app_secret),
getString(R.string.github_app_url)
);
requestTokenClient
.observable()
.subscribe(new Observer<Token>() {
@Override
public void onCompleted() {}
@Override
public void onError(Throwable e) {
showSnackbar( e.getMessage() );
}
@Override
public void onNext(Token token) {
if( token.access_token != null ){
requestGitHubUserData( token.access_token );
}
}
});
}
Nele utilizamos uma instância de RequestTokenClient, classe presente na GitHub Android API, utilizada para solicitar o access token aos servidores do GitHub. Logo abaixo da instanciação temos a vincução de um subscriber ao observer (ainda temos que falar sobre esse padrão no Blog) da instância de RequestTokenClient.
O método sobrescrito onNext() é o que nos importa. Pois devido a requisição do access token ser assíncrona, nosso new Observer(), mais precisamente o método onNext(), nos permiti saber se obtemos ou não o access token sem necessidade de criarmos um AsyncTask ou algo similar. Dentro desse método verificamos se o token não é null e então prosseguimos para o método requestGitHubUserData().
Ok, mas já temos o access token, para que outro método intermediário?
No caso, o GitHub, por um motivo ainda desconhecido, não fornecerá os dados do usuário por meio de nosso FirebaseUser, como em todos os outros métodos de login. Somente o UID é que tem um valor (e isso é muito bom).
O método requestGitHubUserData() vai permitir que nós realizemos a solicitação dos dados do usuário antes de vincular o login dele ao Firebase. Segue método:
private void requestGitHubUserData( final String accessToken ){
GetAuthUserClient getAuthUserClient = new GetAuthUserClient( accessToken );
getAuthUserClient
.observable()
.subscribe(new Observer<com.alorma.github.sdk.bean.dto.response.User>() {
@Override
public void onCompleted() {}
@Override
public void onError(Throwable e) {}
@Override
public void onNext(com.alorma.github.sdk.bean.dto.response.User user) {
LoginActivity.this.user.setName( user.name );
LoginActivity.this.user.setEmail( user.email );
accessGithubLoginData( accessToken );
}
});
}
Com o código muito similar ao método requestGitHubUserAccessToken(), no método acima precisamos do access token e logo depois, em onNext() obtemos os dados que nos interessam, nome e email, e então chamamos o método que vai nos permitir vincular esse usuário logado via GitHub ao nosso Firebase.
Segue código de accessGitHubLoginData():
private void accessGithubLoginData(String accessToken){
accessLoginData(
"github",
accessToken
);
}
Método simples e se você está acompanhando a série vai perceber que é muito similar aos métodos anteriores (Facebook, Google e Twitter) já apresentados nessa série.
Para que a vinculação utilize o GitHub provider do Firebase, temos de atualizar o método accessLoginData():
private void accessLoginData( String provider, String... tokens ){
if( tokens != null
&& tokens.length > 0
&& tokens[0] != null ){
AuthCredential credential = FacebookAuthProvider.getCredential( tokens[0]);
credential = provider.equalsIgnoreCase("google") ? GoogleAuthProvider.getCredential( tokens[0], null) : credential;
credential = provider.equalsIgnoreCase("twitter") ? TwitterAuthProvider.getCredential( tokens[0], tokens[1] ) : credential;
/* ADICIONE A LINHA ABAIXO */
credential = provider.equalsIgnoreCase("github") ? GithubAuthProvider.getCredential( tokens[0] ) : credential;
user.saveProviderSP( LoginActivity.this, provider );
mAuth.signInWithCredential(credential).addOnCompleteListener(new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if( !task.isSuccessful() ){
showSnackbar("Login social falhou");
}
}
});
}
else{
mAuth.signOut();
}
}
Veja que agora temos um novo operador ternário, somente para verificar se é o login com GitHub que foi realizado, caso sim utilizamos o GitHubAuthProvider do Firebase.
Com isso podemos verificar o novo código do nosso listener FirebaseAuth.AuthStateListener.
Por que, se até agora não falamos dele?
Porque, devido a uma falha de lógica do código do post anterior, esse listener está apagando os nomes e emails de usuários que voltam a acessar a APP depois de terem já realizados a login. Segue novo código do métodogetFirebaseAuthResultHandler() que retorna uma instância do listener FirebaseAuth.AuthStateListener:
private FirebaseAuth.AuthStateListener getFirebaseAuthResultHandler(){
FirebaseAuth.AuthStateListener callback = new FirebaseAuth.AuthStateListener() {
@Override
public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) {
FirebaseUser userFirebase = firebaseAuth.getCurrentUser();
if( userFirebase != null
&& user.getId() == null
&& isNameOk( user, userFirebase ) ){
user.saveIdSP( LoginActivity.this, userFirebase.getUid() );
user.setId( userFirebase.getUid() );
user.setNameIfNull( userFirebase.getDisplayName() );
user.setEmailIfNull( userFirebase.getEmail() );
user.saveDB();
callMainActivity();
}
}
};
return( callback );
}
Veja que agora temos uma condição a mais em nossa estrutura de seleção, o isNameOk(), pois ter um nome de usuário é fundamental para nossa APP permitir o login. Vamos verificar o código desse método:
private boolean isNameOk( User user, FirebaseUser firebaseUser ){
return(
user.getName() != null
|| firebaseUser.getDisplayName() != null
);
}
Por que verificar user se ele será preenchido no listenerFirebaseAuth.AuthStateListener?
Na verdade a verificação de user se fez necessária devido ao login com o GitHub, pois como os dados não são recuperados pelo FirebaseUser, os nome e email do usuário passam a ser vazios no Firebase database. Isso porque o método requestGitHubUserData() somente é chamado se o usuário não tiver ainda logado e se for querer logar via GitHub. Ou seja, com essa verificação protegemos os dados do usuário GitHub que está voltando a APP.
Os métodos setNameIfNull() e setEmailIfNull() são autocomentados, ou seja, somente se as variáveis de instância de User tiverem o valor null é que elas receberão os valores passados como parâmetros. Veja os códigos desses métodos:
public class User {
...
public void setNameIfNull(String name) {
if( this.name == null ){
this.name = name;
}
}
...
public void setEmailIfNull(String email) {
if( this.email == null ){
this.email = email;
}
}
...
}
Com isso, ainda temos um problema. Voltando ao condicional do getFirebaseAuthResultHandler():
...
if( userFirebase != null
&& user.getId() == null
&& isNameOk( user, userFirebase ) ){
user.saveIdSP( LoginActivity.this, userFirebase.getUid() );
user.setId( userFirebase.getUid() );
user.setNameIfNull( userFirebase.getDisplayName() );
user.setEmailIfNull( userFirebase.getEmail() );
user.saveDB();
callMainActivity();
}
...
Temos que se o usuário logar via GitHub ou via Email e Senha, se ele sair da APP, quando voltar terá de realizar o login novamente, pois em ambos os tipos de login o método isNameOk() será false na volta do usuário a APP. Logo o que temos de fazer é acrescentar um verificador de usuário logado:
private void verifyLogged(){
if( mAuth.getCurrentUser() != null ){
callMainActivity();
}
else{
mAuth.addAuthStateListener( mAuthListener );
}
}
Ok, desde o post anterior já sabiamos que o FirebaseAuth nos permitia fazer essa verificação, de usuário conectado, logo vamos utilizá-la nesse novo método. Onde devemos chamar esse novo método? Exatamente onde atribuíamos o listener mAuthListener ao mAuth, em nosso método de ciclo de vida da Activity, onStart():
@Override
protected void onStart() {
super.onStart();
verifyLogged();
}
Com isso já podemos realizar o login via GitHub em nossa APP. Segue LoginActivity:
Quando logado:
Com isso finalizamos a atualização de nossa APP de chat com Firebase, agora com o login social do GitHub. Abaixo o vídeo com o passo a passo do código acima.
Se não viu ainda, segue os posts já liberados dessa série:
Recuperação de Senha, Firebase Atualizado - Parte 8
Twitter Login (Fabric), Firebase Android - Parte 7
Google SignIn API, Firebase Android - Parte 6
Facebook Login, Firebase Android - Parte 5
Remoção de Conta e Dados de Login, Firebase Android - Parte 4
Atualização de Dados, Firebase Android - Parte 3
Eventos de Leitura e Firebase UI Android - Parte 2
Persistência Com Firebase Android - Parte 1
Segue links referências:
Authenticate using GitHub on Android
GitHub documentação - Web Application Flow
RFC - Authorization Code Grant
Vlw
Comentários Facebook