2022/08/21

[android] AlarmManager でアプリを起こしたい

やりたいこと

AlarmManager を使って、アプリが起動していないように見える状況でもアプリを起動させたい。

 

結果

アプリがタスク一覧に載っていなくてもアラームから起動することはできるが、強制停止状態では無理。

本体の OS が API 29 以降では Alarm のようなバックグラウンドからアプリを起動させることに制限がかかっているので、一旦 Notification などでユーザに通知だけ行い、ユーザ操作によってアプリを起動させることになるだろう。

バックグラウンドからのアクティビティの起動に関する制限  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/components/activities/background-starts

 


前回、AlarmManager を使って反復アラームを設定し、AndroidManifest.xml に設定した <receiver> の class がブロードキャストされた intent を受け取ってログ出力するところまで確認した。

私がやりたいのは、アプリが立ち上がっていない(と思われる)状況でも AlarmManager での発火をトリガにしてアプリを起動したい、というものだ。
プリインストールのような system 属性のアプリであれば好き勝手できそうだし、Google Play からインストールしたアプリであればあまり勝手なことをされたいとは思わないだろう。

なので今回は Google Play からインストールするような system 属性を持たないアプリで期待したことができるのか、できないならどこまでやれるのかを調べたい。


前回は 15 分にしたが、あれは WorkManager の最短が 15分だったはず、くらいで決めたものだ。
AlarmManager なら 3 分でもできた。

2022-08-20 20:48:32.650/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.MainActivity: setAlarm
2022-08-20 20:53:47.674/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.NotificationReceiver: Receive !!
2022-08-20 20:56:28.482/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.NotificationReceiver: Receive !!
2022-08-20 20:59:47.663/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.NotificationReceiver: Receive !!
2022-08-20 21:02:47.665/com.hiro99ma.alarmtest2 D/com.hiro99ma.alarmtest2.NotificationReceiver: Receive !!

まあ、間隔が短くできるということはリソースを食いやすいということでもあるだろうから、アプリの審査としては厳しくなってしまうのかも? どういう基準で見ているのかは知らんのだけど、面倒なアプリの方が審査に時間がかかりそうな気がするじゃあないか。そういう基準だったら、AlarmManager ではなく WorkManager の方が緩いと思うのだ。

ともかく、3分くらいだったらテストしやすいのでそれで見ていこう。


まず「アプリが終了している」という状態について確認が必要だ。

「アプリが動いている」という状態が一番確実なのは、そのアプリがフォアグラウンドになっていて Activity が表示されているときだ。これは間違いなく動いているだろう。

ならばActivity が表に出ていないときがどうなのか、ということになる。
調べていて出てきたのが「 Doze」と「アプリスタンバイ」というものだ。

Doze とアプリ スタンバイ用に最適化する  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/training/monitoring-device-state/doze-standby?hl=ja

書いてあるとおりなら、

【Doze】
デバイスが長い間使用されていない場合にアプリのバックグラウンド CPU とネットワーク アクティビティを保留する

【アプリスタンバイ】
ユーザーがしばらくの間操作していないアプリのバックグラウンド ネットワーク アクティビティを保留

アプリスタンバイはネットワークだけしか保留しないのか? 文章だけ読むと、先に「アプリスタンバイ」になり、それでも長い間使用されないようであれば「Doze」になるようにも見えたのだが、そうではないのだろう。

アプリで Doze をテストする
https://developer.android.google.cn/training/monitoring-device-state/doze-standby?hl=ja#testing_doze

「dumpsys deviceidle force-idle」を実行してそのアプリの動作が変わるなら、Dozeモードで動作が変わるアプリということだろう。

アプリスタンバイの場合はどれをスタンバイにするか指定しているので別物かなぁ、というところだ。
ネットで検索したらもっとよい情報が出てくるだろうが、今回はそこまで考えたくないので無視だ。


私が「アプリが「終了している」というのは、アプリスタンバイに近いと思う。
そして Android のタスク一覧から該当するアプリを上スワイプしたときも「終了している」だろうから起動してほしい。
できればアプリ情報画面で強制停止(force stop)させたあとからも起動してほしいのだが、これは難しいと思う。

あとは、アプリの中から「終了」を行った場合だ。
これが何をしているのかがよくわからんのだが、Activity が 1枚しかないときに「戻る」を選ぶと「終了しますか?」と尋ねてくるアプリがあったように思う。

ユーザが「アプリを終了したい」と思うことと、アプリとして終了するという実装は、一致しないとはいわないまでも、一致しづらいことが多いと思う。例えばゲームのハイスコアをサーバで管理してみんなで共有するようなアプリがあったとして、ユーザが「ハイスコアを更新したから終了させよう」と思ったタイミングと、アプリが「スコアのデータをサーバにアップしよう」と実装されているタイミングは必ずしも一致しないだろう。ユーザは終わった気分でいても、他のユーザがアクセス中だったりしてアップロードを保留しているかもしれない。サーバにアクセスが集中しないようにわざと遅らせるようにしているかもしれない。

そう考えると、モバイルのアプリは「終了」が即時行われるとは考えづらい。たぶん即時終了させるのがアプリ情報画面から「強制停止」を行う操作だろう。
一覧から上スワイプするのは強制停止ではないと思う。Doze にさせるか、スタンバイにしているかのどちらかではないか。調べていないけどスタンバイじゃないかなぁと思っている(個人の感想です)。

端末を USB接続しているとスリープしない状態になったりするし、その設定をオフにしても実は何かあるんじゃないの?とか、15分を 3分にしたことで挙動が違ったりするんじゃないの?とかいろいろ考えてしまうのだが、まあそのときはそのときだ。

Android Studio の Logcat タブでアプリが (DEAD) になるようであればアラームを受け付けることはさすがにできないだろう(と思ったが、タスク一覧から上スワイプすると DEAD になったので違うのか)。


adb shell でログインすると、 dumpsys というコマンドが使用できる。

dumpsys  |  Android デベロッパー  |  Android Developers
https://developer.android.com/studio/command-line/dumpsys?hl=ja

dumpsys コマンドだけたたくとすべてのシステムサービス情報を出力しようとするので、対象とするサービスを指定するのがよろしい。今回であれば alarm だ。
指定してもたくさん出力されるのだが、試作している alarmtest2 で検索するとこういう情報が見つかった。

    ELAPSED_WAKEUP #5: Alarm{bef203a type 2 origWhen 27853758 whenElapsed 27853758 com.hiro99ma.alarmtest2}
      tag=*walarm*:com.hiro99ma.alarmtest2/.NotificationReceiver
      type=ELAPSED_WAKEUP origWhen=+2m54s204ms window=+2m15s0ms repeatInterval=180000 count=0 flags=0x0
      policyWhenElapsed: requester=+2m54s204ms app_standby=-5s796ms device_idle=-- battery_saver=-5s796ms
      whenElapsed=+2m54s204ms maxWhenElapsed=+5m9s204ms
      operation=PendingIntent{77e94eb: PendingIntentRecord{b073248 com.hiro99ma.alarmtest2 broadcastIntent}}

ここでは ELASPSED_WAKEUP と経過時間で発火するように指定している。おそらく 3行目の type=ELAPSED_WAKEUP の次に出ている origWhen が発火するまでの残り時間だと思う。時間をおいて取得すると減っていたからだ。

これは、alarmtest2 アプリを起動すると出てきたし、タスク一覧で上スワイプして消したときも残っている。たぶん発火すると「App Alarm history」というところにも出てきたし、Alarm stats にも出てくるようになった。しかしアプリ情報から強制停止させると出てこなくなった。おそらくだが、アプリ情報から強制停止ボタンがタップできるような状態であればアラームは動くんじゃないかな。強制停止でアラームが消えるのは、登録元が消えたからか通知先が消えたからか。まあどっちでもよいが、確認するためには通知先を別アプリにしないとわからんな。

 

ん?
ということは、自分で自分にアラームを仕掛けておいて自分を起動する、というのは無理というか、既に起動しているから表に出してあげるだけで目的を達成できるんじゃなかろうか。

override fun onReceive(context: Context?, intent: Intent?) {
    val actionIntent = Intent(context?.applicationContext, MainActivity::class.java).apply {
        action = Intent.ACTION_MAIN
        addCategory(Intent.CATEGORY_LAUNCHER)
        flags = Intent.FLAG_ACTIVITY_NEW_TASK
    }
    context?.startActivity(actionIntent)
}

こんな感じだろうと思ったのだが、例外は発生しないもののアプリが表に出てくることはなかった。 Notification をタップしてアプリを表に出すときはこれでよかったのだが、状況が違うのか。

? I/ActivityTaskManager: START u0 {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.hiro99ma.alarmtest2/.MainActivity} from uid 10502
? W/ActivityTaskManager: Background activity start [callingPackage: com.hiro99ma.alarmtest2; ...(中略)...; inVisibleTask: false]
? D/ActivityTaskManager: getPreferredLaunchDisplay userset_displayid false current displayId -1
? D/ActivityTaskManager: getPreferredLaunchDisplay userset_displayid false current displayId -1
? E/ActivityTaskManager: Abort background activity starts from 10502

BroadcastReceiver だからダメなのかと思い、AlarmManager から setRepeating() するときの PendingIntent で同じ actionIntent を呼ぶようにして設定したのだが、それでも同じだった。

 

「ダメだった」という状況について説明していなかった。
手順はこうだ。

  1. alarmtest2 を起動する
  2. タスク一覧を出して alarmstart2 を上スワイプする
  3. アラームが発火するまで待つ(本体は 2分でスリープになる)

3分以上待っているとアラームが発火して Logcat に何か出てくるので、そのときにスマホをスリープ解除して画面を出し、alarmtest2 アプリが表に出ていれば成功、出ていなければダメ、という判定の仕方をしている。


logcat の 「Abort background activity start」がエラー出力なので、そこら辺か。

バックグラウンドからのアクティビティの起動に関する制限  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/components/activities/background-starts

あー、ユーザが操作しているのにいきなり表に出てくる可能性があるから制限がかかるのか。
わかる。私だったら怒るね。

ただ、私は alarmtest2 を API 26 以上ということで作っている。であれば API 29 からというこの制限から外れるんじゃないのか?

image

雰囲気からすると、ここに書いている APIレベルは本体の OS のものということだろう。

 

ともかく、Android としては「時間に依存する通知」を使ってほしいそうだ。リンク先や "ヘッドアップ通知" という言葉も出てくるので、いわゆる Notification を使えってことなのだと思う。 Notification → 通知をタップしてアプリを出す、がよいのかな。


というわけで、Notification を追加した alarmtest2 の最新版がこちら。

https://github.com/hirokuma/AndroidAlarmTest/commit/7aea025ed07fd53d4774a526f3328984ff4994a7

前々回の Notification 記事から持ってきたのだが、Notification を表示するタイミングとコンテキストが違うので多少変更されている。

アプリが起動して行うのは Notificationチャネルの作成のみ。
BroadcastReceiver の onReceive() で Notificationの作成と表示を行っている。記事を書いたときは with という Kotlin のスコープ関数なるもので notify() を呼んでいたが、BroadcastReceiver からは使えなかったので普通に書いた。

これを実行すると、数分おきに Notification の通知が行われる。スリープしていても Notification が出てくるので非常にうっとうしい。 API が間違っていなくても用法を間違うと嫌な動作をするという例と思ってもらおう。

2022/08/15

[android] AlarmManager でアラームする

Android の基礎学習。
AlarmManager でアプリが起動していなくても何かしたい。 notification が出したいのだが、まずは Log が出るだけでも良いだろう。

Schedule alarms  |  Android Developers
https://developer.android.com/training/scheduling/alarms

 

いろいろ書いてあるのだが、こういう系統はだいたいイベントが発火するときに PendingIntent で何かするものだろう。
PendingIntent は作るとき?に Intent を与えられて、PendingIntent の殻をかぶった Intent みたいなものだろうと思ってる。

作るといっても、PendingIntent.getActivity() だったり PendingIntent.getBroadcast() だったりといくつか作り方がある。前回はnotification の学習だったが、そこでは notification をタップしたときの挙動用に PendingIntent.getActivity() を使っていた。
タップすると自アプリが表に出てくる=Activity に対して通知するからだと思っている。

では、notification を出したい場合は誰が受け取るのだろう?
今回は Activity を表に出したくないので getActivity() ではないだろう。
受け取るために Service がいるのだったら getService() かもしれない。でもわざわざ service を作りたくないとも思う。
PendingIntent で他に PendingIntent を返す static メソッドはなさそうだ(ForegroundService みたいなの以外は)。

PendingIntent.getBroadcast() で作ってやって、それを受け取るところで処理できるなら一番楽そうな気がする。


ブロードキャストを受け取る方法について。

Broadcasts overview  |  Android Developers
https://developer.android.com/guide/components/broadcasts

ローカルだったら LocalBroadcastManager があるようなことを書いてあるが、class 自体が deprecated 扱いだったから今から使わなくてもよいだろう。他の observable pattern で置き換えられるようなことは書いてあったが、これを使えとは書かれていないのでチュートリアルの通りにやれば良いのだろう。

英語版を見ているが、受け取り方は「Manifest-declared receivers」と「Context-registered receivers」の2種類のようだ。
AndroidManifest.xml にあらかじめ書いておくタイプか、ソフトウェア上で必要なときだけ受け付けるタイプか。
その2択であれば、アラームについては AndroidManifest に書くタイプだろう。アプリが立ち上がっていないときに受け取りたいのだから。でも、もしかしたら一度登録しておけばよいというタイプかもしれんので実験はしてよかろう。


これに従ってアラームを作っていく。
「反復アラーム」というのは、ワンショットではないアラームのことだろう。

AlarmManager  |  Android Developers
https://developer.android.com/reference/android/app/AlarmManager

 

アラームの大きな設定は、タイプと精度のようだ。

タイプは、「何分後」みたいな経過時間タイプ(ELAPSED_REALTIME 系)と「何時何分」みたいな時間指定タイプ(RTC 系)があるようだ。
ELAPSED は経過時間といいつつ、デバイスが起動してからの経過時間という基準になっているそうだ。実装は、例では「SystemClock.elapsedRealtime()」からの相対値を指定していた。

精度は、曖昧だけど電池に優しいか、時間きっちりかを選択するもののようだ。メソッドとしてはおそらく第1引数に type を取るものがそれだと思う。

image

チュートリアルでは setInexactRepeating() と setRepeating() が紹介されている。 exact の頭に in- が付くことで「正確ではない」ということなのだろう。反復アラームだからその 2つだけ紹介されているのだろう。

アラームというものが Androidのバージョンで変化しているもののようで、APIの説明にいろいろ書いてある。

  • API 31以降で Intent.EXTRA_ALARM_COUNT を使うなら、 PendingIntent の flags に FLAG_MUTABLE がいる
  • API 19 の時点で反復アラームはすべて inexact になっている。繰り返しで正確にしたいなら、ワンショットのアラームを繰り返すことになる。

うーん、よくわからん。
API 20 以降なら setRepeating() だけでexact にできるということかな?

 

とりあえず動いているようなので記録を残しておいた。
Kotlin + Android 8以降 + Empty Activyty をベースにしている。

https://github.com/hirokuma/AndroidAlarmTest/tree/260da35b09a5012999a96de6d1e62903e7d525c2

 

Logcat のログを必要そうなところだけ抜粋。

2022-08-15 17:33:25.893/com.hiro99ma.alarmtest2 D/MainActivity: setAlarm
2022-08-15 17:52:25.390/com.hiro99ma.alarmtest2 D/NotificationReceiver: Receive !!
2022-08-15 18:14:40.987/com.hiro99ma.alarmtest2 D/NotificationReceiver: Receive !!
2022-08-15 18:23:33.683/com.hiro99ma.alarmtest2 D/NotificationReceiver: Receive !!
2022-08-15 18:35:38.638/com.hiro99ma.alarmtest2 D/NotificationReceiver: Receive !!

15分にしたのだが、15分より短いのもある。
WAKEUP の方にしたから放置した状態で確認したかったのだけど、ときどき触ったりしたので実際はどうだか分からん。

何が苦労したかって、 Activity::onCreate() でアラームを設定すると動作しなかったことだ。
何か方法はあるのかもしれないが、setAlarm() を onCreate() で呼び出すとダメだったのだ。動くサンプルがあったのだけどアラームの設定に違いがなさそうだったのでひどく悩んだのだ。そのサンプルはボタンをタップしてアラームを設定するようになっていたので、ためしに onResume() に持っていったら動作したのだった。

まだいろいろわかっていないが、今回はアラーム動いた記念なのでよしとしよう。

2022/08/11

[android] Notificationを出す

基礎の勉強。

Android Studio でプロジェクトを作る。
対象は Android 8.0 以降。Kotlin。Empty Activity。

Create a Notification  |  Android Developers
https://developer.android.com/training/notify-user/build-notification

日本語版もあるのだけど、若干更新されていない。まあ、実行時エラーで気づける程度のものだが。
ともかく、基礎的な notification を作成する。

Create a basic notification
https://developer.android.com/training/notify-user/build-notification#SimpleNotification

手順

  1. notification で表示する中身を作る。
    NotificationCompat.Builder() の後ろに設定するメソッドを連結して呼んでいくだけ。
    Builder() の第2引数である CHANNEL_ID は String型。これは Android 8.0 以降で追加されたものらしく、それより前は無視するとのこと。
    これは、この次に作る notification channel を指定するものだ。
    なお、日本語版で~と書いたのは、ここの PendingIntent.getActivity() の最後の引数が 0 になっていたからだ。今は何か指定しないといけないそうである。フラグの違いは検索すると出てくるので探してほしい。
  2. notification channel を作る。
    そういえば、Android のどこからかアプリの上にドットを出したりするようになったなぁ。
  3. notify() で表示。

 

こんな感じだ。

    private fun createNotificationChannel() {
        val name = "のてぃふぃけーしょんちゃんねる"
        val descriptionText = "のてぃふぃけーしょんちゃんねるのですくりぷしょん"
        val importance = NotificationManager.IMPORTANCE_DEFAULT
        val channel = NotificationChannel(NOTIFICATION_ID, name, importance).apply {
            description = descriptionText
        }
        val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(channel)
    }

    private fun createNotification(): NotificationCompat.Builder {
        // 自アプリを起こす
        val wakeupMeIntent = Intent(this, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        val wakeupMePendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, wakeupMeIntent, FLAG_IMMUTABLE)

        return NotificationCompat.Builder(this, NOTIFICATION_ID)
            .setSmallIcon(com.google.android.material.R.drawable.ic_clock_black_24dp)
            .setContentTitle("のてぃふぃけーしょんたいとる")
            .setContentText("のてぃふぃけーしょんてきすと")
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setContentIntent(wakeupMePendingIntent)
            .setAutoCancel(true)
    }

これらを、たとえば onCreate() などで呼び出す。
notify() の第1引数は Int 型で、ユニークならよいらしい。あとから中身を更新したり削除したりするときに識別するものだそうだ。

        createNotificationChannel()
        val builder = createNotification()
        with(NotificationManagerCompat.from(this)) {
            notify(NOTIFY_TEST_ID, builder.build())
        }

まあ、アプリの起動と同時に notification を出すことは少ないかもしれんが、うまいことやってほしい。


このアプリを起動すると notification が出てくる。
左側の時計アイコンは、適当に Android Studio の自動補完で候補に出てきたものだから、ちゃんとしたものかどうか知らん。

image

notification の中身は出てきたから良いとして、notification channel で設定したものはどこで出てくるのか?

見つけたのは、アプリごとの「アプリ情報」画面から「通知」をタップして表示される画面だった。

image

これだと notification channel の name だけなのだが、さらに表示されている項目をタップすると description の方も出てきた。

image

 

日本語にしておくと、そしてひらがなにしておくと他の文字列やデバッグ情報と重ならないからありがたいね。

 

ちなみにアプリ名が「AlarmTest」なのは、AlarmManager の実験をしたかったからだ。
時間になったら何かしたい→ notification を出すか、という流れだ。

2022/08/07

[android] ネットワーク通信のライブラリがいろいろある

Linux でネットワーク通信を行うならば、クライアント側だと

  1. socket つくる
  2. サーバ側と connect する
  3. あとはよしなにー

という感じだったと思う。

ただまあ、socket のような素に近いところを直接アプリで触らせたくないと思う。ラップするならそこだろう。
なので「Android では通信するのに volley というライブラリを使う」と書いてあったので、うまいことラップされているのかな、くらいに思っていた。

しかしそこにあったのは、socket をラップしたライブラリとかではなく、なんだかいろいろ隠したライブラリだった。
最終的には「便利」なのかもしれないが、ちょっと通信したいのに使うには難しそうだった。
まあ、使わないことにはわからないので、基本的なところはやってみよう。

関係ない話だが、volley を検索して見ていたページが google.cn の方だった。偽サイトに引っかかったのか?とあせったが google.cn も google の証明書だった。よかった。


まず、最初に書かれているこちら。

Volley は、Android アプリのネットワーク操作を容易化、高速化する HTTP ライブラリです。

socket 通信ではなく、HTTP系専用らしい。
HTTP だったら、というわけでもないが、ネットワークに関しては普通の Java でもライブラリがあったはずだ。 volley だって Android の最初からあったわけではなく、どちらかといえばつい最近だったと思う。それまで HTTP で通信できなかったわけではないので、 volley でなくてもよいはずなのだ。

しかし、Android Developer の接続に関するページを見ると、2項目目にはもう volley が出てきているので、なんらかの理由で推しているのだろう。

接続  |  Android デベロッパー  |  Android Developers
https://developer.android.com/guide/topics/connectivity?hl=ja

ただねぇ、ちょっと内側に行くと Retrofit というライブラリを使う説明をしてあった。なんなんだ。。。

HTTP クライアントを選択する
https://developer.android.com/training/basics/network-ops/connecting?hl=ja#choose-http-client

どうやらライブラリについていろいろと経緯があったらしい。

非同期通信をVolleyとOkHttpとRetrofitそれぞれで書いた場合の超簡易サンプル - Qiita
https://qiita.com/yamacraft/items/2bbc8867ef0a8ffb0945

ああ、okhttp も聞いたことがある。 gRPC を動かすときも okhttp のライブラリがなくてエラーになったし。
しかしすべてを把握したいわけでもない初心者としては、廃れなさそうなライブラリを 1つだけ覚えたいのだ。
volley はgoogle が開発しているから無難なのかな?
しかし OkHttp は HTTP/2 のことが書いてあったり、Retrofit と同じ Square が開発しているというのもあったりして魅力を感じてしまう

Overview - OkHttp
https://square.github.io/okhttp/

Retrofit は OkHttp を使っているので、HTTP 向けにしたラッパみたいなものなのかな? いや、 HTTP client だから違うな。 GitHub の About では、

OkHttp
Square’s meticulous HTTP client for the JVM, Android, and GraalVM

Retrofit
A type-safe HTTP client for Android and the JVM

となっているから、Retrofit はタイプセーフなところが売りなのだろう。

タイプセーフってなんじゃ?と思ったが、文字列ではなく Java や Kotlin の型で扱えることを指すのだろう。 volley はサンプルを見た感じでは文字列が返ってくるようだ。ただ、レスポンスが文字列、画像、JSON の場合は通常の Request で処理してくれるらしいし、特殊なことがしたければカスタマイズもできるようだ。画像って base64 なのかな?

どうせ私が使うなら JSON でやりとりするだろうし、それだったらどっちでもよいのかな。となると決め手がない。。。
なにか明確な違いがあればよかったのだが。
なので、どっちも同じことができるなら、実装してみて気に入った方を使うようにすれば良いと考えることにした。


ライブラリを試す前にちょっと脇道に逸れる。

新規でプロジェクトを作ろうとしたのだが、こっちもいろいろある。。。
どれが「普通」なのだろう?

image

まず、(Material3) の付いているのと付いていないのがある。
今のマテリアルデザインは 3 らしいので、付いていないのは 2 ってことか? 4 のページはなかったのでそうだと思う。

あとは、ただの Activity か Compose Activity かだ。
たぶん Jetpack Compose のことだろうと思うが、私に分かるんだろうか?

とりあえず Net1 という名前で作ってみた。
最初の 3行くらいは Kotlin の class の書き方なので分かるが、setContent 以降が初めて見る形だ。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Net1Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    Net1Theme {
        Greeting("Android")
    }
}

Kotlin の文法をある程度把握する方が先かもしれんな・・・。


まず、Project Settings の Dependencies で "com.android.volley:volley" を search して追加。
今の時点では 1.2.1 が見つかった。

スニペットを貼り付けてみた。
Alt + Enter で解決させていったのだが、そもそもスニペットを Compose Activity の構造としてどこに置いたらいいのかがよくわからない。
@Compose の関数内だとコンテキストがダメそうだったので MainActivity の private 関数として追加し、 onCreate() から呼ぶようにした。画面に出すのはわからないから Log にした。

いつものように Android Studio の再生ボタンを押したのだが、画面は出るものの関数が呼ばれている気配がない。ブレークポイントも止まらない。
どうやら Compose Activity でプロジェクトを作った際には Configuration が DefaultPreview になっているようで、それだと MainActivity の onCreate() すら呼ばれないようだった。

Configuration を app に切り替えると、ようやく動きがあった。

E/Volley: [37] NetworkDispatcher.processRequest: Unhandled exception java.lang.SecurityException: Permission denied (missing INTERNET permission?)
    java.lang.SecurityException: Permission denied (missing INTERNET permission?)

ああ、AndroidManifest.xml を追加していなかった。

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

これだけで動いた。
Koglin で変更したのは MainActivity だけで済んだ。青文字がそれである。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        network()
        setContent {
            Net1Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }

    private fun network() {
        // Instantiate the cache
        val cache = DiskBasedCache(cacheDir, 1024 * 1024) // 1MB cap

        // Set up the network to use HttpURLConnection as the HTTP client.
        val network = BasicNetwork(HurlStack())

        // Instantiate the RequestQueue with the cache and network. Start the queue.
        val requestQueue = RequestQueue(cache, network).apply {
            start()
        }

        val url = "https://www.google.com"

        // Formulate the request and handle the response.
        val stringRequest = StringRequest(
            Request.Method.GET, url,
            { response ->
                // Do something with the response
                Log.d("network", response)
            },
            { error ->
                // Handle error
                Log.e("network", "ERROR: %s".format(error.toString()))
            })

        // Add the request to the RequestQueue.
        requestQueue.add(stringRequest)
    }
}

呼び出しが一回だけだったら Volley.newRequestQueue() でよいそうだ。キャッシュを確保するのにもコストがかかるので、そこら辺は自分のアプリと相談することになるだろう。

JSON での API 実行のようにデータが小さいことがわかっている場合は、むしろキャッシュ無しという選択もあるんじゃなかろうか?
ああ、ちゃんと NoCache.java という

https://www.jma.go.jp/bosai/forecast/data/forecast/400000.json

cache のところを書き換えても動いたので、それでよいのかな?
ついでに JSON のデータを取ってくるようにした。返ってくるデータが JSON の Array なのか Object なのかによって関数名が変わるのは注意しておきたいところだ。

    private fun network() {
        // Instantiate the cache
        val cache = NoCache()

        // Set up the network to use HttpURLConnection as the HTTP client.
        val network = BasicNetwork(HurlStack())

        // Instantiate the RequestQueue with the cache and network. Start the queue.
        val requestQueue = RequestQueue(cache, network).apply {
            start()
        }

        val url = "https://www.jma.go.jp/bosai/forecast/data/forecast/400000.json"

        // Formulate the request and handle the response.
        val stringRequest = JsonArrayRequest(
            Request.Method.GET, url, null,
            { response ->
                // Do something with the response
                Log.d("network", response.toString())
            },
            { error ->
                // Handle error
                Log.e("network", "ERROR: %s".format(error.toString()))
            })

        // Add the request to the RequestQueue.
        requestQueue.add(stringRequest)
    }

最初は String で受け取っていたのだけど、日本語の部分が文字化けするのだった。 JSON で取ると化けなかったのだけど何でだろう? Windows だからか?


volley の欠片くらいは味わえたと思うので、次は Retrofit だ。

https://square.github.io/retrofit/

こういう感じかな?

  1. Retrofilt.Builder() でアクセスする根っこを指定する
  2. interface を作って、 GET したりする下側を準備する
  3. Call で呼び出す

ここだと、根っこの部分が「https://api.github.com/」で、GET するのは「https://api.github.com/users/{ユーザ名}/repos」。
ユーザ名は "octocat" を指定している。

https://api.github.com/users/octocat/repos

 

まず Project Structure > Dependencies からライブラリを追加。
これを書いている時点では 2.9.0 だった。

com.squareup.retrofit2:retrofit

INTERNET の permission は既に追加してあるので、あとはまねしていけばよいだけ。

だとおもったのだが、「android.os.NetworkOnMainThreadException」が出てしまった。
コルーチンでやるのが Compose Activity っぽい気はするが、別スレッドにすればよかろうということで Tread で囲んだ。

    interface Tenki {
        @GET("/bosai/forecast/data/forecast/{area}.json")
        fun listTenkis(@Path("area") area: String?): Call<List<TenkiData?>?>?
    }

    class TenkiData(val publishingOffice: String, val reportDatetime: String)

    private fun networkRetrofit() {
        Thread {
            val retrofit = Retrofit.Builder()
                .baseUrl("https://www.jma.go.jp/")
                .addConverterFactory(GsonConverterFactory.create())
                .build()

            val service = retrofit.create(Tenki::class.java)
            val data = service.listTenkis("400000")
            val result: List<TenkiData?>? = data?.execute()?.body()
            Log.d("network", "${result?.get(0)?.publishingOffice}, ${result?.get(0)?.reportDatetime}")
        }.start()
    }

"?" が多いのは Android Studio の指摘をそのまま採用したためだ。
ここで TenkiData に取得しないパラメータを追加しても、それは別にエラーとはならなかった。なので、あれば読み取るし無ければ読み取らないだけのよう。型が違う場合には変換動作によって例外が発生する。


こうやってみると、楽かどうかという意味では volley の方が事前準備が少なくて楽だった。ただ、読み取って使うくせに型も知らないのか?という意味では Retrofit の方がよいのかもしれんと思った。 JavaScript 風に扱えるといえばよいのかな。

2022/08/06

[android] gRPCの準備をしてみよう(2022年8月)

Android で gRPC をしてみたいので、今の最新と思われる情報でやってみる。
便利なライブラリが公開されているかもしれんが、まずは gRPC のサイトに従ってやってみよう。

Quick start | Kotlin for Android | gRPC
https://grpc.io/docs/platforms/android/kotlin/quickstart/

プロジェクトは Kotlin で、Android OS は 8.0 にした。あまり下げすぎても上げすぎてもなあ、というくらいで決めた。

そこまではよかったのだが、この Quick Start は example code を使うようになっていた。
うーん、プロジェクトに何も入っていないところからやりたかったのだが・・・。
仕方ないので、プロジェクトファイルから変更箇所を探していこう。

grpc-kotlin/examples at master · grpc/grpc-kotlin · GitHub
https://github.com/grpc/grpc-kotlin/tree/0681fc85677e2cca53bdf1cbf71f8d92d0355117/examples

ここが protobuf のファイルから生成された gRPC API のライブラリ?にアクセスするヶ所を作っているのだと思う。

private val greeter = GreeterGrpcKt.GreeterCoroutineStub(channel)
  

しかし GreeterGrpcKt という class はGitHub にはない。
package が io.grpc.examples.helloworld なのでこの proto ファイルでよいと思うので、自動生成されるのだろうか? 検索したら "BUILD.bazel" というファイルの中に protos/ がしばしば見つかる。

Bazel上のAndroid  |  Android オープンソース プロジェクト  |  Android Open Source Project
https://source.android.com/setup/build/bazel/introduction

うーん、サンプル伸びるど手順だと gradle を使うようだから、とりあえずは気にしなくてよいのかな?

それにしても Stub とかテスト用のコードが多いな・・・と思ったら、テストのスタブとは別物だそうだ。

2022年はネイティブ×gRPCが激アツかもしれん
https://zenn.dev/efx/articles/e90a93c1bd210e

そうなのか。

 

2021年11月の Google Developers には、Kotlin が protobuf をサポートしたという記事があった。

Google Developers Blog: Announcing Kotlin support for protocol buffers
https://developers.googleblog.com/2021/11/announcing-kotlin-support-for-protocol.html

そこにリンクされていたのが 2020年4月の記事で、これは grpc-kotlin のことを指しているのかな?

gRPC, meet Kotlin | Google Cloud Blog
https://cloud.google.com/blog/products/application-development/use-grpc-with-kotlin

grpc-kotlin のことなら、まあそれを使うと言うことでよしとしよう。
気になるのは Kotlin の protobuf  サポートについてだ。

From a proto definition, you can use the new built-in Kotlin support in the proto compiler to generate idiomatic Kotlin Domain Specific Languages (DSLs).

protobuf のコンパイラを用意しなくてもうまいことやってくれるということだろうか。。。
いや、記事の真ん中くらいに gradle の設定が書いてあるな。サンプルはこちら。なのだが、このサンプルは拡張子が .kts なので Android Studio で普通にプロジェクトを作った場合の build.gradle を文法が異なる。困ったものだ。


grpc-kotlin/examples を Android で使うだけだったらどのくらい削除して大丈夫か調べてみた。
そうしないと、どれがいるものかわからないからだ。

  • grpc-kotlin/ 直下は examples/ 以外は削除してよさそうだ
  • examples/ 直下は、 client/ , native-client/ , server/ , stub-lite/ , stub/ は削除してもよさそうだ
  • examples/protos/ の中は、下の方までたどって helloworld/ 以外は削除してよさそう

ビルドできるかどうかという観点だとこうなった。動くかどうかとはまた別だろうが、動的にファイルを読み込むこともないと思うから大丈夫なのかな?

stub-android/ はいらないかと思ったのだけど android/build.gradle.kts が参照しているのだった。なくすと io.grpc.ManagedChannelBuilder が import できなくなるので何か使っているんだろう。

 

差分がこちら。
JavaVersion や jvmTarget が 11 になったり、androidx.core:core-ktx が 1.8.0 になったりするのは、まあおまけのようなものだ。

https://github.com/hirokuma/grpc-kotlin-sample/commit/64d02c28220ef5b7c7611c0df8ab3f105c09bf61

protos/ と stub-android/ は grpc-kotlin から持ってきた。
追加した位置は grpc-kotlin と同じ高さで、 app/ の中ではなく app/ と同列。 settings.gradle を変更しているのはそれらを module として追加するためだと思う。
build.gradle は protobuf のプラグインを読み込んでいたのをそのまま持ってきた。 stub-android だけ追加してもなんとかなりそうな気もしたが試してはいない。

プロジェクトの依存関係としては、 app が stub-android を参照するようにした。 stub-android は protos を参照するようになっている(もともと)。

 

取りあえずビルドは通ったのだが、Android から呼び出そうとすると、今の私には知識が足りなかった。。

https://github.com/grpc/grpc-kotlin/blob/0681fc85677e2cca53bdf1cbf71f8d92d0355117/examples/android/src/main/kotlin/io/grpc/examples/helloworld/MainActivity.kt

by lazy ?  setContent ??  suspend fun ???  @Composable ???? といった感じだ。

Android Compose のチュートリアル  |  Android デベロッパー  |  Android Developers
https://developer.android.com/jetpack/compose/tutorial?hl=ja

Kotlin もまだ足りてないのに、コルーチンだの Jetpack だのいろいろ追いついていないのだった。
見栄えは悪いが、簡易的に表示まで確認した。

response · hirokuma/grpc-kotlin-sample@27e6b39
https://github.com/hirokuma/grpc-kotlin-sample/commit/27e6b39fe06e66681bc148d0c8a631d2dfbe3179