20200421のAndroidに関する記事は12件です。

[LiveData] 更新されたときだけ値を受けとる

二通りの方法を示します。

1. LiveData を BroadcastChannel に変換する

いきなりテストです。

class MyTest {
    private val dispatcher = TestCoroutineDispatcher()

    @[Rule JvmField]
    val rule = InstantTaskExecutorRule()

    @Before
    fun before() {
        Dispatchers.setMain(dispatcher)
    }

    @After
    fun after() {
        Dispatchers.resetMain()
        dispatcher.cleanupTestCoroutines()
    }

    @Test
    fun test() = dispatcher.runBlockingTest {
        // ソースとなる LiveData
        val source = object : MutableLiveData<Int>(0) {
            override fun onActive() = println("onActive")
            override fun onInactive() = println("onInactive")
        }

        // BroadcastChannel に変換
        val broadcastChannel = source.asFlow().broadcastIn(this)

        // Flow に変換 (都度 broadcastChannel.openSubscription() しても良い)
        val flow = broadcastChannel.asFlow()

        val job1 = launch { flow.collect { println("coroutine1: $it") } }

        source.value = 1

        job1.cancel()

        source.value = 2

        val job2 = launch { flow.collect { println("coroutine2: $it") } }

        source.value = 3

        job2.cancel()

        broadcastChannel.cancel()
    }
}

実行結果は以下です。

onActive
coroutine1: 0
coroutine1: 1
coroutine2: 3
onInactive

coroutine1 では初期値 0 および 1 を受信していますが、coroutine2 では 3 だけを受信しており、直前の値 2 を受信していないことがわかります。

利点

  • 特別な仕組みを作りこむ必要がない

欠点

  • おまじないっぽい?
  • 多くの関数が @ExperimentalCoroutinesApi だったり @FlowPreview だったりする

2. LiveData.getVersion() を使う

androidx.lifecycle パッケージ配下に以下の関数を用意します。

package androidx.lifecycle

internal fun LiveData<*>.version() = version

次に、任意のパッケージ配下に以下の関数を用意します。

@MainThread
fun <T> LiveData<T>.observeChangesForever(action: (T) -> Unit): Observer<T> =
    object : Observer<T> {
        private val version = version()
        override fun onChanged(t: T) {
            if (version() > version) action(t)
        }
    }.also { observeForever(it) }

省略していますが、LifecycleOwner を受け取る関数も同じように作ることができます。

テストは以下です。

class MyTest {
    @[Rule JvmField]
    val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()

    @Test
    fun test() {
        // ソースとなる LiveData
        val source = object : MutableLiveData<Int>(0) {
            override fun onActive() = println("onActive")
            override fun onInactive() = println("onInactive")
        }

        val observer1 = source.observeChangesForever { println("observer1: $it") }

        source.value = 1

        source.removeObserver(observer1)

        source.value = 2

        val observer2 = source.observeChangesForever { println("observer2: $it") }

        source.value = 3

        source.removeObserver(observer2)
    }
}

実行結果は以下です。

onActive
observer1: 1
onInactive
onActive
observer2: 3
onInactive

observer1observer2 共に直前の値 (0 および 2) を受信していないことがわかります。

利点

  • わかりやすい (かも)

欠点

  • 作りこみが必要
  • LiveData.getValue() はパッケージプライベートなので、今後動かなくなるかも

二通りの方法を示しましたが両者は挙動が異なるので、使い方には注意が必要です。

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[SavedStateHandle] やっぱりDaggerとうまく付き合う

以前 SavedStateHandle を DI する方法について記事を書きましたが、今は正直以下の方法でも良いのではないかと思っています。

class MyViewModel(handle: SavedStateHandle) : ViewModel() {
    @Inject
    lateinit var myRepository: MyRepository

    val myData: LiveData<MyData> = liveData {
        emitSource(myRepository.getMyData())
    }
}
class MyFragment : Fragment() {
    private val viewModel by viewModels<MyViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        (requireActivity().applicationContext as App)
            .component
            .inject(viewModel)
    }
}

SavedStateHandle はデフォルトの ViewModelFactory におまかせして、それ以外のコンポーネントをフィールドインジェクションでセットする方法です。

テストはしていません。
以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

targetSdkVersion を 29 に上げたら Robolectric のテストが動かなくなる

概要

Java 8 の環境で targetSdkVersion を 29 に上げたら RobolectricTestRunner を使うユニットテストが動かなくなったので、対応方法を調べました。

問題

テストを実行すると、以下の例外が出てテストが失敗します。

java.lang.IllegalArgumentException: failed to configure jp.toastkid.yobidashi.media.image.preview.RotateMatrixFactoryTest.test: Package targetSdkVersion=29 > maxSdkVersion=28
    at org.robolectric.RobolectricTestRunner.getChildren(RobolectricTestRunner.java:240)
    at org.junit.runners.ParentRunner.getFilteredChildren(ParentRunner.java:426)

// 中略

    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.IllegalArgumentException: Package targetSdkVersion=29 > maxSdkVersion=28
    at org.robolectric.plugins.DefaultSdkPicker.configuredSdks(DefaultSdkPicker.java:118)
    at org.robolectric.plugins.DefaultSdkPicker.selectSdks(DefaultSdkPicker.java:69)
    at org.robolectric.RobolectricTestRunner.getChildren(RobolectricTestRunner.java:206)

対応

現時点の最新バージョンの 4.3.1 を使えば SDK 29に対応はしているようですが、Java 9 以上が必要です。

java.lang.UnsupportedOperationException: Failed to create a Robolectric sandbox: Android SDK 29 requires Java 9 (have Java 8)

    at org.robolectric.RobolectricTestRunner.getSandbox(RobolectricTestRunner.java:265)
    at org.robolectric.RobolectricTestRunner.getSandbox(RobolectricTestRunner.java:63)
    at org.robolectric.internal.SandboxTestRunner$2.evaluate(SandboxTestRunner.java:215)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)

Workaround

諸事情で Java 8 を使わざるを得ない場合は、 src/test/resources/robolecric.properties というファイルを置き、以下の設定を入れることで動くようになりました。Java 9 を導入するまではこれで対応しましょう。

robolecric.properties
sdk=28

今後、sdk 28を指定していたことでテストに不具合が起こる可能性も否定はできないので、早めに Java と Robolectric をアップグレードする方が良さそうです。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

targetSdkVersion を 29 に上げても Robolectric のテストを動くようにする

概要

Java 8 の環境で targetSdkVersion を 29 に上げたら RobolectricTestRunner を使うユニットテストが動かなくなったので、対応方法を調べました。

問題

テストを実行すると、以下の例外が出てテストが失敗します。

java.lang.IllegalArgumentException: failed to configure jp.toastkid.yobidashi.media.image.preview.RotateMatrixFactoryTest.test: Package targetSdkVersion=29 > maxSdkVersion=28
    at org.robolectric.RobolectricTestRunner.getChildren(RobolectricTestRunner.java:240)
    at org.junit.runners.ParentRunner.getFilteredChildren(ParentRunner.java:426)

// 中略

    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: java.lang.IllegalArgumentException: Package targetSdkVersion=29 > maxSdkVersion=28
    at org.robolectric.plugins.DefaultSdkPicker.configuredSdks(DefaultSdkPicker.java:118)
    at org.robolectric.plugins.DefaultSdkPicker.selectSdks(DefaultSdkPicker.java:69)
    at org.robolectric.RobolectricTestRunner.getChildren(RobolectricTestRunner.java:206)

対応

現時点の最新バージョンの 4.3.1 を使えば SDK 29に対応はしているようですが、Java 9 以上が必要です。

java.lang.UnsupportedOperationException: Failed to create a Robolectric sandbox: Android SDK 29 requires Java 9 (have Java 8)

    at org.robolectric.RobolectricTestRunner.getSandbox(RobolectricTestRunner.java:265)
    at org.robolectric.RobolectricTestRunner.getSandbox(RobolectricTestRunner.java:63)
    at org.robolectric.internal.SandboxTestRunner$2.evaluate(SandboxTestRunner.java:215)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)

Workaround

諸事情で Java 8 を使わざるを得ない場合は、 src/test/resources/robolecric.properties というファイルを置き、以下の設定を入れることで動くようになりました。Java 9 を導入するまではこれで対応しましょう。

robolecric.properties
sdk=28

今後、sdk 28を指定していたことでテストに不具合が起こる可能性も否定はできないので、早めに Java と Robolectric をアップグレードする方が良さそうです。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google認証で ApiException:10 が発生

Google認証をやっていたら、下記の例外が発生

com.google.android.gms.common.api.ApiException: 10:

下記で解決できるらしい。

■参考サイト
https://medium.com/@riscait/apiexception-10-error-in-sign-in-with-google-using-firebase-auth-in-flutter-1be6a44a2086
https://stackoverflow.com/questions/47437678/why-do-i-get-com-google-android-gms-common-api-apiexception-10

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android studioのemulatorでインターネットに繋がらない現象を解決する

Android studioでemulatorを起動し外部APIへGETリクエストを実行しようとするもレスポンスが取得できずなんでだろうと思っていたらそもそもインターネットにできない状態でした.

環境

端末: Nexus5X
API version: 28

現象

cannot_reached.png

対応

ホスト側のDNSを8.8.8.8(GoogleのDNS)に変更後,emulatorを再起動することで解決
dns_setting.png

再起動後
reached.png

参考

Android emulator not able to access the internet
Android 9(Pie)でHTTP通信を有効にする

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]GoogleMap上ランドマークのストリートビュー表示

GoogleMap上ランドマークのストリートビュー表示

以下の手順となります。
・ランドマークからPlaceIDを使ってplace/details API 実行して情報取得
・imageViewに表示するためライブラリ導入
・ストリートビュー表示する

ランドマークからPlaceIDを使ってplace/details API 実行して情報取得

https://qiita.com/myatthinkyu/items/efc901d88a5f9d031c4f

imageViewに表示するためライブラリ導入

implementation "com.github.bumptech.glide:glide:4.8.0"

ストリートビュー表示する

buildingInfo.firstOrNull()?.let {
                var location = it.result!!.geometry!!.location
                var locationlatlng = location!!.lat.toString()+"," + location!!.lng.toString()

                val imageUri = Uri.Builder().apply {
                    scheme("https")
                    path("maps.googleapis.com/maps/api/streetview")
                    appendQueryParameter("size", "400x300")
                    appendQueryParameter("location", locationlatlng)
                    appendQueryParameter("key", BuildConfig.GOOGLE_API_KEY)
                }

                var imageview = findViewById<ImageView>(R.id.street_view_image)
                Glide.with(this).load(imageUri.toString()).into(imageview)

            }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

同一のRadioGroupに入っているRadioButtonを画面上の自由な位置に配置したい

概要

Androidの標準APIで提供されているRadioButtonRadioGroup

複数のRadioButtonを1つのRadioGroupの中に入れ子にすれば、その中で同時に一つだけが選択可能になる、とここまではいいのだが、RadioButtonは自由に配置することが許されない。

なんでRadioButtonは自由に動かせない?

ドキュメントでもなんでも見ればわかるのだが、android.widget.RadioGroupandroid.widget.LinearLayoutを継承しているため。

LinearLayoutが何だかはみんな知ってるでしょう。
つまり横1列か縦1列にしか並べられないということ。

なんでやねん!!

解決策

ということで、作っちゃいました。そういうクラス。

FreeRadioGroup.java
import android.util.Log;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.RadioButton;
import android.widget.RadioGroup;

import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;

public class FreeRadioGroup {

    private List<RadioButton> registeredRadioButtons = new ArrayList<>();

    public FreeRadioGroup(){

    }

    public void addChild(RadioButton rb){
        if(registeredRadioButtons.contains(rb)){
            Log.w("FreeRadioGroup", "RadioButton already registered. Abort registering");
        }else {
            registeredRadioButtons.add(rb);
            rb.setOnCheckedChangeListener(OCL);
        }
    }

    public void removeChild(RadioButton rb){
        if(!registeredRadioButtons.contains(rb)){
            Log.w("FreeRadioGroup", "RadioButton is not registered. Abort unregistering");
        }else {
            registeredRadioButtons.remove(rb);
            rb.setOnClickListener(null);
        }
    }

    public int getSelectedButtonIndex(){
        for(int i=0,n=registeredRadioButtons.size();i<n;i++){
            if(registeredRadioButtons.get(i).isChecked()){
                return i;
            }
        }
        return -1;
    }

    @Nullable
    public RadioButton getSelectedButton(){
        for(int i=0,n=registeredRadioButtons.size();i<n;i++){
            RadioButton r = registeredRadioButtons.get(i);
            if(r.isChecked()){
                return r;
            }
        }
        return null;
    }

    private final CompoundButton.OnCheckedChangeListener OCL = new CompoundButton.OnCheckedChangeListener() {
        @Override
        public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
            if(registeredRadioButtons.contains((RadioButton)compoundButton) && b){
                for(RadioButton r : registeredRadioButtons){
                    if(r.equals((RadioButton)compoundButton)){
                        continue;
                    }
                    r.setChecked(false);
                }
            }

        }
    };

}

とりあえず丸コピして使っていただいて構いません。

addChild(RadioButton rb)で登録したいRadioButtonを登録すると、登録したものの中で同時に1つだけが選択される仕組み。

逆になんでこれが公式に実装されてないのか疑問に思うレベルで必要だと思うのだが……。

現状の問題点

・ボタンの状態が変わった時に呼ばれるリスナーを他に登録しようとすると上書きになってしまうため使えない

自分が使う分には今のところこれで困ってないので、時間があるときに考えておきます。あるいは何かいい方法があったらコメントで教えて頂ければと。OnClickの方は普通に使えるのでそれだけで済むのであれば問題ないはず。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidエミュレータの日本語化(Android9)

設定アプリを起動

設定の一番下にあるSystemを開く

Languages & inputを選択

Languagesを選択

Add a Languageを選択

Add a Languageの下の方にある「日本語」を選択

Language pregerencesで日本語をいちばん上にする

この状態になったら完了

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

M5Stackで測定した温度と気圧をBLEで飛ばし、Androidアプリで受け取ってみる

本記事は、仙台のメイカースペース"FabLab Senda FLAT"で、Arduino等を使いながらIoT的機器を作っている集まりで発表した内容をまとめたものです。
この集まりはハード寄りの人が多いため、スマホアプリのBLEの開発についてのさわりについて発表しました。

概要

M5StackのENV.Sencorから温度と気圧を読み取り、そのデータをBLEのAdvertisingで飛ばします。
そして、Androidアプリで温度と気圧のデータを受け取りアプリ上で表示します。

overview.png

BLEでは双方向通信のポイントトゥーポイントと単方向のブロードキャストモードがありますが、今回は簡単なブロードキャストモードモードのみを扱います。

ソースコード

https://github.com/yhirano/ble_mobile_app_sample/

送信側(M5Stack)

M5Stackとは

M5Stackは液晶ディスプレイ、microSDカードスロット、3つのボタン、USB、Groveのコネクタ5cm四方の基板に搭載しプラスチックのケースに詰め込んだモジュールです。
CPUにはEspressifのESP32が搭載されており、Wi-FiおよびBluetooth v4.2とBLEを使用することができます。

いろいろな拡張基板が公式・非公式を問わずに用意されています。

M5Stack上で動くソフトの開発はEspressifのESP-IDF(C/C++言語)のほか、Arduino IDEや、MicroPythonなどを利用できます。

このように、M5StackはIoT機器の試作にはとても重宝するモジュールします。

M5Stackの準備

本記事では、M5GOに付属の本体とENV.Sensorを使用します。
写真のように本体のPORT AにENV.Sensorを接続します。

M5Stackの開発環境の構築

今回はArduino言語で開発します。
開発環境にはVSCodePlatformIO IDEプラグインを使います。

PlatformIOとは

Arduino言語で開発する際には一般的にはArduino IDEが使われますが、開発になれてくるとArduino IDEで開発をするのはツラいものがあります。

PlatformIOはいろいろなマイコン+いろいろなフレームワークに対応した高機能エディタにプラグインできる組み込み用IDEです。
VSCodeの拡張としてインストールすることができ、簡単に開発を始めることができます。

PlatformIOはVSCodeの他にもAtom、Vim、CLionといったエディタにも対応しています。

VSCodeのインストール

VSCodeのサイトからダウンロードし、インストールして下さい。

VSCodeへのPlatformIO IDE拡張のインストール

VSCodeを起動し、

p1.png
p2.png

使用するライブラリのダウンロード

M5Stack開発用のライブラリと、ENV.Sensorのライブラリをインストールします。

M5Stack開発用のライブラリをインストールします。
VSCodeのPIO Homeのタブより



次にENV.Sensorのライブラリをインストールします。


BMP280とはENV.Sensorに搭載されている温度・気圧センサーのチップです。
データシート等はこちら

プロジェクトの作成

プロジェクトを作成します。
VSCodeのPIO Homeのタブより


各種ファイルが生成されます。

M5Stackのセンサーの読み取りとBLEでのデータのアドバタイジング

src/main.cppを下記のように書き換えます。
処理の内容についてはコード内のコメントを参照してください。

src/main.cpp
#include <M5Stack.h>
#include <Adafruit_BMP280.h>

#include <BLEDevice.h>
#include <BLE2902.h>

// BLE上のこのデバイスの名前。適当な名前でOK。
#define BLE_LOCAL_NAME "M5GO Env.Sensor Advertiser"
// BLEのサービスUUID。適当なUUID(ランダムで生成したものがよい)を設定して下さい。
#define BLE_SERVICE_UUID "133fe8d4-5197-4675-9d76-d9bbf2450bb4"
// BLEのCharacteristic UUID。適当なUUID(ランダムで生成したものがよい)を設定して下さい。
#define BLE_CHARACTERISTIC_UUID "0fc10cb8-0518-40dd-b5c3-c4637815de40"

// BMP280のインスタンスを作成
static Adafruit_BMP280 bmp280;

static BLEServer* pBleServer = NULL;
static BLECharacteristic* pBleNotifyCharacteristic = NULL;

// 温度と気圧のデータを指定したバッファーに書き込みます。
// バッファーには、1〜4バイト目にはリトルエンディアンで温度を格納し、
// 5〜8バイト目にはリトルエンディアンで気圧を格納します。
// 
// - buffer uint8_tの8つ分以上のサイズのバッファを指定すること
static void pack(uint8_t* buffer, float temperature, float pressure) {
  int32_t temperature100 = (int32_t)(temperature * 100);
  buffer[0] = temperature100 & 0x000000ff;
  buffer[1] = (temperature100 & 0x0000ff00) >> 8;
  buffer[2] = (temperature100 & 0x00ff0000) >> 16;
  buffer[3] = (temperature100 & 0xff000000) >> 24;

  int32_t pressure100 = (int32_t)(pressure * 100);
  buffer[4] = pressure100 & 0x000000ff;
  buffer[5] = (pressure100 & 0x0000ff00) >> 8;
  buffer[6] = (pressure100 & 0x00ff0000) >> 16;
  buffer[7] = (pressure100 & 0xff000000) >> 24;
}

// 起動時に最初の1回だけ呼ばれる処理は setup 関数内に書きます
void setup() {
  // M5Stackの初期化
  M5.begin();

  // BMP280の初期化。
  // 初期化の結果が bmp280InitResult の変数に格納されます。
  // 引数に指定している BMP280_ADDRESS_ALT はBMP280センサーのI2Cアドレスです。
  bool bmp280InitResult = bmp280.begin(BMP280_ADDRESS_ALT);

  // M5StackのLCD(液晶ディスプレイ)を黒で塗りつぶします
  M5.Lcd.fillScreen(BLACK);
  // M5StackのLCDで表示する文字色を白、背景色を黒に設定します。
  M5.Lcd.setTextColor(WHITE ,BLACK);
  // M5StackのLCDで表示する文字の大きさを設定します。
  M5.Lcd.setTextSize(2);

  // "BMP280" という文字列をLCDの座標 (10, 20) の位置に表示します。
  // LCDの座標系は左上が(0, 0)、右下が(320, 240)です。
  M5.Lcd.setCursor(10, 20);
  M5.Lcd.print("BMP280");
  // "temperature:" という文字列をLCDの座標 (30, 50) の位置に表示します。
  M5.Lcd.setCursor(30, 50);
  M5.Lcd.print("temperature:");
  // "temperature:" という文字列をLCDの座標 (30, 80) の位置に表示します。
  M5.Lcd.setCursor(30, 80);
  M5.Lcd.print("pressure:");

  // BMP280の初期化が失敗した場合(bmp280InitResultがfalseの場合)は…
  if (!bmp280InitResult) {
    // M5StackのLCDで表示する文字色を黄色にします。
    M5.Lcd.setTextColor(YELLOW ,BLACK);
    // "Failed BMP280 init." という文字列をLCDの座標 (10, 200) の位置に表示します。
    M5.Lcd.setCursor(10, 200);
    M5.Lcd.print("Failed BMP280 init.");
  }

  // BLE環境の初期化
  BLEDevice::init(BLE_LOCAL_NAME);
  // BLEサーバの生成
  pBleServer = BLEDevice::createServer();
  // BLEのサービスの生成。引数でサービスUUIDを設定する。
  BLEService* pBleService = pBleServer->createService(BLE_SERVICE_UUID);
  // BLE Characteristicの生成
  pBleNotifyCharacteristic = pBleService->createCharacteristic(
                                // Characteristic UUIDを指定
                                BLE_CHARACTERISTIC_UUID,
                                // このCharacteristicのプロパティを設定
                                BLECharacteristic::PROPERTY_NOTIFY
                             );
  // BLE Characteristicにディスクリプタを設定
  pBleNotifyCharacteristic->addDescriptor(new BLE2902());
  // BLEサービスの開始
  pBleService->start();
  // BLEのアドバタイジングを開始
  pBleServer->getAdvertising()->start();
}

// 電源が入っている間は、この loop 関数で書いた処理が繰り返されます
void loop() {
  // M5StackのボタンA/B/Cの読み取り状態を更新しています。
  // ボタンを使わない場合でも、loop関数の冒頭で M5.update() を呼んでおくといいでしょう。
  M5.update();

  // BMP280から温度を取得します
  float temperature = bmp280.readTemperature();
  // BMP280から気圧を取得します。取得した気圧を100でわることで、hPaの単位になります。
  float pressure = bmp280.readPressure() / 100;

  // M5StackのLCDで表示する文字色を白、背景色を黒に設定します。
  M5.Lcd.setTextColor(WHITE ,BLACK);

  // LCDの座標 (180, 50) の位置に温度を小数二桁で表示します。
  M5.Lcd.setCursor(180, 50);
  M5.Lcd.printf("%.2fC", temperature);

  // LCDの座標 (180, 50) の位置に気圧を小数二桁で表示します。
  M5.Lcd.setCursor(180, 80);
  M5.Lcd.printf("%.2fhPa", pressure);

  // BLEでのデータ通知用バッファを定義
  uint8_t dataBuffer[8];
  // 温度と気圧のデータをdataBufferに格納します
  pack(dataBuffer, temperature, pressure);
  // データをBLEに設定し、送信します
  pBleNotifyCharacteristic->setValue(dataBuffer, 8);
  pBleNotifyCharacteristic->notify();

  // 33ミリ秒停止します
  delay(33);
}

プログラムの転送と実行

M5GO本体のUSB-CポートとPCを接続します。
そして、VSCodeの下部の「→」のをクリックするとプログラムが本体に転送され、本体でプログラムが実行されます。

本体の液晶ディスプレイに温度と気圧が表示されれば成功です。

受信側(Android)

Android端末(実機)の用意

Androidアプリの開発はPC上のAndroidエミュレータでもできますが、エミュレータではBLEがサポートされていませんので、BLEが搭載されたAndroid端末が必要です。

Androidアプリを実機で開発するには、実機で デバイスの開発者向けオプション を有効にする必要があります。
端末の [設定] > [デバイス情報] > [ビルド番号](端末によってメニュー階層が異なる場合があります) を7回タップすると デバイスの開発者向けオプション を有効にすることができます。

  • Android 7.1(API レベル 25)以下: [設定] > [デバイス情報] > [ビルド番号]
  • Android 8.0.0(API レベル 26)および Android 8.1.0(API レベル 26): [設定] > [システム] > [デバイス情報] > [ビルド番号]
  • Android 9(API レベル 28)以上: [設定] > [デバイス情報] > [ビルド番号]

デバイスの開発者向けオプション を有効にすると [設定] > [システム] > [詳細設定] > [開発者向けオプション](端末によってメニュー階層が異なる場合があります) のメニューが出現します。

Androidアプリ開発のためには、 [設定] > [システム] > [詳細設定] > [開発者向けオプション]でオプションをオンにし、 さらに[USBデバッグ] オプションもオンにする必要があります。

  • Android 7.1(API レベル 25)以下: [設定] > [開発者向けオプション] > [USBデバッグ]
  • Android 8.0.0(API レベル 26)および Android 8.1.0(API レベル 26): [設定] > [システム] > [開発者向けオプション] > [USBデバッグ]
  • Android 9(API レベル 28)以上: [設定] > [システム] > [詳細設定] > [開発者向けオプション] > [USBデバッグ]

詳しくはグーグルのドキュメントを参照して下さい。

Android Studioのインストール

Androidアプリの開発にはAndroid Studioを使用します。
Android Studioのインストールはグーグルのドキュメントと動画を参照して下さい。

プロジェクトの作成

Android Studioを起動し、



プロジェクトが作成されます。

Androidアプリでのデータの受け取りと表示

app/src/main/AndroidManifest.xml

app/src/main/AndroidManifest.xml を書き換えます。

app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.github.yhirano.ble_mobile_app_sample.android">

    <!-- BLEを搭載した端末でのみインストールできるように設定 -->
    <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

    <!-- BLEを使用するにはBluetoothのパーミッションが必要 -->
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <!--
      Android 6以降はBLEを使用するには位置情報取得のパーミッションが必要。
      Android 9以下は ACCESS_FINE_LOCATION の代わり にACCESS_COARSE_LOCATION でもOK。
     -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>

app/src/main/res/layout/activity_main.xml

app/src/main/res/layout/activity_main.xml を書き換えます。
(このファイルは画面のレイアウトを指定するためのファイルです)

app/src/main/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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=".MainActivity">

    <TextView
        android:id="@+id/text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:text="センサーデータの表示欄"/>

</androidx.constraintlayout.widget.ConstraintLayout>

app/src/main/src/java/your-package-path/MainActivity.kt

app/src/main/src/java/your-package-path/MainActivity.kt を書き換えます。

app/src/main/src/java/your-package-path/MainActivity.kt
package your-package-path.android // パッケージ名は書き換えないでください

import android.Manifest
import android.bluetooth.*
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.*


class MainActivity : AppCompatActivity() {

    /** 温度と湿度を表示するTextView */
    private val sensorTextView by lazy {
        findViewById<TextView>(R.id.sensorTextView)
    }

    /** RSSI(電波強度)を表示するTextView */
    private val rssiTextView by lazy {
        findViewById<TextView>(R.id.rssiTextView)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if (allPermissionsGranted()) {
            // 位置情報のパーミッションが取得できている場合は、BLEのスキャンを開始
            bleScanStart()
        } else {
            // 位置情報のパーミッションが取得できていない場合は、位置情報の取得のパーミッションの許可を求める
            ActivityCompat.requestPermissions(
                this,
                REQUIRED_PERMISSIONS,
                REQUEST_CODE_PERMISSIONS
            )
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                // 位置情報のパーミッションが取得できている場合は、BLEのスキャンを開始
                bleScanStart()
            } else {
                sensorTextView.text = "パーミッションが許可されていません"
                rssiTextView.text = null
            }
        }
    }

    /** REQUIRED_PERMISSIONSで指定したパーミッション全てが許可済みかを取得する */
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
    }

    /** BLEのスキャンを開始 */
    private fun bleScanStart() {
        val manager: BluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        val adapter = manager.adapter
        if (adapter == null) {
            sensorTextView.text = "Bluetoothがサポートされていません"
            rssiTextView.text = null
            return
        }
        if (!adapter.isEnabled) {
            sensorTextView.text = "Bluetoothの電源が入っていません"
            rssiTextView.text = null
            return
        }
        val bluetoothLeScanner = adapter.bluetoothLeScanner
        // "M5GO Env.Sensor Advertiser" というデバイス名のみの通知を受け取るように設定
        val scanFilter = ScanFilter.Builder()
            .setDeviceName("M5GO Env.Sensor Advertiser")
            .build()
        val scanSettings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
            .build()
        Log.d(TAG, "Start BLE scan.")
        bluetoothLeScanner.startScan(listOf(scanFilter), scanSettings, scanCallback)
    }

    /** スキャンでデバイスが見つかった際のコールバック */
    private val scanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            super.onScanResult(callbackType, result)

            rssiTextView.text = "RSSI(受信信号強度) ${result.rssi}"

            // デバイスのGattサーバに接続
            val bluetoothGatt = result.device.connectGatt(this@MainActivity, false, gattCallback)
            val resultConnectGatt = bluetoothGatt.connect()
            if (resultConnectGatt) {
                Log.d(TAG, "Success to connect gatt.")
            } else {
                Log.w(TAG, "Failed to connect gatt.")
            }
        }
    }

    /** デバイスのGattサーバに接続された際のコールバック */
    private val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
            super.onConnectionStateChange(gatt, status, newState)

            if (gatt == null) {
                Log.w(TAG, "Gatt is empty. Maybe Bluetooth adapter not initialized.")
                return
            }

            if (newState == BluetoothGatt.STATE_CONNECTED) {
                Log.d(TAG, "Discover services.")
                // GATTサーバのサービスを探索する。
                // サービスが見つかったら onServicesDiscovered が呼ばれる。
                gatt.discoverServices()
            }
        }

        override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
            super.onServicesDiscovered(gatt, status)

            Log.d(TAG, "Services discovered.")

            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (gatt == null) {
                    Log.w(TAG, "Gatt is empty. Maybe Bluetooth adapter not initialized.")
                    return
                }
                val service = gatt.getService(BLE_SERVICE_UUID)
                val characteristic = service?.getCharacteristic(BLE_CHARACTERISTIC_UUID)
                if (characteristic == null) {
                    Log.w(TAG, "Characteristic is empty. Maybe Bluetooth adapter not initialized.")
                    return
                }

                // Characteristic "0fc10cb8-0518-40dd-b5c3-c4637815de40" のNotifyを監視する。
                // 変化があったら onCharacteristicChanged が呼ばれる。
                gatt.setCharacteristicNotification(characteristic, true)
                val descriptor = characteristic.getDescriptor(
                    UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
                )
                descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
                gatt.writeDescriptor(descriptor)
            }
        }

        override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) {
            super.onCharacteristicChanged(gatt, characteristic)

            Log.v(TAG, "onCharacteristicChanged")

            this@MainActivity.runOnUiThread {
                val data = Data.parse(characteristic?.value) ?: return@runOnUiThread

                val sb = StringBuilder()
                sb.append("Temperature: ${String.format("%.2f", data.temperature)}\n")
                sb.append("Pressure: ${String.format("%.2f", data.pressure)}")
                sensorTextView.text = sb.toString()
            }
        }
    }

    /** 温度と気圧を持つデータクラス */
    private data class Data(val temperature: Float, val pressure: Float) {
        companion object {
            /**
             * BLEから飛んできたデータをDataクラスにパースする
             */
            fun parse(data: ByteArray?): Data? {
                if (data == null || data.size < 8) {
                    return null
                }

                val temperatureBytes = ByteBuffer.wrap(data, 0, 4)
                val pressureBytes = ByteBuffer.wrap(data, 4, 4)

                val temperature = temperatureBytes.order(ByteOrder.LITTLE_ENDIAN).int.toFloat() / 100.toFloat()
                val pressure = pressureBytes.order(ByteOrder.LITTLE_ENDIAN).int.toFloat() / 100.toFloat()
                return Data(temperature, pressure)
            }
        }
    }

    companion object {
        private val TAG = MainActivity::class.java.simpleName

        private const val REQUEST_CODE_PERMISSIONS = 10

        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)

        /** BLEのサービスUUID */
        private val BLE_SERVICE_UUID = UUID.fromString("133fe8d4-5197-4675-9d76-d9bbf2450bb4")

        /** BLEのCharacteristic UUID */
        private val BLE_CHARACTERISTIC_UUID = UUID.fromString("0fc10cb8-0518-40dd-b5c3-c4637815de40")
    }
}

アプリの実行

アプリを実行すると、M5Stackから飛んできた温度・気圧のデータを受け取り、画面に温度と気圧を表示します。

BLEを使うアプリ開発での気にすべきこと

BLEを搭載していない端末のサポートをする

BLEを搭載していない端末のサポートが必要です。
本記事のアプリのようにBLEがない端末でインストールできないようにするか、BLEがない端末で動くように設計をする必要があります。
最近ではBLEがない端末は見なくなったので、BLEがない端末にはインストールできないようにするのもよいかもしれません。

AndroidアプリでBLEを使用する場合はたくさんの端末でテストする

Android端末によってはBluetoothの挙動が異なっていることが結構あるので、可能な限りいろいろなOSバージョンと端末で確認することをおすすめします。(不特定多数の端末で使うアプリの場合は特に)

iOSアプリは、最新のiOSバージョンから1〜2つ前のバージョンをサポートしておけば市場的には充分で、かつ端末の種類もAndroidに比べれば少ないので、BLEを使うアプリを開発するならiOSの方が楽です。

AndroidのBLEのAPIはひどい

AndroidのBLEのAPIはひどいと評判です。実際書いててツラいものがあります。
BLEを使うAndroidアプリを開発する際は工数等の見積もりには注意しましょう。

Android BLE ひどい - Google 検索

参照資料

M5Stack + BLE

PlatformIO

PlatformIO + M5Stack

Android + BLE

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flutter環境構築 - 4(VSCodeセットアップ編)

はじめに

今回はFlutter環境構築第4弾 「VSCodeセットアップ編」です!
Flutter SDKのインストール・Flutterコマンドのセットアップがお済みでない方は、
先に「Flutter SDKインストール編」をご覧ください。

Flutter環境構築シリーズ
Flutter環境構築 - 1(Flutter SDKインストール編)
Flutter環境構築 - 2(Xcode・iOSシミュレーターセットアップ編)
Flutter環境構築 - 3(Android Studio・Androidエミュレータセットアップ編)

「VSCodeセットアップ編」でやること

本編では以下の項目を行います

  • VSCodeのインストール
  • VSCodeのセットアップ

VSCodeのインストール

1.VSCodeをインストールする

VSCode公式サイトへ行き、今すぐ無料でダウンロードするをクリックします。
スクリーンショット 2020-04-21 11.13.15.jpg

各自使用しているOSのものをダウンロードします。
スクリーンショット 2020-04-21 11.18.38.png

ダウンロードが完了したら解凍し、VSCodeを起動します。

VSCodeのセットアップ

1.Flutterプラグインをインストールする

VSCodeが起動できたら、左タブのExtensionsをクリックします。
スクリーンショット 2020-04-21 11.31.51.jpg

検索欄でFlutterと入力し、Flutterプラグインをインストールします。
スクリーンショット 2020-04-21 11.30.59.png

2.Androidライセンスを承諾する

ターミナルにて以下を実行します。

flutter doctor —android-licenses

yを複数回入力していき、ライセンス条項を承諾していきます。

3.flutter doctorを実行

ターミナルにて以下を実行します。

flutter doctor

実行すると以下のように表示されます。
ここで、Connected device以外のものに全て✔︎(チェックマーク)が入っていたらセットアップ完了となります。
もし!(ビックリマーク)がついているものがありましたら、そこに記述されている内容通りに行い、再度flutter doctorを実行してください。
スクリーンショット 2020-04-21 11.46.48.png

最後に

以上で「VSCodeセットアップ編」は終了となります。お疲れ様でした。

また、これにてFlutter環境構築シリーズは終了となります。

最後までご覧くださりありがとうございました。

Flutter環境構築シリーズ
Flutter環境構築 - 1(Flutter SDKインストール編)
Flutter環境構築 - 2(Xcode・iOSシミュレーターセットアップ編)
Flutter環境構築 - 3(Android Studio・Androidエミュレータセットアップ編)

参考サイト[Flutter公式サイト]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]Google アカウントを利用して Firebase にサインインする

はじめに

Firebase には FireabaseUI Auth という予め用意された UI を使ってサインインする方法と Fireabase SDK を利用してサインインする 2 つの方法があります。今回は Fireabase SDK を利用して Firebase にサインインする方法を試してみます。また Firebase には Mail・Google・Twitter・Facebook などでサインインする方法があるのですが、色々試すのは大変なので Google を使ったサインインするようにしてみます。

準備

Firebase を利用するには Firebase プロジェクト、 Android プロジェクトの作成が必要になります。なので下記の手順でセットアップをしていきます。

Firebase のプロジェクトを作成する

次の手順で Firebase のプロジェクトを作成していきます。

  1. [Firebase] にサインインし、「プロジェクト」の作成を選択する。
  2. 「プロジェクト名」に任意の名称を入力して、「続行」を選択する。
  3. 「Googleアナリティクス」を選択し、「続行」を選択する
  4. 「Googleアナリティクスアカウント」を選択し、「続行」を選択する
  5. Firebase のコンソールに移動し、作成した Firebase プロジェクトを選択する。

作成したプロジェクトに次の設定を行い、 Android アプリから Firebase を利用できるようにします。

  1. Firebase のコンソールで Firebase プロジェクトを選択する。
  2. アプリを追加 を選択し、プラットフォーム で Android を選択する。
  3. Androidパッケージ名の入力が求められるので、Android プロジェクトのパッケージ名を入力する。
  4. デバッグ用の署名証明書を入力し、アプリを登録を選択する。(署名証明書の作成はこちら)
  5. 設定ファイルのダウンロード や Firebase SDK の追加 や
    アプリを実行してインストールを確認 は後で確認するので、次へを選択する。
  6. Firebase のコンソールに戻り、Authentication -> Sign-in methodを選択する
  7. Sign-in method で Google を有効にする。
  8. 最後に プロジェクトの概要 -> 作成したアプリ名称 設定アイコン -> google-service.json でファイルをダウンロードする。

Android プロジェクトを作成する

そして次の手順で Android Studio で Android プロジェクトを作成します。これで Android から Firebase が利用できるようになります。

  1. [Android Studio]Android プロジェクトを作成する。
  2. [Andriod Studio]app に google-service.json をコピーする
  3. [Android Studio]app の build.gradle で必要なライブラリを加える
buildscript {
    ext.kotlin_version = '1.3.71'
    repositories {
        google() // 追加する
        jcenter()

    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.google.gms:google-services:4.3.3' // 追加する
    }
}

allprojects {
    repositories {
        google() // 追加する
        jcenter()

    }
}
apply plugin: 'com.google.gms.google-services'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    // Firebase Authentication
    implementation 'com.firebaseui:firebase-ui-auth:6.2.1' // 追加
    implementation 'com.google.firebase:firebase-auth:19.3.0' // 追加

    // Kotlin Coroutine
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5' // 必要に応じて追加
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5' // 必要に応じて追加

    // KTX
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-alpha01' // 必要に応じて追加
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-alpha01' // 必要に応じて追加

    // LiveEvent
    implementation "com.github.hadilq.liveevent:liveevent:1.0.1" // 必要に応じて追加
}

実装

初期化する

次のコードで GoogleSignInClient と FirebaseAuth クラスのオブジェクトの初期化をします。GoogleSignInClient は Google アカウントへのサインインを制御するクラス、 FirebaseAuth は Firebase へのサインインを制御するクラスになります。これらの 2つのオブジェクトを利用して Google アカウントを利用した Firebase へのサインインを実装していきます。

class GoogleAuthController(private val activity: AppCompatActivity) {
    // FirebaseAuth の生成
    private val firebaseAuth: FirebaseAuth by lazy {
        FirebaseAuth.getInstance()
    }

    // GoogleSignInClient の生成
    private val googleSignInClient: GoogleSignInClient by lazy {
        GoogleSignIn.getClient(activity, googleSignInOptions)
    }

    // GoogleSignInClient のオプション、今回は特に設定が必要ないのでデフォルト値的なものを利用する
    private val googleSignInOptions: GoogleSignInOptions by lazy {
        val signInOption = GoogleSignInOptions.DEFAULT_SIGN_IN
        val idToken = activity.getString(R.string.default_web_client_id)
        GoogleSignInOptions.Builder(signInOption).requestIdToken(idToken).requestEmail().build()
    }
}

サインインする

まずはFirebaseへのサインインに利用する Google アカウントを選択する必要があります。アカウントの選択は GoogleSignInClient の signInIntent 経由で選択できるようになっているので signInIntent を取得して startActivityForResult にセットして Intent を起動してやります。

class GoogleAuthController(private val activity: AppCompatActivity) {
    fun startSignIn() {
        val signInIntent = googleSignInClient.signInIntent
        activity.startActivityForResult(signInIntent, RC_SIGN_IN)
    }

    companion object {
        private const val RC_SIGN_IN = 100
    }
}

startActivityForResult で signInIntent を起動してやると次のような画面が出てくるので、表示された Google アカウントを選択して Firebase にサインインに利用するアカウントを決めてやります。

Image from Gyazo

アカウントの選択が完了したら、onActivityResult で選択した Google のアカウント情報(GoogleSignInAccount) を 引数の data(Intent) から取得してやります。あとは取得した アカウント情報(GoogleSignInAccount) から credential を生成し、FirebathAuth . signInWithCredential に credential を渡して実行すればサインインできます。あとサインイン完了の通知を受け取りたいこともあると思います、その場合は firebaseAuth.signInWithCredential の戻り値の Task の addOnCompleteListener でリスナーを追加してやります。今回はサインイン完了の処理はあとからカスタマイズできるようにサインイン開始時に引数で関数を渡し、完了時に呼び出す形にしています。

class GoogleAuthController(private val activity: AppCompatActivity) {
   private var completed: (FirebaseUser) -> (Unit) = {}

   fun startSignIn(completed: (FirebaseUser) -> (Unit)) {
      this.completed = completed
      val signInIntent = googleSignInClient.signInIntent
      activity.startActivityForResult(signInIntent, RC_SIGN_IN)
   }

   fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        
        val credential = GoogleAuthProvider.getCredential(account.idToken, null)
        firebaseAuth.signInWithCredential(credential).addOnCompleteListener(activity) { task ->
            if (!task.isSuccessful) {
                return@addOnCompleteListener
            }

            if (firebaseAuth.currentUser == null) {
                return@addOnCompleteListener
            }

            completed(firebaseAuth.currentUser!!)
        }
    }


}

サインアウトする

サインアウトは簡単です、Google と Firebase 両方をサインアウトしてやれば良いです。

class GoogleAuthController(private val activity: AppCompatActivity) {
    fun startSignOut() {
        firebaseAuth.signOut()
        googleSignInClient.signOut()
    }
}

最終的なコード

最終的にこのようなコードになりました、あとはこいつを Activity で呼び出してやればサインイン・サインアウトができるようになります。

class GoogleAuthController(private val activity: AppCompatActivity) {
    private val firebaseAuth: FirebaseAuth by lazy {
        FirebaseAuth.getInstance()
    }

    private val googleSignInOptions: GoogleSignInOptions by lazy {
        val signInOption = GoogleSignInOptions.DEFAULT_SIGN_IN
        val idToken = activity.getString(R.string.default_web_client_id)
        GoogleSignInOptions.Builder(signInOption).requestIdToken(idToken).requestEmail().build()
    }

    private val googleSignInClient: GoogleSignInClient by lazy {
        GoogleSignIn.getClient(activity, googleSignInOptions)
    }

    private var completed: (FirebaseUser) -> (Unit) = {}

    val currentUser: FirebaseUser? get() = firebaseAuth.currentUser

    fun startSignIn(completed: (FirebaseUser) -> (Unit)) {
        this.completed = completed
        val signInIntent = googleSignInClient.signInIntent
        activity.startActivityForResult(signInIntent, RC_SIGN_IN)
    }

    fun startSignOut() {
        firebaseAuth.signOut()
        googleSignInClient.signOut()
    }

    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        if (requestCode != RC_SIGN_IN) {
            return
        }

        val clazz = ApiException::class.java
        val account = GoogleSignIn.getSignedInAccountFromIntent(data).getResult(clazz)
        if (account == null) {
            return
        }

        val credential = GoogleAuthProvider.getCredential(account.idToken, null)
        firebaseAuth.signInWithCredential(credential).addOnCompleteListener(activity) { task ->
            if (!task.isSuccessful) {
                return@addOnCompleteListener
            }

            if (firebaseAuth.currentUser == null) {
                return@addOnCompleteListener
            }

            completed(firebaseAuth.currentUser!!)
        }
    }

    companion object {
        private const val RC_SIGN_IN = 100
    }
}

動作確認

あと動作を確認するための UI を作ってやります。Button を押すとサインイン・サインアウト、サインインできたらユーザートークンを取得し更新するシンプルなUIです。

Image from Gyazo

class MainActivity : AppCompatActivity() {
    private val coroutineScope = CoroutineScope(Dispatchers.IO)
    private val googleAuthController: GoogleAuthController by lazy {
        GoogleAuthController(this)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val userIdTextView = findViewById<TextView>(R.id.user_id_text_view)
        val signInButton = findViewById<Button>(R.id.sign_in_button)
        signInButton.setOnClickListener {
            googleAuthController.startSignIn {
                coroutineScope.launch {
                    val token = Tasks.await(it.getIdToken(true)).token
                    withContext(Dispatchers.Main) {
                        userIdTextView.text = token
                    }
                }
            }
        }

        val signOutButton = findViewById<Button>(R.id.sign_out_button)
        signOutButton.setOnClickListener {
            googleAuthController.startSignOut()
            userIdTextView.text = ""
        }

        coroutineScope.launch {
            val currentUser = googleAuthController.currentUser
            if (currentUser != null) {
                val token = Tasks.await(currentUser.getIdToken(true)).token
                withContext(Dispatchers.Main) {
                    userIdTextView.text = token
                }
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        googleAuthController.onActivityResult(requestCode, resultCode, data)
    }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent">

    <TextView
        android:id="@+id/user_id_text_view"
        android:layout_width="0dp"
        android:layout_height="0dp"a
        android:gravity="center"
        android:textSize="16sp"
        app:layout_constraintBottom_toTopOf="@id/sign_in_button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/sign_in_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Sign In"
        app:layout_constraintTop_toBottomOf="@id/user_id_text_view"
        app:layout_constraintBottom_toTopOf="@id/sign_out_button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/sign_out_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Sign Out"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@id/sign_in_button"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

サインインするとユーザートークンが表示され、

Image from Gyazo

サインアウトするとユーザートークンが消えますね。

Image from Gyazo

まとめ

Google アカウントを利用した Firebase へのサインインは Google アカウント情報を取得するために Intent の起動、 Intent の結果取得を自身で制御するので少しややこしいです。あと Google アカウントを利用したサインインは公式ドキュメントもあまり充実していないような気がするので、自分で少しずつ試していくしかなさそうです。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む