LoginSignup
2

More than 3 years have passed since last update.

Kotlin, LiveData, coroutine なんかを使って初めてのAndroidアプリを作る(12)ユーザー認証編(Firebase Authentication)

Last updated at Posted at 2020-05-17

前回の続きです。

Firebaseのクラウドデータベースである、Firestoreを使う準備として、ユーザー認証を行います。
ユーザー認証には、Firebase Authenticationを使います。

今回の目標

Firebase AuthenticationのFirebaseUI Authを使ってユーザー認証(ログイン/ログアウト)が出来る。様々な認証フローが使える。

環境など

AndroidStudioを3.6.3に上げました。

Firebase Authentication

Firebase Authenticationには、2つの実装手段が用意されています。
FirebaseUI Authと、Firebase Authentication SDKです。
https://firebase.google.com/docs/auth?authuser=0

1.FirebaseUI Auth

ドロップイン認証ソリューション

と書かれています。
様々なログイン方法(メールアドレス+パスワード/TwitterなどのSNS認証/電話番号認証)などでの認証フローを全部やってくれるものです。

要は、認証ページから何から、すべてお任せ出来るってことです。
アプリですることは、認証UIを呼び出す、ログアウトページからログアウトできるようにしておく、程度のことになります。
このアプリでは、こちらを使います。

2.Firebase Authentication SDK

様々な種類の認証フローを、自分で組み合わせて適宜呼び出す方法です。UIはすべて自作する必要があり、またSNS認証を行う場合は、OAuthトークンなどは自分で貰ってきてSDKにわたすと言うことが必要になります。

こちらは、ログイン画面は既に他で作っていて(変えづらく)、認証フローを追加せざるを得なくなった場合や、認証フローの中に独自の処理を挟まなければならないような場合に使えるかと思います。

3.使える認証方法

FirebaseUI Authで使える認証方法/認証プロバイダは、以下の物があります。

  • メール+パスワード
  • 電話番号
  • Google
  • Twitter
  • Facebook
  • Apple
  • Microsoft
  • GitHub
  • Yahoo

詳しくはこちらに。
https://github.com/firebase/FirebaseUI-Android/blob/master/auth/README.md

このうち、Facebookは、Facebookへの開発者登録が必要になります。
また、Facebookのみ、FacebookSDKをアプリに設定する必要があります。

準備

1.証明書のSHA-1情報の取得

まず準備として、証明書のSHA-1が必要になるログインフローがあるので、それを取得して設定しておきましょう

(1)デバッグビルド用の証明書を作ってない場合

デバッグビルド用の証明書を自前で用意していない場合は、作ってください。
というのも、AndroidStudioがデフォルトで使う開発者証明書は、開発マシンによって異なるからです。AndroidStudioをアップデートしたり、PCのOSをアップデートしたりすると変わってしまう可能性もあります。例え個人でやっていてもその都度書き変えるのは現実的ではありません。
なので、デバッグビルド用の証明書も作ってしまっておきましょう。

作成方法は、こちらなどを参考にしてください。
Kotlin, LiveData, coroutine なんかを使って初めてのAndroidアプリを作る(10)リリースビルドとGithub Actions編 #3デバッグ証明書を作成する

(2)デバッグビルド用の証明書がある場合

証明書のあるフォルダ(app)まで移動して、次のようにコマンドを打ちます。

$ keytool -exportcert -list -v \
-alias エイリアス名 -keystore <path-to-debug-keystore>

パスワードを聞かれるので、キーストアのパスワードを入力します。

このシリーズの手順でやってきた場合、デバッグビルド用の証明書は、エイリアス名=androiddebugkey、パスワードはandroidのはずです。
それ以外の人は、作ったときの情報を思い出して下さい。(忘れていたら証明書の作り直しですw)

次のように出力されるので、証明書のフィンガープリントのうち、SHA1となっている部分をコピーします。(SHA256でも多分大丈夫です。お好みで)

続いて、Firebaseコンソールのプロジェクト設定ページで、[全般]タブの下の方にある[マイアプリ]で、デバッグ用のアプリを選びます。

  • [フィンガープリントを追加]をクリック

qiita12_18.png

  • 先ほどコピーした値を貼り付けて保存する

(3)リリースビルド用の証明書がある場合

デバッグビルド用の証明書と同じ手順で、リリース証明書のSHA1(またはSHA256)をコピーしてセットします。

このシリーズをやってきてkey.propertiesファイルを作ってある人は、その中の情報を参照できますね。

両方セットできたら準備は完了です。

2.Facebookの開発者登録

Facebookログインを入れない人は飛ばして結構です。
こちらのページから登録を開始します。
https://developers.facebook.com/

なお、このアカウントは、通常のFacebookアカウントとは別に作成してます。
多分別にした方が良いかと思います。

(1)氏名など情報を入力する

qiita12_01.png

(2)メールが届くので、認証コードを入力する

qiita12_02.png

認証するとこのように表示されます。

qiita12_03.png

ショートカットは要らないでしょう。[後で]をクリックします。

qiita12_04.png

(3)開発アカウント認証をする

上記手順だと、普通のFacebookページに遷移してしまうので、改めて以下を表示します。
https://developers.facebook.com/tools

  • 右上の[スタート]をクリック

qiita12_05.png

  • [次へ]をクリック

qiita12_06.png

  • アカウント認証の実行をする

    • SMSまたは電話のどちらかお好きな方で
    • SMSにする場合、電話帳未登録を拒否している場合は解除が必要です
  • ロールを選ぶ

    • 私は「開発者」にしました

qiita12_07.png

  • 「最初のアプリを作成」をクリック

qiita12_08.png

  • アプリの表示名を入力して[アプリIDを作成してください]をクリック

qiita12_09.png

  • ロボットチェックをこなす
  • 開発アプリページが開く

qiita12_10.png

(4)開発アプリを登録する

  • デバッグ証明書のSHA1ハッシュをbase64エンコードした文字列を以下のコマンドで出力し、コピーしておく
$ cd app
$ keytool -exportcert -alias エイリアス名 -keystore 
      <path-to-debug-keystore> | openssl sha1 -binary | openssl base64
  • 左側のメニューにある、[設定]を開き、[ベーシック]をクリック
  • 右のページの下までスクロールすると、[+プラットフォームを追加]があるので、それをクリック
qiita12_90.png
  • [Android]を選ぶ

qiita12_91.png

  • 必要な情報を入力する
    • Google Playパッケージ名 : debugアプリのパッケージ名です。suffixID付けている場合は付け忘れないように注意
    • クラス名 : アプリの起動画面です。Splash等を用意している場合はそちらのクラス名を指定します。これを後から変更するのはアプリの再申請(Facebook側でもアプリをレビューします)が必要となるため、一度リリース後は変えられないと思った方が良いです。
    • キーハッシュ : デバッグ用証明書のSHA1のキーハッシュを貼り付ける
    • その他はデフォルトのままでOK.
qiita12_93.png
  • [変更を保存]をクリック
    • Playストアにパッケージがないと言われますが、リリース前なので当然です。無視して[このパッケージ名を使用する]をクリックして下さい

qiita12_25.png

  • [設定]-[詳細設定]を開く
  • ネイティブアプリまたはデスクトップアプリはいにする
qiita12_31.png
  • [製品を追加]にある、[Facebookログイン]をクリック
qiita12_21.png

いったんは以上でOKです。

3.Twitter開発者登録

Twitterログインを入れない場合は飛ばして結構です。

(1)開発用アカウントを作成する

多分、個人のアカウントとは分けた方が良いです。

(2)開発者申請する

下記のページで、先ほど作ったアカウントでログインします。
https://dev.classmethod.jp/articles/twitter-developer/

  • [Create an app]をクリック
qiita12_41.png
  • [Apply]をクリック
    • よく読んでくださいね。

qiita12_42.png

  • 目的を聞かれるので、自分に合った物を選んで[Next]をクリック
    • 無料でリリースするアプリはどれが一番いいんだろう?と思いますが、[Building consumer producs]としてみました。
qiita12_43.png
  • [Add a valid phone number]をクリックして電話番号認証を行う
    • SMSのアドレス帳登録以外を拒否や非通知にしている場合は気をつけて下さい。
qiita12_44.png
  • 戻ってページをリロードする
  • Team developer accountの[Switch to an indivitual developer account]をクリック
    • 業務で作成する場合は、そのままにして次のページで必要情報を入力して下さい。
qiita12_45.png
  • 下に項目が追記されるのでスクロールして表示
    • What country do you live in? : Japanを選択
    • What would you like us to call you? : 開発アカウント名を入力(多分サポートとやりとりするのに使うのかと)
    • Want updates about the Twitter API? : TwitterAPIの更新通知を受け取るかどうか任意でセット

qiita12_46.png

  • Nextをクリック

  • アプリの目的などを入力する

    • 以下は私の場合です。丸々同じだとスパムアプリのように思われるので、ご自分で考えて書くか、もっとちゃんとした英語の出来る人に考えて貰いましょうw
qiita12_47.png
qiita12_48.png
qiita12_49.png
  • [Next]をクリックすると、確認画面になるので、内容をもう一度確認して、問題なければ[Looks good!]をクリック
qiita12_50.png
  • メールを認証しろと出るので、メールのリンクをクリックして認証を完了させる
    • これ忘れるとレビューが進まないようですのでご注意を。

なお、日本語でも申請可能だという記事もありましたが、承認に時間がかかったり、もう一度やりとりする必要が生じる可能性があるとのことです。
https://dev.classmethod.jp/articles/twitter-developer/

※私は英語でやり、直ぐに承認メールが来ました。

(3)開発アプリを登録する

  • 開発者ページにログイン
  • [Create an app]をクリック
qiita12_52.png
  • 右上の[Create an app]をクリック
qiita12_51.png
  • アプリ名を入力
  • アプリの説明(ユーザーが読む)を入力
qiita12_53.png
  • Wbesite URL

    • 必須です。Firebase Hostingとか、無料のホームページツールとかで、適当なページを作ってアプリの説明を書いて置いておきましょう。リリース済みのアプリなら、Playストアへのリンクとかがあってもいいですね。取り敢えず、今回はFirebase Hostingで簡単なページを作っておきました。CSSとか苦手なんで酷いページですw
    • Firebase Hostingの導入については、こちらなどを参考にして下さい。 Firebase HostingでFlutterアプリのプライバシーポリシーのページを作る
  • Enable Sign in with Twitter

    • 今はチェックしないでおきます。
qiita12_54.png
  • アプリの説明(Twitter社が読む)を英語で記入
qiita12_55.png
  • [Create]をクリック

    • クリックできないときは、文字数を満たしてないなど不十分な項目があります。
  • 規約をよく読んで、同意できる場合は[Create]をクリック

qiita12_56.png

作成できたら、今はいったんここまででOKです。

認証の設定と実装

1.Firebaseコンソールで認証方法を設定

(1)認証フローの決定

今回は、以下でやってみようと思います。

  • メール+パスワード
  • Google
  • Twitter
  • Facebook
  • GitHub

手順はほとんど同じになってくるので、お好みで良いと思います。
個人的には、MicrosoftやAppleログインがあるのが、へえー!と思いました^^;
なお、iOSアプリの場合、ログイン機能があるアプリにはAppleログインが必須なようですね。

(2)メールログインの設定

  • [ログイン方法を設定]をクリック

qiita12_10.png

  • [Sign-in method]タブを選ぶ
  • メール/パスワードにカーソルを合わせ、右に出てきたエディットアイコンをクリック

qiita12_11.png

  • トグルボタンをクリックして、[有効]にする
    • メールリンクは、無効にしておきます(仕様次第)
    • パスワード再発行などもやってくれるんですね。至れり尽くせりです!

qiita12_13.png

  • [保存]をクリック

(3)Googleログインの設定

Androidスマホを持っている人なら持って無いはずはないGoogleログインを使わない手はありませんね。

同じように、[Sign-in method]のタブから追加していきます。

  • Googleの行にマウスポインタをホバリングしてエディットアイコンをタップ
  • トグルボタンをクリックして、[有効]にする

プロジェクト設定のアプリ名やサポートメールを設定するように出た場合は、ここで設定できます。ただ、メールアドレスはログインしているアカウントのものしか選べません。もし、サポートメールのアドレスを変更したい場合には、そのアドレスを持つ「オーナー」アカウントをもう一名Firebaseプロジェクトに招待して、そちらのアカウントでFirebaseコンソールにログインする必要があります。

qiita12_14.png

以下の手順は任意です。今のアドレスで問題ない人は、そのメールアドレスを設定して、[保存]ボタンをクリックして下さい。

オーナーアカウントを追加するには、プロジェクト設定ページの右上の、[ユーザーと権限]タブを表示します。

qiita12_14_2.png

招待したアカウントに招待メールが届くので、そのアカウントで、Firebaseコンソールにログインします。
そして、そのアカウントで、プロジェクト設定のページを開きます。

qiita12_16.png

サポートメールのところで、ドロップダウンリストを表示すると、メールアドレスを変更できます。(一度そのアカウントものに変えると、もう一度他のものへの変更は出来ません。また変更したい方のアカウントからログインして行う必要があります。)

FirebaseUI Auth設定のページに戻って、リロードします。
Googleログインの設定編集ページを開き、有効にして、[保存]をクリックします。

(4)Facebookログインの設定

  • Facebookの編集アイコンをクリックします。
  • トグルボタンをクリックして、[有効]にする
  • アプリケーションIDをFacebook開発者ページからコピーして貼り付ける

qiita12_19.png

  • アプリシークレットも、Facebook開発者ページにあります。左側のメニューから、設定の[ベーシック]を開くと、右側にあります。
qiita12_20.png
  • OAuthリダイレクトURLをコピーして、[保存]をクリック

次に、Facebook開発者ページでOAuthリダイレクトURLを設定します。

  • 左側のメニューの[Facebookログイン]のリストを開き、設定を選ぶ
  • 有効なOAuthリダイレクトURIに、先ほどコピーしたURLを貼り付ける
qiita12_30.png
  • [変更を保存]をクリック

(5)Twitterログインを設定

  • Twitterの開発アプリのページから、アプリの[Details]を開く
qiita12_57.png
  • [Keys and tokens]タブを開く
  • API keyAPI secret keyをコピーしておく
qiita12_58.png
  • Firebaseコンソールの[Authenticatoin]-[Sign-in method]で、Twitterの編集アイコンをクリックする
  • トグルボタンをクリックして、[有効]にする

    • Twitterの開発アプリのAPI keyAPIキーに貼り付ける
    • Twitterの開発アプリのAPI secret keyAPIシークレットに貼り付ける
  • コールバックURLをクリックする

  • [保存]をクリック

qiita12_60.png

  • Twitterの開発アプリのページから、アプリの[Details]を開く
  • [Edit]-[Edit details]をクリック
  • Enable Sign in with Twitterにチェックを入れる
  • Callbak URLsに先ほどコピーしたURLを貼り付ける
qiita12_59.png
  • [Save]をクリック

(6)Githubログインを設定

一般ユーザーはこれよりYahooがあった方が良いような気がするけどもうこれ以上開発アカウントを作るのもアレなので。Githubならこの記事の読者はみんな持ってるよね。

  • Firebaseコンソールの[Authenticatoin]-[Sign-in method]で、Githubの編集アイコンをクリックする
  • トグルボタンをクリックして、[有効]にする
  • 認証コールバックURLをコピーする

いったんGithubのページへ行きます。
必要なアカウントでログインして下さい。
https://github.com/settings/applications/new

  • 必要な情報を入力 ここにもHomepageURLがあるので、Twitter用に作ったページを設定できますね。
  • Authorization callback URLに、先ほどコピーしたURLを貼り付ける
  • [Register app]をクリック
  • 表示されたページのCllient IDClient Secretをメモする
qiita12_71.png

Firebaseに戻ります。

  • クライアントIDとクライアントシークレットに、GithubのClient IDとClient Secretをそれぞれ貼り付ける
qiita12_72.png
  • [保存]をクリック

お疲れ様でした。準備はこれで終わりです。
やっとAndroidアプリの方に取りかかります。

3.AndroidアプリにFirebaseUI Authを導入する

(1)依存関係の追加

app/build.gradleに以下を追記します。
FacebookのSDKはFacebookログインを入れない場合は不要です。

app/build.gradle
    implementation 'com.firebaseui:firebase-ui-auth:6.2.1'
    implementation 'com.facebook.android:facebook-android-sdk:7.0.0'

なお、FacebookSDKの最新バージョンは以下から見つけられます。

※日本語のFirebaseドキュメントには、TwitterSDKも依存関係に入れるように書いてありますが、firebase-ui-auth:6.2.0から不要になったようです。

それと、他言語の膨大な文字列リソースを読み込まないように、可能なら言語を指定しておきましょう。apkのサイズが肥大化してしまうのを防げます。
ただし英語だけは入ってしまうようです。

app/build.gradle
    defaultConfig {
        ...
        resConfigs "ja"
    }

Gradle Syncしておいて下さいね。

(2)ビルドしてみる

取り敢えずビルドしてみて下さい。

通らなかった!と言う人もいるでしょうね。

多分そういう方は、minSDKVersoinを20以下にしていませんか?
残念でしたね。これは20以下にしているアプリの宿命です。

エラーメッセージの最後の方に、こんなふうに出ていると思います。

* What went wrong:
Execution failed for task ':app:mergeDexDebug'.
> A failure occurred while executing com.android.build.gradle.internal.tasks.Workers$ActionFacade
   > com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: 
     The number of method references in a .dex file cannot exceed 64K.
     Learn how to resolve this issue at https://developer.android.com/tools/building/multidex.html

はい、Andoridの非常に厳しい制限がここで登場します。
Androidでは、デフォルトでは1つのアプリに65535個を超えるメソッドや変数があってはならないのです。

この数字に、ITエンジニアなら覚えはあるでしょうか。最近の人はないかなあ。
基本情報技術者試験を受けている人ならピンとこないと行けませんよ。

プログラマー的な言い方をすれば、SHORT型なんだね、と。
もっと一般的?な言い方をすると、16bitを超えられないということになります。

「今のご時世に16bitの制限があるとは」

と驚きたくもなりますが、Windowsのフルパス名も長らく255文字の制限(こちらは8bit^^;)があったことを考えると、そこの型のサイズを変えるのはとても大変なのでしょう。

じゃあ、どうするか?このままじゃ、アプリビルドできない?
何か機能を減らさないとダメなのか??
minSDKVersionを21以上に上げなきゃダメ?

なんてことはありません。ちゃんと回避策はあります。
先ほど、「Androidでは、デフォルトでは1つのアプリに〜」と書きましたが、もう少し厳密に言うと、AndroidはJavaのクラスファイルを.dexというファイルに書き出していて、その.dexファイルの1つには、64kの制限があるのです。
で、デフォルトでは、その.dexファイルを1つしか作ろうとしないために、64k超えたらビルドできなくなるわけです。

(3)ようこそMultidexの世界へ(minSDKVersion<21限定)

ということで、複数のdexファイルを作ってくれるように指定します。

  • app/build.gradleに、multiDexEnabled trueとライブラリを追加します。
app/build.gradle
    android {
        defaultConfig {
            ...
            minSdkVersion 19
            targetSdkVersion 28
            multiDexEnabled true
        }
        ...
    }

    dependencies{
        ....
        // multidex
        implementation 'androidx.multidex:multidex:2.0.1'
    }
  • MyAppクラスの基底クラスを、MultiDexApplicationにする
MyApp.kt
class MyApp : MultiDexApplication()

これでビルド、実行が出来るはずです。

(4)マニフェストファイルとリソースファイル

  • マニフェストファイルにINTERNETパーミッションを追加していない場合は追加する
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="jp.les.kasa.sample.mykotlinapp">

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

    <application ...
  • Facebookログインをする場合、/app/res/values/strings.xmlに次のように追加
/app/res/values/strings.xml
    <string name="facebook_application_id" translatable="false">{アプリID}</string>
    <!-- Facebook Application ID, prefixed by 'fb'. Enables Chrome Custom tabs. -->
    <string name="facebook_login_protocol_scheme" translatable="false">fb{アプリID}</string>

{アプリID}の部分を、FacebookのアプリIDに置き換えて下さい。

※Firebaseのドキュメント(英語も)には、Twitterのクライアントシークレットなども記述するように記載がありますが、不要です。

(5)ログインメニューを作る

ログインの契機となる場所を作っていきます。
アプリの初回起動時でも良いですが、いったん、このアプリは、

  • クラウドでデータを同期したいユーザーだけが、ログインをする

とします。
つまり、ローカルでデータを記録するだけならログインしなくてもアプリを使えるということにします。
RoomとFirestoreの共存になり大変ですが、まあ勉強なので。
それに、Firestoreを使い始めると、多分ユーザー数によりますがお金が掛かってきます。
なので、「クラウド保存は課金して使える機能にする」とか、そんなことも出来ますね。
その場合、Room⇔Firestoreのデータ変換も必要になるので、かえって大変かも知れませんが・・・

ということで、アプリの右上に、ログインメニューを追加することにします。

Activityへのメニューの追加は、こちらでやりましたね。
Kotlin, LiveData, coroutine なんかを使って初めてのAndroidアプリを作る(3)リスト表示編

Android Studioで、[res]-[menu]を右クリックし、[New]-[Menu Resource File]とします

qiita12_80.png

ファイル名はmain_menu.xmlとします。

qiita12_81.png

出来たファイルの中身を以下のようにします。

main_menu.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
            android:id="@+id/login"
            android:icon="@drawable/ic_person_24dp"
            android:title="@string/menu_label_login"
            app:showAsAction="ifRoom" />
</menu>

アイコンは、[Drawable]-[New]-[Vector Asset]で作りました。
menu_label_login<string name="menu_label_login">ログイン</string>です。
今後メニューが増えるかもしれないので、app:showAsActionifRoomにしました。ifRoomにしておくと、Toolbarに十分なスペースがあればアイコンが表示され、足りないとメニューアイコンでまとめられてポップアップでリストが出てくるようになります。

(6)MainActivityにメニューを追加

MainActivityにこのメニューを追加します。

MainActivity.kt
    // メニュー追加
    override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        val inflater = menuInflater
        inflater.inflate(R.menu.main_menu, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        item?.let {
            return when (it.itemId) {
                R.id.login -> {
                    val intent = Intent(this, SignInActivity::class.java)
                    startActivityForResult(intent, REQUEST_CODE_SIGN_IN)
                    true
                }
                else -> false
            }
        }
        return false
    }

REQUEST_CODE_SIGN_INcompanion objectで他のリクエストコードと被らない
任意の数値を指定して下さい。

(7)ログインページの追加

新しいActivityを作成します。LoginActivityなどでいいでしょう。でも個人的に、LogInputとかLogItemとか同じスペルで始まるのが多いので、ここはSignInActivityにしました。
ちなみに、一般的に"Sign Up"だと「アカウント作成」で、"Sgin in"だといわゆる「ログイン」として使われていますね。パッケージ名は悩んだのですが、いったんsigninにしました。

レイアウトはこんな感じです。

qiita12_97.png

サンプルコードはこちらから
activity_signin.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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"
        tools:context="jp.les.kasa.sample.mykotlinapp.activity.signin.SignInActivity">

    <com.google.android.material.appbar.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/AppTheme.AppBarOverlay">

        <androidx.appcompat.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" />

    </com.google.android.material.appbar.AppBarLayout>

    <ScrollView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:padding="16dp"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

        <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">

            <ImageView
                    android:layout_width="100dp"
                    android:layout_height="100dp"
                    android:layout_gravity="center_horizontal"
                    android:src="@drawable/ic_cloud_upload_24dp"
                    android:tint="@color/colorPrimary" />

            <TextView
                    android:id="@+id/descriptionSignIn"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="16dp"
                    android:text="@string/text_sign_in_description" />

            <Button
                    android:id="@+id/buttonSignIn"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center_horizontal"
                    android:layout_marginTop="8dp"
                    android:text="@string/label_sign_in" />
        </LinearLayout>
    </ScrollView>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

実際にログインをするページと言うよりは、ログインすることで出来ることなどの説明のページになります。
たとえば、この機能を有料にしたい場合は、ここで課金する機能を入れても良いでしょう。

signin/SignInActivity.kt
class SignInActivity : BaseActivity() {
    companion object {
        const val SCREEN_NAME = "サインイン画面"
    }

    // 画面報告名
    override val screenName: String
        get() = SCREEN_NAME

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_signin)
        setSupportActionBar(toolbar)

        supportActionBar?.setDisplayHomeAsUpEnabled(true)

        buttonSignIn.setOnClickListener {
            // TODO FirebaseUI Auth呼び出し
        }
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            android.R.id.home -> {
                onBackPressed()
                return true
            }
        }
        return super.onOptionsItemSelected(item)
    }
}

(8)FirebaseUI Authの呼び出し

いよいよFirebaseUI Authを呼び出します。

signin/SignInActivity.kt
        buttonSignIn.setOnClickListener {
            analyticsUtil.sendSignInStartEvent()

            // Choose authentication providers
            val providers = arrayListOf(
                AuthUI.IdpConfig.EmailBuilder().build(),
                AuthUI.IdpConfig.GoogleBuilder().build(),
                AuthUI.IdpConfig.FacebookBuilder().setPermissions(listOf("email")).build(),
                AuthUI.IdpConfig.TwitterBuilder().build(),
                AuthUI.IdpConfig.GitHubBuilder().build()
            )

            FirebaseCrashlytics.getInstance().log("FirebaseUI Auth called.")

            // Create and launch sign-in intent
            startActivityForResult(
                AuthUI.getInstance()
                    .createSignInIntentBuilder()
                    .setAvailableProviders(providers)
                    .build(),
                REQUEST_CODE_AUTH
            )
        }

呼び出しは、これだけ。REQUEST_CODE_AUTHcompanion objectに定義して下さいね。整数値ですよ。

val providers = arrayListOfに、FirebaseコンソールでSign-in Methodに設定した認証方法を設定しています。
問い合わせやクラッシュに繋がりそうなのでレポートもいろいろ仕込みました。

FacebookだけsetPermissions(listOf("email"))しているのは、何も指定しないとそれ以外のdefault権限に定義されているユーザー情報も取ってしまうらしいので、このアプリでは不要なため取らないようにするためです。(確か、defaultで取れるように権限付けてくれている癖に、「不要なのに要求している」とFacebookからリジェクトされたと聞いたことがあります--;)

startActivityForResultで、FierbaseUIを呼び出しています。
普通のActivity遷移と変わりませんね。startActivityForResultですから、onActivityResultで結果を受け取ります。

signin/SignInActivity.kt
   override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == REQUEST_CODE_AUTH) {
            FirebaseCrashlytics.getInstance()
                .log("FirebaseUI Auth finished. result code = [$resultCode]")

            val response = IdpResponse.fromResultIntent(data)

            if (resultCode == Activity.RESULT_OK) {
                // Successfully signed in
                val user = FirebaseAuth.getInstance().currentUser

                analyticsUtil.sendSignInEvent()
                // TODO Roomのデータをコンバートしてアップロード
                // or Firestoreからデータをダウンロード

            } else response?.error?.errorCode?.let { errorCode ->
                analyticsUtil.sendSignInErrorEvent(errorCode)

                FirebaseCrashlytics.getInstance()
                    .log("FirebaseUI Auth finished. error code = [$errorCode]")
                // Sign in failed. If response is null the user canceled the
                // sign-in flow using the back button. Otherwise check
                // response.getError().getErrorCode() and handle the error.
                // ...
                showError(errorCode)
            }
        }
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun showError(errorCode: Int) {
        val messageId =
            when (errorCode) {
                EMAIL_MISMATCH_ERROR -> {
                    // メールアドレス不一致
                    R.string.error_email_mismacth
                }
                ERROR_GENERIC_IDP_RECOVERABLE_ERROR, PROVIDER_ERROR -> {
                    R.string.error_id_provider
                }
                ERROR_USER_DISABLED -> {
                    R.string.error_user_disabled
                }
                NO_NETWORK -> {
                    R.string.error_no_netowork
                }
                PLAY_SERVICES_UPDATE_CANCELLED -> {
                    R.string.error_service_update_canceled
                }
                else -> {
                    R.string.error_unknown
                }
            }

        val error = getString(R.string.label_error_code, errorCode)
        val message = "${getString(messageId)}\n\n$error"
        val dialog = ErrorDialog.Builder().message(message).create()
        dialog.show(supportFragmentManager, DIALOG_TAG_AUTH_ERROR)
    }

あとでテストすることを考えて、showErrorには@VisibleForTestingを付けています。

アプリを起動して、いろいろな認証方法でログインしてみましょう。

なお、端末の言語設定が英語だと英語でUIが表示されます。
エミュレーターだと日本語にしてないこともあるかと思うので、ご注意を。

qiita12_100.png

メールログインだと、初めての登録とか、パスワード忘れたときのリセットリンクとか、全部やってくれます。なお、その場合のメールのテンプレートは、Firebaseのコンソールで[Authentication]の[Templates]タブで変更が出来ます。ただし変更できるのは「パスワードの再設定」だけ見たいです。スパム防止のためらしいですが・・・

Facebookは、Facebookアプリがインストールされてない場合は、ブラウザが起動してブラウザログインになります。
注意事項としては、Facebookは、「開発者アカウント」でログインする必要があることです。
というのも、アプリがまだ「開発」で登録してあるからです。リリースモードにするには、Facebookのレビューが必要になります。
逆にGithubは開発者アカウント「以外」でログインしないとダメなようです。
TwitterはどちらでもOKでした。また、Twitterアプリのインストールに関係なく、ブラウザログインでの認証がされます。

※Twitter/Githubは、ブラウザからログイン済みで、アカウント認証しようとすると、AuthUIがエラーになってしまいます。エラーメッセージは何も出ませんが、コンソール見ると例外ログが出ています。

2020-05-16 03:35:29.515 22989-22989/jp.les.kasa.sample.mykotlinapp.debug E/AuthUI: A sign-in error occurred.
    com.firebase.ui.auth.data.model.UserCancellationException: Unknown error
        at com.firebase.ui.auth.data.remote.GenericIdpSignInHandler$1.onFailure(GenericIdpSignInHandler.java:112)
        at com.google.android.gms.tasks.zzl.run(Unknown Source:4)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
2020-05-16 03:35:29.819 22989-22989/jp.les.kasa.sample.mykotlinapp.debug E/AuthUI: A sign-in error occurred.
    com.firebase.ui.auth.data.model.UserCancellationException: Unknown error
        at com.firebase.ui.auth.data.remote.GenericIdpSignInHandler$1.onFailure(GenericIdpSignInHandler.java:112)
        at com.google.android.gms.tasks.zzl.run(Unknown Source:4)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

エミュレーターのせいだけかと思い、実機でやったら多分大丈夫でした。(試した限りでは・・・)

また、実機でテストする際、Facebookアプリやブラウザで通常アカウントをログアウトしておく必要があります。そうでないと、開発者以外のアカウントでのログインはまだ開発アプリの段階なので許可されません。

それと、サインアップすると識別子としてメールアドレスが記録されます。同じメールアドレスのユーザーは同じアカウントとみなさるため、例えば、同じGmailでFacebookやTwitterのアカウントを作っている場合、最初にGoogleログインしたあと、ログアウトして他の認証方法でログインしようとすると、「Googleアカウントでログイン済みです」のように表示され、別の方法ではログイン出来ませんのでご注意下さい。
同じメールアドレスで他の認証フローを試したい場合は、Firebase Authenticationのコンソールでユーザーを一度削除する必要があります。

以下、色々ログインしてみた後のFirebase Authenticationのコンソールページの[Users]タブの表示内容です。

qiita12_94.png

Githubのメールアドレスがなぜかこの時は提供されませんでした。
その後もう一度アカウントを作り直したら入っていたので、何かバグですかね・・・

ログイン画面のカスタマイズ

FirebaseUIが表示する画面は、変更することが可能です。
たとえば、ロゴをセットできます。
アプリアイコンなどを出すのが良いでしょうね。今はドロイドくんになってしまいますが。
いずれアイコン画像の作り方の回も必要ですね。

ロゴとテーマをセットする例

SignInActivity.kt
            startActivityForResult(
                authUI.createSignInIntentBuilder()
                    .setAvailableProviders(providers)
                    .setLogo(R.mipmap.ic_launcher) // ロゴをセット
                    .setTheme(R.style.SinUpTheme)  // テーマを変更
                    .build(),
                REQUEST_CODE_AUTH
            )

SignUpThemeはこんなので作ってみました。

styles.xml
    <!-- ログイン画面用の別テーマ -->
    <style name="SinUpTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="colorPrimary">@color/colorPrimarySignIn</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDarkSignIn</item>
        <item name="colorAccent">@color/colorAccentSignIn</item>
        <item name="android:windowBackground">@color/colorBackgroundSinIn</item>
    </style>

windowBackgroundが直接カラー指定だとビルドエラーになってしまうので、わざわざcolorリソースも作りました。

colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
  ...
    <color name="colorPrimarySignIn">#5C6BC0</color>
    <color name="colorPrimaryDarkSignIn">#303F9F</color>
    <color name="colorAccentSignIn">#FF6D00</color>
    <color name="colorBackgroundSinIn">#B2EBF2</color>
</resources>

もっとカスタムしたレイアウトを使うことも出来ます。

SignInActivity.kt
            val authLayout = AuthMethodPickerLayout.Builder(R.layout.layout_auth)
                .setEmailButtonId(R.id.email_button)
                .setFacebookButtonId(R.id.facebook_button)
                .setTwitterButtonId(R.id.twitter_button)
                .setGoogleButtonId(R.id.google_button)
                .build()

            // Create and launch sign-in intent
            startActivityForResult(
                authUI.createSignInIntentBuilder()
                    .setAvailableProviders(providers)
                    .setAuthMethodPickerLayout(authLayout)
                    .build(),
                REQUEST_CODE_AUTH
            )

R.layout.layout_authに使いたいレイアウトを組んでおき、それをAuthMethodPickerLayout.Builderに指定、各ボタンIDをauthUI側に連携させるためにセットする、という形になります。なお、setGithubButtonIdがないので、この場合はGithubログインを諦めるしか無さそうです。
(一応Issuesに登録してみました: https://github.com/firebase/FirebaseUI-Android/issues/1783)

setTosAndPrivacyPolicyUrlsで利用規約ページとプライバシーポリシーページをセットすると、自動的にページに表示してくれます。

SignInActivity.kt
            startActivityForResult(
                authUI.createSignInIntentBuilder()
                    .setAvailableProviders(providers)
                    .setLogo(R.mipmap.ic_launcher)
                    .setTheme(R.style.SinUpTheme)
                    .setTosAndPrivacyPolicyUrls(
                       "https://qiitapedometersample.web.app/policy.html",
                       "https://qiitapedometersample.web.app/policy.html"
                    )
                    .build(),

ロゴ、テーマ、ポリシーページを入れたサンプルです。
配色の悪さは気にしないで下さいw

qiita12_95.png

ログアウトページ

ログイン中は、先ほどの画面をログアウトできるようにして行きます。

ここはActivityの中でレイアウトを切り替えるより、起動時にログイン中だったら、ログイン中のActivityを起動して自分は終了することにします。
(※このアプローチだけが正解ということはありません。アプリの設計思想に依ります。例えば、Fragmentを分けておいて、ログイン中かどうかで出し分けるというのもアリです。ただ、Fragmentのテストが結構厳しいので、Activityにしておきたい感じです^^;)

1.ログイン中判定

FirebaseAuth.getInstance().currentUserがnullでないかで判定できます。
なので、画面を起動する場合、こうなります。

SignInActivity.kt
    override fun onResume() {
        super.onResume()
        // ログイン中だったら画面を変える
        val user = FirebaseAuth.getInstance().currentUser
        if (user != null) {
            startActivity(Intent(this, SignOutActivity::class.java))
            finish()
        }
    }

やるのはonResumeがいいです。というのも、ログインが成功して戻ってきたときに変えたいからです。
とはいえ、ログイン成功したらこのページは終了しても良いので、onActivityResultでfinishしてもいいかもしれませんね。

SignInActivity.kt
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        if (requestCode == REQUEST_CODE_AUTH) {
            ...
            if (resultCode == Activity.RESULT_OK) {
                Log.d("AUTH", "Auth Completed.")
                // Successfully signed in
                analyticsUtil.sendSignInEvent()
                finish()
             }

取り敢えず今は、ログイン→ログアウト→ログインが直ぐ出来るように、onActivityResultでのfinishはしないでおきます。

2.ログアウト画面

ログアウト画面はこんなデザインにしました。

qiita12_96.png

レイアウトxmlは自由に組んでみてください。

サンプルレイアウトxml
activity_signout.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

        <variable
                name="userData"
                type="jp.les.kasa.sample.mykotlinapp.data.LoginUserData" />
    </data>

    <androidx.coordinatorlayout.widget.CoordinatorLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context="jp.les.kasa.sample.mykotlinapp.activity.signin.SignOutActivity">

        <com.google.android.material.appbar.AppBarLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:theme="@style/AppTheme.AppBarOverlay">

            <androidx.appcompat.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" />

        </com.google.android.material.appbar.AppBarLayout>

        <ScrollView
                android:id="@+id/signOutScroll"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:padding="16dp"
                app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical">


                <TextView
                        android:id="@+id/labelSignIn"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_horizontal"
                        android:layout_marginTop="16dp"
                        android:text="@string/text_sign_in_now"
                        android:textSize="20dp" />

                <ImageView
                        android:id="@+id/imageCloudDone"
                        android:layout_width="100dp"
                        android:layout_height="100dp"
                        android:layout_gravity="center_horizontal"
                        android:src="@drawable/ic_cloud_done_24dp"
                        android:tint="@color/colorPrimary" />

                <TextView
                        android:id="@+id/textUserName"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_horizontal"
                        android:text="@{userData.userName}"
                        android:textSize="14dp"
                        tools:text="user_display_name" />

                <TextView
                        android:id="@+id/textUserEmail"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_horizontal"
                        android:text="@{userData.emailAddress}"
                        android:textSize="14dp"
                        tools:text="hoge@abc.xyz.com" />

                <TextView
                        android:id="@+id/descriptionSignIn"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="16dp"
                        android:text="@string/text_sign_out_description" />

                <Button
                        android:id="@+id/buttonSignOut"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_horizontal"
                        android:layout_marginTop="8dp"
                        android:text="@string/label_sign_out" />

                <Button
                        android:id="@+id/buttonConvert"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_horizontal"
                        android:layout_marginTop="8dp"
                        android:text="@string/label_convert_to_local" />

                <Button
                        android:id="@+id/buttonAccountDelete"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_gravity="center_horizontal"
                        android:layout_marginTop="8dp"
                        android:layout_marginBottom="8dp"
                        android:text="@string/label_account_delete"
                        android:textColor="@color/colorAccent" />
            </LinearLayout>
        </ScrollView>

    </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

(1)ユーザー情報の表示

ユーザー表示名と、メールアドレスを表示し、自分がどんなアカウントでログイン中なのか分かるようにします。

SignOutActivity.kt
    lateinit var binding: ActivitySignoutBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_signout)

        setSupportActionBar(toolbar)

        supportActionBar?.setDisplayHomeAsUpEnabled(true)

        val user = FirebaseAuth.getInstance().currentUser
        val userData = LoginUserData(user?.displayName ?: getString(R.string.label_you), user?.email ?: getString(R.string.label_no_email))

        binding.userData = userData
   }

Databindingを使っています。
displayNameemailnullableなので、nullだった場合にはデフォルトで"あなた"と"-"を表示するようにしています。

LogUserDataはこんな感じです。

LogUserData.kt
/**
 * ログイン中のユーザー情報を表示するための情報
 */
data class LoginUserData(val userName: String, val emailAddress: String)

(2)サインアウト

サインアウト処理は、以下のように行います。

SignOutActivity.kt
        // サインアウトボタン
        buttonSignOut.setOnClickListener {
            analyticsUtil.sendSignOutEvent()
            authUI.signOut(this)
                .addOnCompleteListener {
                    Log.d("AUTH", "User logout completed.")
                    // サインイン画面に戻る
                    startActivity(Intent(this, SignInActivity::class.java))
                    finish()
                }
        }

analyticsUtil.sendSignOutEventでサインアウト完了をアナリティクスに送っていますが、実は、ログインは定義済みイベントFirebaseAnalytics.Event.LOGINがあるのですが、ログアウトがありません。なのでカスタムイベントとして送っています。

AnalyticsUtil.kt
    /**
     * サインイン完了イベント送信
     */
    fun sendSignInEvent() {
        firebaseAnalytics.logEvent(FirebaseAnalytics.Event.LOGIN, null)
    }

    /**
     * サインアウト完了イベント送信
     */
    fun sendSignOutEvent() {
        firebaseAnalytics.logEvent("logout", null)
    }

(3)アカウント削除

アカウント削除も出来ます。

SignOutActivity.kt
    private fun doDeleteAccount() {
        authUI.delete(this)
            .addOnCompleteListener {
                Log.d("AUTH", "Account delete completed.")
                analyticsUtil.sendDeleteAccountEvent()
                // サインイン画面に戻る
                startActivity(Intent(this, SignInActivity::class.java))
                finish()
            }
    }

これをボタンのクリックイベントで直ぐに呼ぶのはやめておきましょう。アカウントの削除は致命的な動作なので、しつこいくらい確認ダイアログを出した方が良いでしょうね。
ってことで、このプロジェクトでは、

  • ローカルにデータ変換した?
    • [はい] 削除決行
  • ローカルにデータ変換してないとデータ復元できないよ?
    • [はい] 削除決行

みたいな流れにします。

ConfirmDialogは、以前作成して使ってなかったクラスですが、以下のようなものです。

ConfirmDialogクラス
ConfirmDialog.kt
/**
 * 確認メッセージを表示するダイアログ<br>
 *     [YES/NO]ボタンを表示します
 * 2019/08/30
 **/
class ConfirmDialog : DialogFragment(), DialogInterface.OnClickListener {

    interface ConfirmEventListener {
        /**
         * 確認ダイアログのコールバック<br>
         * @param which : AlertDialogの押されたボタン(POSITIVE or NEGATIVE)
         * @param bundle : data()でセットしたBundleデータ
         * @param requestCode : targetFragmentと併せて指定したrequestCode
         */
        fun onConfirmResult(which: Int, bundle: Bundle?, requestCode: Int)
    }

    private val analyticsUtil: AnalyticsUtil by inject()

    class Builder() {
        private var message: String? = null
        private var messageResId: Int = 0
        private var target: Fragment? = null
        private var requestCode: Int = 0
        private var data: Bundle? = null

        fun message(message: String): Builder {
            this.message = message
            return this
        }

        fun message(resId: Int): Builder {
            this.messageResId = resId
            return this
        }

        fun target(fragment: Fragment): Builder {
            this.target = fragment
            return this
        }

        /**
         * only for targetFragment
         */
        fun requestCode(requestCode: Int): Builder {
            this.requestCode = requestCode
            return this
        }

        fun data(bundle: Bundle): Builder {
            this.data = bundle
            return this
        }

        fun create(): ConfirmDialog {
            val d = ConfirmDialog()
            d.arguments = Bundle().apply {
                if (message != null) {
                    putString(KEY_MESSAGE, message)
                } else {
                    putInt(KEY_RESOURCE_ID, messageResId)
                }
                if (data != null) {
                    putBundle(KEY_DATA, data)
                }
            }
            if (target != null) {
                d.setTargetFragment(target, requestCode)
            }
            return d
        }
    }

    companion object {
        const val KEY_MESSAGE = "message"
        const val KEY_RESOURCE_ID = "res_id"
        const val KEY_DATA = "data"
        const val SCREEN_NAME = "確認ダイアログ"
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        // AlertDialogで作成する
        val builder = AlertDialog.Builder(requireContext())

        // メッセージの決定
        val message =
            when {
                arguments!!.containsKey(KEY_MESSAGE) -> arguments!!.getString(KEY_MESSAGE)
                else -> requireContext().getString(
                    arguments!!.getInt(KEY_RESOURCE_ID)
                )
            }
        // AlertDialogのセットアップ
        builder.setMessage(message)
            .setTitle(R.string.confirm)
            .setIcon(android.R.drawable.ic_dialog_info)
            .setNegativeButton(R.string.label_no, this)
            .setPositiveButton(R.string.label_yes, this)
        return builder.create()
    }

    override fun onResume() {
        super.onResume()
        activity?.let { analyticsUtil.sendScreenName(it, SCREEN_NAME) }
    }

    override fun onClick(dialog: DialogInterface?, which: Int) {
        FirebaseCrashlytics.getInstance().log("ConfirmDialog selected:$which")
        val data = arguments!!.getBundle(KEY_DATA)
        if (targetFragment is ConfirmEventListener) {
            val listener = targetFragment as ConfirmEventListener
            listener.onConfirmResult(which, data, targetRequestCode)
            return
        } else if (activity is ConfirmEventListener) {
            val listener = activity as ConfirmEventListener
            listener.onConfirmResult(which, data, targetRequestCode)
            return
        }
        Log.e(
            "ConfirmDialog",
            "Target Fragment or Activity should implement ConfirmEventListener!!"
        )
    }

SignOutActivityConfirmDialog.ConfirmEventListenerを実装させ、

SignOutActivity.kt
class SignOutActivity : BaseActivity(), ConfirmDialog.ConfirmEventListener {

onConfirmResultを実装します。

SignOutActivity.kt
    override fun onConfirmResult(which: Int, bundle: Bundle?, requestCode: Int) {
        when (bundle?.get("tag")) {
            TAG_CONFIRM_1 -> {
                if (which == DialogInterface.BUTTON_POSITIVE) {
                    // データ削除した、なので削除決行
                    doDeleteAccount()
                } else {
                    // データ削除しなくてよいかもう一度確認
                    val dialog = ConfirmDialog.Builder().data(Bundle().apply {
                        putString("tag", TAG_CONFIRM_2)
                    })
                        .message(R.string.confirm_account_delete_2)
                        .create()
                    dialog.show(supportFragmentManager, TAG_CONFIRM_2)
                }
            }
            TAG_CONFIRM_2 -> {
                if (which == DialogInterface.BUTTON_POSITIVE) {
                    // アカウント削除してよい、なので削除決行
                    doDeleteAccount()
                } else {
                    // いいえなので何もしない
                }
            }
        }
    }

最後に、アカウント削除ボタンのイベントリスナーの処理を書きます。

SignOutActivity.kt
        // アカウント削除ボタン
        buttonAccountDelete.setOnClickListener {
            analyticsUtil.sendButtonEvent("delete_account")
            // 確認フローを開始する
            val dialog = ConfirmDialog.Builder().data(Bundle().apply {
                putString("tag", TAG_CONFIRM_1)
            })
                .message(R.string.confirm_account_delete_1)
                .create()
            dialog.show(supportFragmentManager, TAG_CONFIRM_1)
        }

「ローカルデータに変更する」ボタンは、Firestoreのデータ形式をRoomのデータに戻すことを考えています。
Firestoreのデータは、実はNonSQLなので、SQLiteなRoomのデータ構造とは異なってきます。なのでコンバートが必要なんですね。

ただ、実はFirebase Authenticationには匿名ログインというのもあります。これを使って、匿名でFirestoreを使って貰うことは可能だと思いますので、取り入れてみるのも良いかも知れません。そうすると、Room用のデータ変換なども一切不要になります。

この記事用のプロジェクトでは、Roomのコードも残しておきたいのとFirestoreへの課金が怖いので(笑)、ローカルで使える方法も残しておきます。

データのコンバート方法については、Firestoreに対応してから入れますので、いまは空っぽです。

SignOutActivity.kt
        // ローカルデータに変更ボタン
        buttonConvert.setOnClickListener {
            analyticsUtil.sendButtonEvent("convert_to_local_data")
            // TODO FirestoreのデータをRoomに入れる
        }

以上で、サインインとサインアウトが出来るようになりました。
今は特に意味が無いですが、Firestoreなどを使っていくと意義が出てくると思います。

アプリのリリースをする前に必要な準備

アプリを正式にリリースする際には、ちょっとした注意点があるので気をつけて下さい。

  • Firabseコンソールのリリースビルド用情報への書き変え

    • パッケージ名や証明書のSHA1情報をリリースビルド用のものに置き換える必要があります。
    • リリースにGoogle Playアプリ署名を使う場合、自分で作成した証明書の情報では無く、実際に署名に使われた証明書のSHA1が必要になります。Fiebaseコンソールのプロジェクト設定にセットした署名情報をそちらで置き換える必要があります。Google Playアプリ署名で使われた証明書の情報は、各種フィンガープリントがGoogle Playコンソールで確認できます。(一度apkをアップロードすることが必要です。クローズドアルファ版のリリースの作成などを利用しましょう。)
  • Facebookログインがある場合

    • Firebaseコンソールへの登録と同様、パッケージ名、証明書のSHA1ハッシュの書き換えが必要です。
    • Facebookへの審査提出が必要となります。Facebook側が、こちらのアプリが不要な個人情報を取得していないか、レビューします。そのため、こちらの承認が通るまで、アプリのリリースは出来ません。なお、2020/05/16現在、新型コロナ対応のため個人開発者の承認を止めているとアナウンスされています。個人で上げようと考えている方は、しばらく待つか、Facebookログインは後日対応にするなどの対策が必要そうです。

テスト

サインインとサインアウトはさすがに自動テストするのは怖いので、今回作ったレイアウトが正しいか程度のテストにしておきます。
ただ、気をつけないと行けないのは、FirebaseAuth.getInstance().currentUserが返す値です。
これにより画面が自動で遷移してしまうので、テスト中にはこの値に左右されないようにする必要があります。

ということで、DI出来るようにして、Koinモジュールを差し替えるという以前やったのと同じことをやります。

1.DI出来るようにする

FirebaseAuth.getInstance().currentUserをDIできるようにしていきます。

(1)FirebaseAuthのインスタンスを得るためのクラスを作る

AuthProviderクラスをこんな感じで作りました。

AuthProvider.kt
import android.app.Application
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import jp.les.kasa.sample.mykotlinapp.R
import jp.les.kasa.sample.mykotlinapp.data.LoginUserData


/**
 * FirebaseAuthのInstance取得を提供するプロバイダ用の抽象クラス
 */
abstract class AuthProviderI(app: Application) {
    abstract val user: FirebaseUser?

    val defaultUserName: String by lazy { app.getString(R.string.label_you) }
    val defaultEmail: String by lazy { app.getString(R.string.label_no_email) }

    abstract val userData: LoginUserData
}


/**
 * FirebaseAuthのInstance取得を提供するプロバイダ用のアプリで実際に使うクラス
 */
class AuthProvider(app: Application) : AuthProviderI(app) {
    override val user: FirebaseUser?
        get() {
            return FirebaseAuth.getInstance().currentUser
        }

    override val userData: LoginUserData
        get() {
            return LoginUserData(
                user?.displayName ?: defaultUserName,
                user?.email ?: defaultEmail
            )
        }
}

override val user = FirebaseAuth.getInstance().currentUserとすると、最初に代入された値のままになるのでやっちゃダメですよ:grin:

これをKoinモジュールに登録します。

// FirebaseService
val firebaseModule = module {
    single { AnalyticsUtil(androidApplication()) }
    single { AuthProvider(androidApplication()) as AuthProviderI } // 追加
}

FirebaseAuth.getInstance()していたところを、AuthProviderIを使うように変更します。

SignInActivity.kt
    private val authProvider: AuthProviderI by inject()

    ...

    override fun onResume() {
        super.onResume()
        // ログイン中だったら画面を変える
//        val user = FirebaseAuth.getInstance().currentUser
//        if (user != null) {
        if (authProvider.user != null) {
            startActivity(Intent(this, SignOutActivity::class.java))
            finish()
        }
    }

SignOutActivityLoginUserDataを作っていましたね。それをAuthProviderのプロパティアクセスに変更します。

SignOutActivity.kt
        binding.userData = authProvider.userData

変更漏れが無いかどうかは、FirebaseAuth.getInstance()でプロジェクト内検索をすると良いです。
ショートカットはMacだとShift+Command+fかな?

(2)テスト用のKoinモジュールの変更

テスト用のモック化モジュールでAuthProviderを変更します。
なお、Mockitoが使えればもっと簡単にできるのですが、どうやっても上手くいかなかったので、苦肉の策になっています。

MockModules.kt
// FirebaseAuthを提供するプロバイダのテスト用
class TestAuthProvider(app: Application) : AuthProviderI(app) {
    override val user: FirebaseUser?
        get() {
            return if (mockFirebaseUser) MockFirebaseUser()
            else null
        }

    override val userData: LoginUserData
        get() {
            return LoginUserData(
                "ユーザー名",
                "foo@bar.com"
            )
        }

    var mockFirebaseUser = false
}

// FirebaseUserモック
class MockFirebaseUser : FirebaseUser() {
    override fun zzg(): String {
        TODO("Not yet implemented")
    }

    override fun zze(): zzff {
        TODO("Not yet implemented")
    }

    override fun getEmail(): String? {
        TODO("Not yet implemented")
    }

    override fun zzc(): FirebaseApp {
        TODO("Not yet implemented")
    }

    override fun zza(): MutableList<String> {
        TODO("Not yet implemented")
    }

    override fun zza(p0: MutableList<out UserInfo>): FirebaseUser {
        TODO("Not yet implemented")
    }

    override fun zza(p0: zzff) {
        TODO("Not yet implemented")
    }

    override fun getProviderData(): MutableList<out UserInfo> {
        TODO("Not yet implemented")
    }

    override fun writeToParcel(p0: Parcel?, p1: Int) {
        TODO("Not yet implemented")
    }

    override fun getMetadata(): FirebaseUserMetadata? {
        TODO("Not yet implemented")
    }

    override fun getMultiFactor(): MultiFactor {
        TODO("Not yet implemented")
    }

    override fun isAnonymous(): Boolean {
        TODO("Not yet implemented")
    }

    override fun getPhoneNumber(): String? {
        TODO("Not yet implemented")
    }

    override fun getUid(): String {
        TODO("Not yet implemented")
    }

    override fun isEmailVerified(): Boolean {
        TODO("Not yet implemented")
    }

    override fun zzf(): String {
        TODO("Not yet implemented")
    }

    override fun zzd(): String? {
        TODO("Not yet implemented")
    }

    override fun zzb(): FirebaseUser {
        TODO("Not yet implemented")
    }

    override fun zzb(p0: MutableList<MultiFactorInfo>?) {
        TODO("Not yet implemented")
    }

    override fun getDisplayName(): String? {
        TODO("Not yet implemented")
    }

    override fun getPhotoUrl(): Uri? {
        TODO("Not yet implemented")
    }

    override fun getProviderId(): String {
        TODO("Not yet implemented")
    }
}

外からmockFirebaseUserを無理矢理変えられるようにして、userをnullか実体を返すか処理を分けています。

これをtestMockModuleに追加します。

MockModules.kt
// テスト用にモックするモジュール
val testMockModule = module {
   ....

    single(override = true) {
        TestAuthProvider(androidApplication()) as AuthProviderI
    }
}

これで準備は完了しました。

2.UnitTest(androidTest)

今回から、Robolectric版を試すのは少し時間がかかってしんどいので、割愛します。すみません。気が向いたらGithubのリポジトリにはアップしておきます・・・

Robolectric版を書いてみる方は、今回の内容では、ダイアログ周りのテストがandroidTest版とRobolectric版では異なってきますので、その辺りに注意してみてください。

(1)SignInActivityのテスト

まずは初期化までのコード。
とくに真新しいところはない・・・はずですね。

SignInActivityTest.kt
import android.app.Instrumentation
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import jp.les.kasa.sample.mykotlinapp.R
import jp.les.kasa.sample.mykotlinapp.di.TestAuthProvider
import jp.les.kasa.sample.mykotlinapp.di.testMockModule
import jp.les.kasa.sample.mykotlinapp.utils.AuthProviderI
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.context.loadKoinModules
import org.koin.core.inject
import org.koin.test.AutoCloseKoinTest

@RunWith(AndroidJUnit4::class)
class SignInActivityTest : AutoCloseKoinTest() {
    @get:Rule
    val activityRule = ActivityTestRule(SignInActivity::class.java, false, false)

    lateinit var activity: SignInActivity

    private val authProvider: AuthProviderI by inject()

    @Before
    fun setUp() {
        loadKoinModules(testMockModule)
    }
}

通常起動時のレイアウトのテストです。

SignInActivityTest.kt
    @Test
    fun layout() {
        activity = activityRule.launchActivity(null)

        // クラウドアイコン
        onView(withId(R.id.imageCloudUpload))
//            .check(matches(withDrawable(R.drawable.ic_cloud_upload_24dp))) // Tintカラー付けていると使えない
            .check(matches(ViewMatchers.isDisplayed()))
        // 文言
        onView(withText(R.string.text_sign_in_description))
            .check(matches(ViewMatchers.isDisplayed()))
        // ログインボタン
        onView(withText(R.string.label_sign_in))
            .check(matches(ViewMatchers.isDisplayed()))
    }

コメントにもありますが、withDrawableがTintカラーを付けているとfalseになってしまうため、そのチェックをやむを得ず外しています。これも試行錯誤したのですが、解決できませんでした。

ログイン済みだった時用の、SignOutActivityが起動しているかのテストはこうなります。

SignInActivityTest.kt
    /**
     *   ログイン中の場合にサインアウト画面がでるかのテスト
     */
    @Test
    fun moveToSignOut() {
        // モックを作成
        (authProvider as TestAuthProvider).mockFirebaseUser = true
        // ResultActivityの起動を監視
        val monitor = Instrumentation.ActivityMonitor(
            SignOutActivity::class.java.canonicalName, null, false
        )
        InstrumentationRegistry.getInstrumentation().addMonitor(monitor)

        activity = activityRule.launchActivity(null)

        assertThat(activity.isFinishing).isTrue()

        // ResultActivityが起動したか確認
        InstrumentationRegistry.getInstrumentation().waitForMonitorWithTimeout(monitor, 1000L)
        assertThat(monitor.hits).isEqualTo(1)
    }

それとエラー表示のテストですね。

SignInActivityTest.kt
   @Test
    fun showError_EMAIL_MISMATCH_ERROR() {
        activity = activityRule.launchActivity(null)
        activity.showError(ErrorCodes.EMAIL_MISMATCH_ERROR)
        InstrumentationRegistry.getInstrumentation().waitForIdleSync()

        onView(withText(startsWith(getString(R.string.error_email_mismacth))))
            .check(matches(isDisplayed()))
        onView(
            withText(
                endsWith(
                    getString(
                        R.string.label_error_code,
                        ErrorCodes.EMAIL_MISMATCH_ERROR
                    )
                )
            )
        )
            .check(matches(isDisplayed()))
        onView(withText(R.string.close))
            .check(matches(isDisplayed()))
            .perform(click())

        onView(withText(startsWith(getString(R.string.error_email_mismacth))))
            .check(doesNotExist())
    }

FirebaseUIを起動したくないので、resultDataを指定してActivity起動の戻りをモックする方法は使いません。そのためにshowErrorを関数に出していました^^;
後はエラーコード毎にチェックする関数を増やせば良いですね。
JUnit5のParameterizedTestが出来ると楽なんですが・・・

(2)SignOutActivityのテスト

同じように、初期起動時のレイアウトのチェックと、各ボタンを押したときのダイアログのチェックなどを入れていけば良いですね。
ちょっと自力で頑張って書いてみて下さい。

サンプルコードはこちらから。
SignOutActivityTest.kt
package jp.les.kasa.sample.mykotlinapp.activity.signin

import android.app.Instrumentation
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.*
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import jp.les.kasa.sample.mykotlinapp.R
import jp.les.kasa.sample.mykotlinapp.di.TestAuthProvider
import jp.les.kasa.sample.mykotlinapp.di.testMockModule
import jp.les.kasa.sample.mykotlinapp.utils.AuthProviderI
import org.assertj.core.api.Assertions
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.context.loadKoinModules
import org.koin.core.inject
import org.koin.test.AutoCloseKoinTest

@RunWith(AndroidJUnit4::class)
class SignOutActivityTest : AutoCloseKoinTest() {
    @get:Rule
    val activityRule = ActivityTestRule(SignOutActivity::class.java, false, false)

    lateinit var activity: SignOutActivity

    private val authProvider: AuthProviderI by inject()

    @Before
    fun setUp() {
        loadKoinModules(testMockModule)
    }

    /**
     *   起動直後の表示のテスト<br>
     */
    @Test
    fun layout() {
        activity = activityRule.launchActivity(null)

        // サインイン中
        onView(withText(R.string.text_sign_in_now))
            .check(matches(isDisplayed()))
        // クラウドアイコン
        onView(withId(R.id.imageCloudDone))
//            .check(matches(withDrawable(R.drawable.ic_cloud_upload_24dp))) // Tintカラー付けていると使えない
            .check(matches(isDisplayed()))
        // ユーザー名
        onView(withText("ユーザー名")).check(matches(isDisplayed()))
        // メールアドレス
        onView(withText("foo@bar.com")).check(matches(isDisplayed()))
        // 文言
        onView(withText(R.string.text_sign_out_description))
            .check(matches(isDisplayed()))
        // ログアウトボタン
        onView(withText(R.string.label_sign_out))
            .check(matches(isDisplayed()))
        // ローカルデータ変換ボタン
        onView(withText(R.string.label_convert_to_local))
            .check(matches(isDisplayed()))
        // アカウント削除ボタン
        onView(withText(R.string.label_account_delete))
            .check(matches(isDisplayed()))
    }

    @Test
    fun signOut() {
        // モックを作成
        (authProvider as TestAuthProvider).mockFirebaseUser = true

        activity = activityRule.launchActivity(null)

        // ResultActivityの起動を監視
        val monitor = Instrumentation.ActivityMonitor(
            SignInActivity::class.java.canonicalName, null, false
        )
        InstrumentationRegistry.getInstrumentation().addMonitor(monitor)

        // ログアウトボタン
        onView(withText(R.string.label_sign_out))
            .perform(click())

        Assertions.assertThat(activity.isFinishing).isTrue()

        // ResultActivityが起動したか確認
        InstrumentationRegistry.getInstrumentation().waitForMonitorWithTimeout(monitor, 1000L)
        Assertions.assertThat(monitor.hits).isEqualTo(1)
    }

    @Test
    fun deleteAccount_cancel() {
        activity = activityRule.launchActivity(null)

        onView(withId(R.id.signOutScroll)).perform(swipeUp())
        // アカウント削除ボタン
        onView(withId(R.id.buttonAccountDelete))
            .perform(scrollTo(), click())

        onView(withText(R.string.confirm_account_delete_1))
            .check(matches(isDisplayed()))

        onView(withText(R.string.label_no))
            .check(matches(isDisplayed()))
            .perform(click())

        onView(withText(R.string.confirm_account_delete_1))
            .check(doesNotExist())

        onView(withText(R.string.confirm_account_delete_2))
            .check(matches(isDisplayed()))

        onView(withText(R.string.label_no))
            .check(matches(isDisplayed()))
            .perform(click())
    }
}

3.CIでのテスト

Github Actionsで、なぜかSignOutActivitysignOutテストが必ず失敗しました。手元のローカルマシンでは必ず成功するのにです。
サインアウトボタンを押すと、実際のauthUI.signOutが呼ばれているので、これかなあと思い、AuthUIもDIしてみました。

AuthProvider.kt
/**
 * FirebaseAuthのInstance取得を提供するプロバイダ用の抽象クラス
 */
abstract class AuthProviderI(app: Application) {
    ....
    abstract fun createSignInIntent(context: Context): Intent
    abstract fun signOut(context: Context): Task<Void?>
    abstract fun delete(context: Context): Task<Void?>
}

/**
 * FirebaseAuthのInstance取得を提供するプロバイダ用のアプリで実際に使うクラス
 */
class AuthProvider(app: Application) : AuthProviderI(app) {
    ....

    private val authUI = AuthUI.getInstance()

    override fun createSignInIntent(context: Context): Intent {
        // Choose authentication providers
        val providers = arrayListOf(
            AuthUI.IdpConfig.EmailBuilder().build(),
            AuthUI.IdpConfig.GoogleBuilder().build(),
            AuthUI.IdpConfig.FacebookBuilder().setPermissions(listOf("email")).build(),
            AuthUI.IdpConfig.TwitterBuilder().build(),
            AuthUI.IdpConfig.GitHubBuilder().build()
        )

        return authUI.createSignInIntentBuilder()
            .setAvailableProviders(providers)
            .setLogo(R.mipmap.ic_launcher)
            .setTheme(R.style.SinUpTheme)
            .setTosAndPrivacyPolicyUrls(
                "https://qiitapedometersample.web.app/policy.html",
                "https://qiitapedometersample.web.app/policy.html"
            )
            .build()
    }

    override fun signOut(context: Context): Task<Void?> {
        return authUI.signOut(context)
    }

    override fun delete(context: Context): Task<Void?> {
        return authUI.delete(context)
    }
}

テスト用のモックはこうしました。

MockModules.kt
// FirebaseAuthを提供するプロバイダのテスト用
class TestAuthProvider(app: Application) : AuthProviderI(app) {
    ...

    override fun createSignInIntent(context: Context): Intent {
        return Intent(context, MockAuthUIActivity::class.java)
    }

    override fun signOut(context: Context): Task<Void?> {
        return Tasks.forResult(null)
    }

    override fun delete(context: Context): Task<Void?> {
        return Tasks.forResult(null)
    }
}

class MockAuthUIActivity : AppCompatActivity()

debugビルドでMockAuthUIActivityが起動できるように、app/src/debugにマニフェストファイルを作って置きました。

app/src/debug/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="jp.les.kasa.sample.mykotlinapp">

    <application>

        <activity android:name="jp.les.kasa.sample.mykotlinapp.di.MockAuthUIActivity" />
    </application>

</manifest>

MockAuthUIActivityapp/src/debug/java/...下にないので、このファイルをAndroid Studioで開くと赤くなってしまいますが、無視して大丈夫です。

SignInActivityでサインインボタンを押したときに、このモック画面が開くかのテストを一応追加しました。

SignInActivityTest.kt
    @Test
    fun signIn() {
        activity = activityRule.launchActivity(null)

        // ResultActivityの起動を監視
        val monitor = Instrumentation.ActivityMonitor(
            MockAuthUIActivity::class.java.canonicalName, null, false
        )
        InstrumentationRegistry.getInstrumentation().addMonitor(monitor)

        onView(withText(R.string.label_sign_in)).perform(click())

        // ResultActivityが起動したか確認
        InstrumentationRegistry.getInstrumentation().waitForMonitorWithTimeout(monitor, 1000L)
        assertThat(monitor.hits).isEqualTo(1)
    }

あとは、AuthUI.getInstance()していたところをauthProvider経由に変えれば、ビルドが通過し、アプリが動作するはずです。

で、Github Actionsの結果は・・・

通るようになりました\(^_^)/
やはりAuthuUIの呼び出しが悪さをしていたようです。
(MainActivityTestIも時々失敗しますが、これはまあ目を瞑ります。通ることもあるので・・・)

ところで、Github Actionsは最近タイムアウトすることがあるので(みんな使いすぎ?)、privateリポジトリで使っている方はあっという間に無料枠を使い切ります(返してくれないんですねーT_T)。ご注意を〜
(publicなリポジトリは無制限です。Flutterのプロジェクトがprivateなのですが、やらかしてくれましたorz)

なんにも設定していないと、360分タイムアウトまで待つらしいので、次のように設定しておくと良いみたいです。

github/workflow/android.yaml
jobs:

  build:
    runs-on: macOS-latest
    timeout-minutes: 30

まとめ

エミュレーターでの挙動がおかしくて、かなり悩んで時間を費やしてしまいました。
素直に実機でテストすれば良かったです。エミュレーターで出来ないとなると、Facebookアプリをインストールしていない端末を用意するのが少し面倒かも知れませんね。(プリインされていてアンインストールできない端末が多い)

ここまでのプロジェクトコードは、以下のリポジトリにアップしてあります。
https://github.com/le-kamba/qiita_pedometer/tree/feature/qiita_12

予告

いよいよ、Firestoreを使ってクラウド保存するのをやってみます。

参考サイトなど

Facebookログインの必要な設定について参考にしました。
https://www.techotopia.com/index.php/Facebook_Login_Authentication_using_FirebaseUI_Auth

言語リソースの特定方法について参考にしました。
https://github.com/firebase/FirebaseUI-Android/blob/master/auth/README.md#configuration

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2