20200711のAndroidに関する記事は4件です。

ExoPlayerのUI変更方法

以下のように、 ExoPlayer は特別な変更をしなくても良い感じの UI にしてくれるのですが、

image.png

これらの UI をカスタマイズする事も出来ます。 :upside_down:

動画再生UIの変更

ExoPlayer の UI変更 はちょっと特殊で、 com.google.android.exoplayer2.ui.PlayerViewapp:player_layout_id に動画再生画面のレイアウトIDを指定します。

<com.google.android.exoplayer2.ui.PlayerView
        ~~~
        app:player_layout_id="{layout_id}" 
        />

UI を変更する時は適切な ID や Viewクラス を用いる必要があります。
それらは、PlayerView (ExoPlayer library)Overriding the layout file の項目に使用出来る resource_id が列挙されているので、こちらを参考にレイアウトを構築してください。

ちなみに、デフォルトではexo_simple_player_view.xmlというレイアウトが表示されております。

res_id 意味
exo_content_frame 再生中のメディアやアルバムアートを表示するフレームです。 AspectRatioFrameLayout
exo_buffering プレイヤーがバッファリングしている時に表示される View 。 View
exo_subtitles 字幕の View です。 SubtitleView
exo_shutter ビデオを非表示にするときに表示される View 。応用的な方法として、再生開始時は動画再生画面を隠すことで、ビデオ再生時のちらつきをなくす事が出来ます。 View
exo_artwork アルバムのアートワークを表示します。 ImageView
exo_error_message エラーが起きたときに表示されるテキスト。 TextView
exo_controller_placeholder インフレートされたPlayerControlViewを置き換えられます。exo_controllerが存在する時は無視されます。 View
exo_controller すでにインフレートされているPlayerControlView PlayerControlView
exo_ad_overlay 広告のUIを表示するためのオーバーレイです。 FrameLayout
exo_overlay プレイヤーの上部に表示されるオーバーレイです。 FrameLayout

exo_controller_placeholderexo_controller の違いはいまいちわかっていませんので、詳しい方は教えて頂けるとうれしいです。。。 :bow:

動画の読み込み中に Lottieアニメーション が表示されるようにします。
アニメーションは LottieSample に良い感じの物が落ちていたので、こちらを使わせて頂きます。
https://lottiefiles.com/1055-world-locations

事前準備

読み込み中に exo_buffering を表示させるには、 Layout に以下の設定が必要です。

<com.google.android.exoplayer2.ui.PlayerView
        ...
        app:show_buffering="always" 
        ...
        />

UI変更

プレイヤーのUI

exo_buffering を画面全体を覆う半透明の黒い Layout の上に先ほどの LottieAnimation を表示する様にします。

my_movie_view.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/exo_content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center">

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">


        <TextView
                android:id="@+id/exo_error_message"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        <View
                android:id="@id/exo_controller_placeholder"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent" />

        <androidx.constraintlayout.widget.ConstraintLayout
                android:id="@+id/exo_buffering"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:background="#9E000000"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent">

            <com.airbnb.lottie.LottieAnimationView
                    android:layout_width="300dp"
                    android:layout_height="300dp"
                    app:layout_constraintBottom_toBottomOf="parent"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    app:lottie_autoPlay="true"
                    app:lottie_loop="true"
                    app:lottie_rawRes="@raw/loading" />
        </androidx.constraintlayout.widget.ConstraintLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>

UI指定

activity_main.xmlで UI の レイアウトID を指定します。

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">

    <com.google.android.exoplayer2.ui.PlayerView
            android:id="@+id/player_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="#00000000"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:player_layout_id="@layout/my_movie_view"
            app:show_buffering="always" />

</androidx.constraintlayout.widget.ConstraintLayout>

完成

こんな感じになります。

output-1.gif

コントロールバーの変更

コントロールバーも動画再生UIの変更と同じような感じで、 com.google.android.exoplayer2.ui.PlayerViewapp:controller_layout_id にコントロールバーの レイアウトID を指定します。

<com.google.android.exoplayer2.ui.PlayerView
        ~~~
        app:controller_layout_id="{layout_id}" 
        />

動画再生UI と同じく、 UI を変更する時は適切な ID や Viewクラス を用いる必要があります。
それらは、PlayerControlView (ExoPlayer library)Overriding the layout file の項目に使用出来る resource_id が列挙されているので、こちらを参考にレイアウトを構築してください。

ちなみに、デフォルトではexo_playback_control_view.xmlというレイアウトが表示されております。

res_id 意味
exo_play 再生ボタン :arrow_forward: View
exo_pause 一時停止ボタン :pause_button: View
exo_rew 巻き戻しボタン :rewind: View
exo_ffwd 早送りボタン :fast_forward: View
exo_prev 前に戻るボタン :track_previous: View
exo_next 次に進むボタン :track_next: View
exo_repeat_toggle リピートボタン :repeat: drawableのID指定によって、トグルの画像を変更します。
exo_controls_repeat_offexo_controls_repeat_oneexo_controls_repeat_all
ImageView
exo_shuffle シャッフルボタン :twisted_rightwards_arrows: drawableのID指定によって、トグルの画像を変更します。
exo_controls_shuffle_offexo_controls_shuffle_on
ImageView
exo_vr VRモードボタン View
exo_position 再生位置を表すテキスト。 TextView
exo_duration 動画自体の長さを表すテキスト。 TextView
exo_progress_placeholder インフレートされたDefaultTimeBarを置き換えられます。exo_progressが存在する時は無視されます。 View
exo_progress 再生中に更新され、シークできるプログレスバー。 DefaultTimeBarが表示されています。 TimeBar

exo_progress_placeholderexo_progress の違いはいまいちわかっていませんので、詳しいから教えて頂けるとうれしいです。。。 :bow:

プログレスバーについて

ExoPlayerのプログレスバー、一見SeekBarを使っていそうに見えますが、実は違うんです・・・。 :innocent:

public class DefaultTimeBar extends View implements TimeBar {
  ...
}

普通の Viewクラス ですね・・・。じゃあどうやってバーを描画しているの?と思ってコードを追っていると、毎回表示位置を計算して Canvas に描画している事がわかりました。 :innocent:

@Override
public void onDraw(Canvas canvas) {
  canvas.save();
  drawTimeBar(canvas);    // タイムバーをCanvasへ描画
  drawPlayhead(canvas);  // プレイヘッドをCanvasへ描画
  canvas.restore();
}

これの何が痛いかというと、プログレスバーの背景を画像にしたり、角丸にしたり等が容易に出来ないのです。 :innocent:
DefaultTimeBar側で背景色の変更やプレイヘッドの Drawable変更 のインターフェースは設けられているのですが、背景変更は存在しません。。。

こうなった経緯

以下の Commit を確認すると、元々はSeekBarで実装されていたことがわかります。

https://github.com/google/ExoPlayer/commit/9d20a8d41c0cc6cd01b20bd72f3bbfcf11e49cae#diff-e62855b4d9e67db5a4bbac0616213cc3L68

では、なぜわざわざ Canvas への描画方式へ変更したのかと言いますと、この変更がリリースされたのが、 v2.4.0 になっており、この時のリリースノートには以下の様に記述されています。

New time bar view, including support for displaying ad break markers.

広告の再生位置表示を行う様にしたという内容です。 YouTube のように広告再生位置をバー上に表示する、という内容ですね。
このようなバー上に何かを描画する場合は、 SeekBar でやるより、 Canvas へ描画した方が楽なので、このようにしたのだと推測出来ます。

プログレスバーの背景変更の解決方

しばらく、どうすればいいのだ・・・?と途方に暮れていましたが、 TimeBar を実装した、 SeekBar のサブクラスを作成すれば良いという事に気づきました。
以下の様なクラスを作ると行けます。

SeekTimeBar.kt
class SeekTimeBar @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null
) : SeekBar(context, attrs), TimeBar {
    private val listeners: CopyOnWriteArraySet<TimeBar.OnScrubListener> = CopyOnWriteArraySet()
    private var isSeeking = false

    init {
        setLayerType(View.LAYER_TYPE_SOFTWARE, null)
        setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                listeners.forEach {
                    it.onScrubMove(this@SeekTimeBar, progress.toLong())
                }
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {
                isSeeking = true
                listeners.forEach {
                    it.onScrubStart(this@SeekTimeBar, progress.toLong())
                }
            }

            override fun onStopTrackingTouch(seekBar: SeekBar?) {
                isSeeking = false
                listeners.forEach {
                    it.onScrubStop(this@SeekTimeBar, progress.toLong(), false)
                }
            }

        })
    }

    override fun setKeyCountIncrement(count: Int) {
        progress += count
    }

    override fun setBufferedPosition(bufferedPosition: Long) {
        secondaryProgress = bufferedPosition.toInt()
    }

    override fun addListener(listener: TimeBar.OnScrubListener?) {
        listeners.add(listener)
    }

    override fun setDuration(duration: Long) {
        max = duration.toInt()
    }

    override fun removeListener(listener: TimeBar.OnScrubListener?) {
        listeners.remove(listener)
    }

    override fun setKeyTimeIncrement(time: Long) {
        progress += time.toInt()
    }

    override fun setPosition(position: Long) {
        //シーク中は何もしない(ちらつき防止)
        if (isSeeking) return
        progress = position.toInt()
    }

    override fun setAdGroupTimesMs(adGroupTimesMs: LongArray?, playedAdGroups: BooleanArray?, adGroupCount: Int) {
    }

    override fun verifyDrawable(who: Drawable): Boolean {
        return super.verifyDrawable(who) || who == progressDrawable
    }
}

あとは、 XML側 で android:progressDrawableandroid:thumb を指定すれば背景変更やプレイヘッド変更が可能になります。
(広告再生位置の表示は未対応です。それを実装したい場合は少々めんどくさい・・・。)

長らくプログレスバーについて説明していましたが、ここではコントロールバーの変更の例を示します。
再生ボタンと停止ボタンを前面に表示するだけのコントロールバー(バー・・・?)を実装します。

事前準備

今回はコントロールバーを全画面表示したいので、 my_movie_view.xmlexo_controller_placeholder のサイズを 0dp に変更します。

my_movie_view.xml
<View
        android:id="@id/exo_controller_placeholder"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

UI変更

コントロールバーのUI

背景を半透明にして、再生ボタンと停止ボタンを中央に表示します。
ややこしいですが、再生中は停止ボタンを表示するようにし、停止中は再生ボタンを表示するようにしています。(YouTubeと同じ方式)

my_controller_view.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"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#BE000000">

    <ImageButton
            android:id="@+id/exo_pause"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="@null"
            android:scaleType="fitXY"
            android:src="@drawable/exo_controls_pause"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    <ImageButton
            android:id="@+id/exo_play"
            android:layout_width="200dp"
            android:layout_height="200dp"
            android:background="@null"
            android:scaleType="fitXY"
            android:src="@drawable/exo_controls_play"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_goneMarginStart="25dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

UI指定

activity_main.xml で UI の レイアウトID を指定します。

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">

    <com.google.android.exoplayer2.ui.PlayerView
            android:id="@+id/player_view"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:background="#00000000"
            app:controller_layout_id="@layout/my_controller_view"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:player_layout_id="@layout/my_movie_view"
            app:show_buffering="always" />

</androidx.constraintlayout.widget.ConstraintLayout>

完成

こんな感じになります。

controller.gif

最後に

ExoPlayer の UI変更 についてまとめました。
UI変更方法自体はちょっと複雑ですが、マスターすればいろんな動画再生の UI を実現出来るような気がします。
(もっと楽に UI変更できて欲しいという気持ちはありますが・・・。 :innocent:

今回、 UI変更 について再調査している時に、まだ自分の把握していない設定がありましたので、次回触るときにはマスターしておきたいですね・・・。

参考

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

Bitriseで32分かかっていたAndroidのCI/CDをGitHub Actionsに移行して15分にした話

はじめに

これは自分が内定者バイトとして参画している OPENREC.tv(オープンレック) のタスクとして取り組んだ、CIの高速化に関しての知見を残すために書くブログです。

CIのある環境でコードを書いた経験はありますが、CI自体の構築は3ヶ月前に初めて個人Repositoryで行った程度でした。

そのときに書いた記事Github ActionsでマルチモジュールAndroidプロジェクトのCI環境を整えよう(ビルド/Slack通知/Danger/ktlint)も是非読んでみてください。

Bitirse運用での課題

workflow実行時間が長い

一番の問題がworkflow実行時間が長すぎることです。

OPENREC.tvはサービスを開始してから約5年経過しており、コードの量もかなり増えてきたため1度の実行で30~40分程かかっていました。

PRレビューの修正後も35分待ち、再度レビュー指摘があったりktlintFormat忘れをしようものなら再度35分CIを待たなければなりませんでした。 

workflow実行j感がが長いせいでなかなかマージができずに作業が進まないことや、hotfixも出すのに時間がかかる問題がありました。

ボトルネック

上図を見ると、 Android Lint Unit Test でかなり時間がかかっていることがわかります。

また、これら2つよりは短いものの、 android-build も他と比べると時間がかかっています。

これらはプロダクトが成長していくにつれてさらに肥大化していくため、今後も実行時間が長くなっていくことが想像できます。

そのため、

  • Android Lint(ついでにktlint, detektも)
  • Unit Test
  • Build

の実行時間を短くするアプローチではなく

これらを並列で実行することでworkflow全体の実行時間を短縮する アプローチで改善していくことにしました。

Bitriseで試みた解決方法(失敗)

そもそもBitriseには、他のサービスと違ってjobを並列で回すことはできません。

しかし、 build-router-startを使うことで、 workflow の並列化ができます。

ex) Unit Test と lintを並列で回す場合

※画像は

PRpush で左の build-test workflowが起動し、 Bitrise Start Build で右の lint workflowをtriggerさせます。

lintが完了するのを Bitrise Wait for Build stepで待つことで、見かけ場は並列実行ができているように見えます。

しかし、

  • Bitirseのworkflow並列実行可能数を圧迫してしまうため実質的な高速化にはつながらない
  • lint reportをマージするのが困難

この2つの理由から build-router-start での並列実行は断念しました。

Bitirseのworkflow並列実行可能数を圧迫してしまうため実質的な高速化にはつながらない

この方法はあくまでも workflow 単位での並列化のため、同時並列実行可能枠が1である無料プランではそもそも使えませんし、有料プランでも並列実行可能枠に応じた課金となるため、この方法ではworkflowを圧迫してしまい、結局高速化には繋がりませんでした。

※4枠あったとして、30分を4枠で同時に実行するのと、15分だけど2枠使うworkflowを4枠で実行するのでは実行時間としてはほぼ同じ(むしろinstall missing Android SDK等で実行時間は伸びる)

lint reportをマージするのが困難

workflow間でデータの共有をする方法は主に2つあると思っています。

  • 環境変数を用いる
  • Cacheを用いる

前者に関しては、 公式ドキュメント(環境変数のサイズ制限を増やす)より、1つあたり20KB、合計120KBという制限があります。

multi moduleでは各moduleでreportが生成されますし、ktlint以外にもAndroid Lint, detekt, test reportも環境変数で渡そうとするとサイズ制限にひっかかるため難しいので断念。

後者のCacheに関しては、少しややこしいので図解します。

キャッシュは、ブランチ単位で保持されます。

今回やりたいこととしては、

lint reportbuild-test workflowに共有したい」

のですが、

lint reportがブランチ単位で保持されるため、任意のPRでのみ使用するlint reportをキャッシュで持ってしまうと、タイミングが悪ければ同じブランチのPRでそのキャッシュが適用されてしまう可能性があります。

【具体例】hoge branchで初めてPRを出した時にlint結果をDangerで指摘する場合

スクリーンショット 2020-07-11 17.03.12.png

解決策は、キャッシュするディレクトリ名を変えたり、できなくはないのですがBitriseの設計思想としておそらく上記のユースケースは想定されていないため、無理やり実装することになりそうです。

結論

結局、上記の理由からBitriseで並列実行するのは諦め、他のCIツールに乗り換えることにしました。

BitriseはGUIで簡単にworkflowが構築できる反面、プロジェクトが大きくなるにつれ辛みもでてくるなーという感じです。

Github ActionsでのCI構築

Github ActionsでマルチモジュールAndroidプロジェクトのCI環境を整えよう(ビルド/Slack通知/Danger/ktlint) にも書いたので、詳しい構文の説明はしません。

スクリーンショット 2020-07-11 17.17.21.png

jobの構成はこんな感じです。
時間のかかるlint, build, testを並列化し、それらのjobが終わった後にreportを元にdangerでPRにコメントしてSlackに通知する形にしました。
deployのjobはdangerと並列しても良いですし、workflowを分けても良いと思います。

マルチモジュール配下でのjobの並列化

初めての慣れないymlでのCI構築というのと、マルチモジュールでつまづいた点もあったので共有です。

lint結果、test結果のupload

Bitirseでも少し触れましたが、job間は環境がクリアされるためlintやtest結果をjob間でやりとりする必要があります。

スクリーンショット 2020-07-11 17.17.28.png

Cacheを使うと、上述したような状況になる可能性があります。

そこで使うのが アーティファクトです。

成果物を使用してワークフローデータを永続化する

Artifacts allow you to persist data after a job has completed, and share that data with another job in the same workflow

より、同一workflowのjob間で共有することができます。

- name: Upload Hoge
  uses: actions/upload-artifact@v2
  with:
    name: hoge
    path: hoge/hogehoge.txt

しかし、multi module環境下では、各moduleでreportは生成され得ます。

スクリーンショット 2020-07-11 17.17.35.png

upload actionは上記のように、ファイルかディレクトリを指定してuploadします。

multi moduleで、各moduleのディレクトリで良いのですが、moduleが数十もあるととてもじゃないですがメンテナンスができません。

そこで、conference-app-2020(DroidKaigi2020) を参考にし、各moduleのreport結果を1つのディレクトリにまとめてuploadすることですっきり書くことができました。

Dangerfile でlint reportのディレクトリを指定している場合はDir.glob("**/checkstyle.xml").each do |xml| のように任意のディレクトリのreportを集めてあげるように修正する必要があるので注意です。

upload, download での注意点

upload-artifactは、ファイル単位でjob間でデータを渡すことができます。
しかし、download-artifactでは、ディレクトリ単位でデータを受け取るため、注意が必要です。

例えば build job でapkをuploadし、 deploy job でdownloadするとき、このように書くと意図せぬ結果になります。

- uses: actions/upload-artifact@v2
  with:
    name: generated development apk
    path: ./app/build/outputs/apk/hoge/debug/app-hoge-debug.apk

ここまではok

- uses: actions/download-artifact@v2
  with:
    name: generated development apk
    path: ./app/build/outputs/apk/hoge/debug/app-hoge-debug.apk

このようにdownloadしようとすると、見かけ上はdownloadできているようにlogが残るのですが、
deploy時にバイナリが破損していてdeployできない問題に遭遇しました。

download-artifactのドキュメントを見ると、

Basic (download to the current working directory):
or
Download to a specific directory:

と、基本的にはディレクトリのパスを指定してのdownloadをするひつようがありました。
そのため、

- uses: actions/upload-artifact@v2 
  with:
    name: generated development apk
    path: ./app/build/outputs/apk/hoge

のようにupload時もディレクトリで指定して、downloadするときも同様のディレクトリを指定することで解決しました。

定期実行

schedule構文を使います。

on:
  # default branchの最新commitに対して規定の時刻でworkflowを実行する
  schedule:
    - cron:  '0 14 * * 1-5'

土日を抜いた23時にCIを回すよう設定できました。

時間に関しては、デフォルトはUTCです。

他の記事で envにTZ: 'Asia/Tokyo' を指定するとみたのですが、TimeZoneの変更がうまくいかず、 UTC時間で記載しています。(わかる方いたら教えてください)

cron: のところ、spaceが2つなので気をつけてください。めっちゃハマりました。

tag push

tag構文を使います。

tagにバージョンを指定することでリリースビルドが走り、deployするようなことをする際には、

on:
  push:
    tags:
      - ver*

このように書けます。

無駄なworkflowをキャンセルする

Github Actionsは従量課金のため、同一PRで既に走っているworkflowは自動でキャンセルするようにしないとかなりコストがかかってしまいます。

ここに関してはまだデファクトスタンダードなものはないみたいですが、現段階で一番スターの多いCancel Workflow Actionを使ってみました。

- name: Cancel Previous Runs
  uses: styfle/cancel-workflow-action@0.4.0
    with:
      workflow_id: 1111111
      access_token: ${{ secrets.GITHUB_TOKEN }}

private repositoryの場合は

https://api.github.com/repos/:org/:repo/actions/workflowsrepo の読み取りができるアクセストークンを添えてcurlで叩くとworkflowのidが取得できます。

このactionを使うことで、新しくcommitをpushした際に前回分commitのworkflowは自動でキャンセルされるので無駄な課金がされなくなり安心ですね。

おわりに

全てのstepを直列実行していたBitriseからGithub Actionsに移行し、時間のかかるstepを並列で実行したことにより、

1回あたりのworkflow実行時間が30~40分 → 15分に短縮できました?

スクリーンショット 2020-07-11 17.17.54.png

これでCIを待つ時間が半分になり快適な開発に貢献できたと思うと、最高ですね。

UI実装や機能開発などの普段のAndroid開発と違い、経験が浅かったため少しのバグや機能追加でも結構詰まりましたが、苦労してやった分かなり力が着いたと思います。

まだ色々Github Actionsで効率化できそうなので、今後も個人でちょくちょく触って記事を書いていくのでよろしくお願いします。

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

Xamarin.Forms ガワネイティブアプリで、C# から JavaScript へのリクエスト~レスポンスを Task<T> にする

メモ書きなので雑。

シナリオ

  • Xamarin.Forms + WebView(HTML, JavaScript な SPA) のガワネイティブアプリである
  • Android アプリの BACK キーの有効無効を、WebView 内のページ状態によって切り替えたい
    • データ編集中(未保存)なので Back キーで history.back やアプリ終了されたら困る、とか

流れ

1. [Forms側] Action<TaskCompletionSource<bool>> OnRequestIsEnableBackKey { get; set; } を生やす

2. [Forms側] WebViewEx(WebViewを拡張したもの)に Task<bool> IsEnableBackKeyAsync() も生やす。実装は次のように。

public Task<bool> IsEnableBackKeyAsync()
{
    var comp = new TaskCompletionSource<bool>();

    if (OnRequestIsEnableBackKey != null)
    {
        OnRequestIsEnableBackKey.Invoke(comp);
    }
    else
    {
        comp.SetResult(false);
    }

    return comp.Task;
}

3. [Android側] WebViewRenderer を拡張した MyWebViewRenderer を作る。

4. Xamarin.Forms の WebView で JavaScript 連携を行う(with iOS/Android共通化) - Qiita を参考に JavaScriptHandler も作る。コンストラクタで Control に加え e.NewElement as WebViewEx も渡す。引数の名前は WebViewEx outer とする。AddJavascriptInterface の第2引数は GawaApp とでもしておく。

5.JavaScriptHandler のコンストラクタで OnRequestIsEnableBackKey を受信するコードを書く。

private TaskCompletionSource<bool> isEnableBackKeyComp = null;

public JavaScriptHandler(Android.Webkit.WebView webView, WebViewEx outer)
{
    outer.OnRequestIsEnableBackKey = comp => 
    {
        isEnableBackKeyComp = comp;

        // メインスレッドから呼ばないとエラー
        webView.Post(() =>
        {
            webView.EvaluateJavascript("window.requestIsEnableBackKey()", null);
            // webView.LoadUrl("javascript:window.requestIsEnableBackKey();"); ←これでもOKっぽい
        });
    };
}

ここまでの処理で、Forms 側で await webViewEx.IsEnableBackKeyAsync() が呼び出されたら、JavaScript の window.requestIsEnableBackKey() 関数が呼び出される。

6.JavaScriptHandler 内に onResultCanExitApp を生やす。JavaScript側 から結果が通知されるメソッドである。次のように。

[Export]
[Android.Webkit.JavascriptInterface]
public void onResultIsEnableBackKey(bool value)
{
    isEnableBackKeyComp?.SetResult(value);
    isEnableBackKeyComp = null; // one shot
}

JavaScript 側で GawaApp.onResultIsEnableBackKey(true or false) を呼び出すと、このメソッドがコールバックされる。
TaskCompletionSource である isEnableBackKeyComp に値を設定すれば、Forms 側の await webViewEx.IsEnableBackKeyAsync() の結果が返される。

7.JavaScript 側はきっとこんな感じ

window.requestIsEnableBackKey = () => {

  // 何かの処理

  GawaApp.onResultIsEnableBackKey(true or false);
}

途中まで実装したけど、面倒になって(JavaScript→C# に単方向の方がシンプルでいいやと思って)やめちゃったので、アイデアだけ残しておきます。

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

Androidのヒンジ角度センサに関しての補足

はじめに

  • ヒンジ角度センサに関する記事をすでに投稿しましたが、いくつか積み残しのタスクがありました。
  • ということで、今回は積み残しのそれぞれに対応してみます。

2番目のヒンジ角度センサの値を取得する

こんな感じで変更を加えました。

  • Before
val hingeSensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
sensorManager.registerListener(this,hingeSensor,SensorManager.SENSOR_DELAY_NORMAL)
  • After
val hingeSensors = sensorManager.getSensorList(Sensor.TYPE_HINGE_ANGLE)
for (hingeSensor in hingeSensors) {
    sensorManager.registerListener(this, hingeSensor, SensorManager.SENSOR_DELAY_NORMAL)
}
  • なんてことはありませんでした。複数のセンサがあるのだから、それぞれを登録する、というただそれだけですね。

ただし、onSensorChangedで値を得て、何かをする際に意識すべきこととして、
値を上げてきたセンサはどちらのセンサか?という点があります。

前回の記事のログより、センサの情報は以下の様になるので、区別する方法はnameくらいしかなさそうです。このため、センサの値を集める時は、nameもセットにする必要がありそうです。

{Sensor name="Goldfish hinge sensor0 (in degrees)", vendor="The Android Open Source Project", version=1, type=36, maxRange=360.0, resolution=1.0, power=3.0, minDelay=10000}, 
{Sensor name="Goldfish hinge sensor1 (in degrees)", vendor="The Android Open Source Project", version=1, type=36, maxRange=360.0, resolution=1.0, power=3.0, minDelay=10000},

Postureを扱う

※JetpackのWindow Managerは2020/7/10時点ではリリースされたばかりですのでご注意ください。

現時点の端末の折りたたみの状態(Posture)を取得するためには、DeviceState.getPosture()を使います。

上記のAPIを使う部分がわかりづらかったので、WindowManagerの使い方含めてサンプル実装してみました。
ボタンをタップするとその時点での端末のPostureの値を取得する様な処理になります。

WindowManagerの利用設定(dependencyを設定すれば良い)

dependencies {
    implementation "androidx.window:window:1.0.0-alpha01"
}

WindowManagerの取得

class MainActivity : AppCompatActivity(), SensorEventListener {

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

        val wm = WindowManager(this, null)
        // ... (略)
    }

WindowManagerコンストラクタの第二引数であるWindowBackendは端末の状態などを取得しようとした時に、特殊な処理を入れる場合に指定します。
デフォルトはnullでよいです。

DeviceState.getPosture()の呼び出し

        findViewById<Button>(R.id.getPosture).setOnClickListener { view ->

            val posture = wm.deviceState.posture
            Log.d("POSTURE_TEST", posture.toString())
        }

例えばヒンジの角度を変え、開いている状態なら3、半開きなら2、閉じているなら1などとpostureの値を取得できます。

Postureの変更を検知して処理を実行する

Postureの変更を検知する場合は、WindowManager#registerDeviceStateChangeCallbackにExecutorと変更検知の際のコールバックを登録してあげればよいです。

例えば、登録ボタンタップ時に変更の検知を開始して、解除ボタンタップ時に検知を終了する様な処理については以下の通りです。
実装の際は、registerを複数回実行を避ける様にしてください。

        val mainThreadExecutor = Executor { Handler(Looper.getMainLooper()).post(it) }

        val deviceStateChangeCallback = Consumer<DeviceState> {
            Log.d("CHANGE_POSTURE", it.posture.toString())
        }

        findViewById<Button>(R.id.registerChangePosture).setOnClickListener { view ->
            Log.d("CHANGE_POSTURE", "register")
            wm.registerDeviceStateChangeCallback(mainThreadExecutor,
                deviceStateChangeCallback)
        }

        findViewById<Button>(R.id.unregisterChangePosture).setOnClickListener { view ->
            Log.d("CHANGE_POSTURE", "unregister")
            wm.unregisterDeviceStateChangeCallback(deviceStateChangeCallback)
        }

参考

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