20200320のAndroidに関する記事は7件です。

AndroidのDozeを回避してみようと思った話(回避できたとは言っていない)

はじめに

DozeはAndroid6から導入された素晴らしいバッテリー消費抑制機能です。

内容としては、バッテリー駆動かつ、静止状態かつ、画面OFFの状態が一定時間続くとで端末をスヤァーっとお眠りさせてバックグラウンド処理をさせなくするものです。

無法地帯と化していたバックグラウンドでのアプリの処理に制限をかけた事によりバッテリーの消費量は大幅に減ったと思います。

一般ユーザにはありがたい機能ですね。

しかし、企業向けアプリにとってはちょっと厄介な機能だと思っています。

顧客からバックグラウンドで位置情報やセンサーデータを取り続けたいんだーと言われたらやるしかありません。

そこで今回は、色々な方法でDozeを回避できるか試してしいこうと思います。

施策

駄目だった施策も交えつつ一つずつ試した結果を説明します。

施策1:フォアグラウンドサービスで処理を行う

結論から言いますが、駄目です。

制限されるのはバックグラウンド処理だけだからフォアグラウンドとして動作するフォアグラウンドサービスは大丈夫じゃないか?と思ったら大間違いです。きっちりDozeの対象です。

フォアグラウンドサービスにすることによってOSからプロセスをキルされることはほぼなくなりますが、それだけです。

https://qiita.com/naoi/items/03e76d10948fe0d45597

施策2:ホワイトリストに入れてDozeの条件から除外する

結論から言いますが、駄目です。

設定→電池→メニュー開く→電池の最適化で表示されるホワイトリストにアプリを追加するとDozeの影響をまったく受けないと思われるかもしれませんが、そうでもありません。

公式ドキュメントにも書いてある通り「このホワイトリストに含まれるアプリは、Doze モード中やアプリ スタンバイ中でもネットワークの使用が可能で、ネットワークの使用が可能で、部分的な wake locksを保持することができます。ただし、ホワイトリストに含まれるアプリにも、他のアプリと同様に他の制限は適用されます。」

https://developer.android.com/training/monitoring-device-state/doze-standby?hl=ja#support_for_other_use_cases

AlarmManagerは抑制されるしGPSなども取得できなくなります。

施策3:TimerやAlarmManagerで定期的に画面をONにする

結論から言いますが、駄目になりました。

Android10からバックグラウンドプロセスからのActivity起動に制限が追加されたためActivityを起動して画面をONにすることができなくなりました。

https://developer.android.com/guide/components/activities/background-starts?hl=ja

Android9まではこの方法でDozeを回避可能でした。抜け道をAndroid10で潰したということですね。

ただ、Android10で追加されたこの制約は一般ユーザにはメリットです。

ある動画再生アプリが、アプリを利用していない時にも一定間隔で広告画面を全画面表示するというスパム行為をしていたのですが、そのような迷惑行為をできなくする効果があります。

施策4:Push受信時に通知を全画面Intentで表示する

結論から言いますが、Android10以上だと微妙ですがこの方法しかもはや方法が残っていません。

電話アプリが利用する全画面での通知表示機能を利用してActivityを起動し、そのActivityで画面をONにします。

Activityは画面をONにしたあとすぐに終了させればユーザの操作には問題ありません。

全画面での通知表示をする条件を画面OFF+バッテリー駆動状態にしておけば、無駄な起動もありません。

MyFirebaseMessagingService.kt
        /* push通知受信時処理は省略 */

        val fullScreenIntent = Intent(context, TransparentActivity::class.java)
        val fullScreenPendingIntent = PendingIntent.getActivity(context, 0,
            fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT)

        val builder: NotificationCompat.Builder =
            createNotificationCompatBuilder(context)
                .setSmallIcon(R.drawable.ic_notification)
                .setContentTitle(getString(R.string.app_name))
                .setContentText("端末を強制的にWakeupさせました")
                .setWhen(System.currentTimeMillis())
                .setAutoCancel(true)
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setCategory(NotificationCompat.CATEGORY_CALL)// 着信画面じゃないけど着信カテゴリを設定・・・
                .setFullScreenIntent(fullScreenPendingIntent, true)// 全画面インテントを設定

TransparentActivity.kt
        // 画面ON処理(パーミッションの記載をAndroidManifestに忘れないように)
        wakeLock =
            (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
                newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK
                        or PowerManager.ACQUIRE_CAUSES_WAKEUP, "MyApp::MyWakelockTag").apply {
                    acquire(1000)
                }
            }

AndroidManifest.xml
    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

これをPush通知を受けるたびに実行するだけです。送信処理後すぐに端末が受け取るためにはPush通知は重要で送信する必要があります。

ここまですればこのアプリだけDozeを回避することが可能です。というかPush通知を利用している時点で定期的な処理が数十秒可能になります。

本来は画面を強制的にONにすることによって、他のアプリもすべてDozeの影響を受けなくしたかったのですが、Android10, Pixel3aで試したところDoze状態になったログが残りました。

しかし、定期的なPush通知の受信も画面ONもできていたためPushを受信しているアプリは問題なくバックグラウンド処理ができているようです。

なんとも微妙な結果ですが、仕方ありません。Android10からアプリごとにDozeの条件が管理されるようになったのかもしれません。

自分で試してみたい方へ

Dozeは、 端末メーカーによって微妙に挙動が変わるためこの結果が確実ではない可能性があります。

信じられない場合は、自身でサンプルアプリを作って試すことをおすすめします。

また、施策4だけは、下記アプリを入れて確認ができます。

Doze Buster
https://play.google.com/store/apps/details?id=jp.nittan.dozebuster&hl=jp

Doze状態になっているのかどうかのログを残すだけにも利用できるのでどうぞお使いください。

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

ActivityResultContractのコードを少し読んでみる

Activity 1.2.0-alpha02 と Fragment 1.3.0-alpha02で追加されたAPIのようで、型安全にstartActivityForResultを扱えるようです。またテストもできるようです。

コード例
AppCompatActivityやFragmentでprepareCall()でlauncherを用意して、launchを呼ぶとstartActivityForResultを行うことができるようです。

MainActivity.kt
val intent = Intent(this, SecondActivity::class.java)
val launcher: ActivityResultLauncher<Intent> = prepareCall(
        ActivityResultContracts.StartActivityForResult()
) { activityResult: ActivityResult ->
    Log.d("MainActivity", activityResult.toString())
    //  D/MainActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }}
}

fab.setOnClickListener { view ->
    launcher.launch(intent)
}
SecondActivity.kt
class SecondActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setResult(Activity.RESULT_OK, Intent().putExtra("my-data", "data"))
        finish()
    }
}

コードを剥がしていく

ちょっと魔法みたいなので、中身を覗いて、今のコードに適応させてみましょう。

ActivityResultContractを直接使ってみる

ActivityResultContracts.StartActivityForResultクラスは以下のように ActivityResultContractを継承しています。

ActivityResultContracts.java
public class ActivityResultContracts {
    private ActivityResultContracts() {}
...
    public static class StartActivityForResult
            extends ActivityResultContract<Intent, ActivityResult> {

        @NonNull
        @Override
        public Intent createIntent(@NonNull Intent input) {
            return input;
        }

        @NonNull
        @Override
        public ActivityResult parseResult(int resultCode, @Nullable Intent intent) {
            return new ActivityResult(resultCode, intent);
        }
    }

そのためActivityResultContractを使って書き直して以下と同じです。
ここでは型引数としてIntentとActivityResultクラスをとっています。このように自分でstartActivityのときに使いたい引数の型とActivityResultで受け取る型も型引数で指定して書いていくことができます。

MainActivity.kt
val launcher: ActivityResultLauncher<Intent> = prepareCall(
        // ** ↓ **
        object : ActivityResultContract<Intent, ActivityResult>() {
            override fun createIntent(input: Intent): Intent {
                return input
            }

            override fun parseResult(resultCode: Int, intent: Intent?): ActivityResult {
                return ActivityResult(resultCode, intent)
            }
        }
        // ** ↑ **
) { activityResult: ActivityResult ->
    Log.d("MainActivity", activityResult.toString())
    //  D/MainActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }}
}

ActivityResultRegistryを直接使ってみる

またComponentActivity.prepareCallは以下のように、mActivityResultRegistryにregisterActivityResultCallback()しています。

ComponentActivity.java
    @NonNull
    @Override
    public <I, O> ActivityResultLauncher<I> prepareCall(
            @NonNull ActivityResultContract<I, O> contract,
            @NonNull ActivityResultCallback<O> callback) {
        return prepareCall(contract, mActivityResultRegistry, callback);
    }

    @NonNull
    @Override
    public <I, O> ActivityResultLauncher<I> prepareCall(
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultRegistry registry,
            @NonNull final ActivityResultCallback<O> callback) {
        return registry.registerActivityResultCallback(
                "activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
    }

    @NonNull
    public ActivityResultRegistry getActivityResultRegistry() {
        return mActivityResultRegistry;
    }

そのためComponentActivity.getActivityResultRegistry()をしてActivityResultRegistry.registerActivityResultCallback()を利用すると以下のように書くことができます。

MainActivity.kt
        // ** ↓ **
val launcher: ActivityResultLauncher<Intent> = activityResultRegistry
        .registerActivityResultCallback(
                "activity_rq#0", // ←この数字は本来は呼び出すたびにincrementが必要
        // ** ↑ **
                object : ActivityResultContract<Intent, ActivityResult>() {
                    override fun createIntent(input: Intent): Intent {
                        return input
                    }

                    override fun parseResult(resultCode: Int, intent: Intent?): ActivityResult {
                        return ActivityResult(resultCode, intent)
                    }
                }
        ) { activityResult: ActivityResult ->
            Log.d("MainActivity", activityResult.toString())
            //  D/MainActivity: ActivityResult{resultCode=RESULT_OK, data=Intent { (has extras) }}
        }

registerActivityResultCallback(key, ActivityResultCallback, ActivityResultContract)を使うとActivityResultRegistoryが持っているHashMapにputされます

ComponentActivity周りのかんたんな構造

単純にActivityがActivityResultRegistoryを持っていて、ActivityResultRegistoryがActivityResultContractとActivityResultCallbackを持っているようです。
このHashMapのキーはregisterActivityResultCallbackに渡される "activity_rq#" + mNextLocalRequestCode.getAndIncrement()などになっています。
image.png

今まであったrequest codeはどうなったか?

今まではstartActivityForResultを呼び出すときに呼び出しと返却を識別するために使うrequest codeというのが必要でしたが、それが利用するときに必要なくなりました。こちらもAutoIncrementして管理しています。また、プロセスが死んでも再度使えるようにonSaveInstanceStateでrequest codeとActivityResultRegistoryのkeyのペアを管理して、保存してうまくやってくれるようです

ActivityResultRegistry.java
    private int registerKey(String key) {
        Integer existing = mKeyToRc.get(key);
        if (existing != null) {
            return existing;
        }
        int rc = mNextRc.getAndIncrement();
        bindRcKey(rc, key);
        return rc;
    }

Fragmentでメモリリークしないのか?

Fragment.prepareCall()の実装を見るとFragmentでON_CREATEになったときにgetActivity().getActivityResultRegistry().registerActivityResultCallbackします。Activityのスコープの変数にFragmentで作られたコールバックを突っ込むため、
これだけ見るとリークしてそうに見えます。

Fragment.java
    public <I, O> ActivityResultLauncher<I> prepareCall(
            @NonNull final ActivityResultContract<I, O> contract,
            @NonNull final ActivityResultCallback<O> callback) {
...
        getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner,
                    @NonNull Lifecycle.Event event) {

                if (Lifecycle.Event.ON_CREATE.equals(event)) {
                    ref.set(getActivity()
                            .getActivityResultRegistry()
                  // **ここでregisterActivityResultCallback**
                            .registerActivityResultCallback(
                                    key, Fragment.this, contract, callback));
                }
            }
        });

        return new ActivityResultLauncher<I>() {
            @Override
            public void launch(I input) {
...
            }
        };
    }

しかし、以下でLifecycleOwnerとして渡しています。

Fragment.java
registerActivityResultCallback(
                                    key, Fragment.this  // **←ここでLifecycleOwnerとして渡す**

また、以下のようにON_DESTROYでunregisterActivityResultCallbackするため問題なさそうに見えます。

ActivityResultRegistry.java
       lifecycle.addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner lifecycleOwner,
                    @NonNull Lifecycle.Event event) {
                if (Lifecycle.Event.ON_DESTROY.equals(event)) {
                    unregisterActivityResultCallback(key);
                }
            }
        });

まとめ

  • ActivityResultContractに型引数を渡すことで、渡す方と受け取る型を指定していけそう。
  • ComponentActivity.ActivityResultRegistryでコールバックなどは管理されている。
  • この部分に関してFragmentのリークは気にしなくても良さそう。

参考

https://android-review.googlesource.com/c/platform/frameworks/support/+/1233518
https://developer.android.com/training/basics/intents/result
https://medium.com/@star_zero/activityresultcontract%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9Factivity%E9%96%93%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E5%8F%97%E3%81%91%E6%B8%A1%E3%81%97-17052bfafde0

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

Android (Kotlin) でブロードキャスト送受信の仕方を理解する

きっかけ

最近、Android 開発を仕事で行っていますが、複数のアプリ間連携の方法を今まで知りませんでした。
最近知ったのですが、IntentBroadcastReceiverを使用することで簡単にアプリ間連携が可能でした。

しかし、まったくの初心者であった自分にとっては???でした。。
そこで今回は 2 つのアプリを連携させるIntentの使い方について学ぶことができたので、共有します。

環境

  • Android Studio 3.6.1
  • Android 7.0 以降

概要

1 つの Android 端末に 2 つのアプリケーションをインストールし、その 2 つのアプリ間の連携方法について記述します。
今回作成するアプリは以下になります。

IntentBroadcast アプリ

  • Intentを Android 端末アプリケーション内の全てのアプリにブロードキャストするアプリケーション

IntentReceiver アプリ

  • ブロードキャストされたIntentを受信するアプリケーション

1. IntentBroadcast アプリについて

Activity 画面にボタンを配置し、そのボタンをタップすることでIntentを送信します。
ブロードキャストの方法は、以下のようにIntentインスタンスを作成し、sendBroadcast(intent)を実行することで可能です。

MainActivity.kt
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

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

        // ボタン1配置
        button1.setOnClickListener {
            // インテントをブロードキャスト
            val intent = Intent("com.example.broadcast.MY_NOTIFICATION1")
            intent.putExtra("data", "Notice me senpai!")
            sendBroadcast(intent)
        }

        // ボタン2配置
        button2.setOnClickListener {
            // インテントをブロードキャスト
            val intent = Intent("com.example.broadcast.MY_NOTIFICATION2")
            intent.putExtra("data", "Hello, world!")
            sendBroadcast(intent)
        }
    }
}

ここで、Intentのインスタンス作成時に以下のように Action 文字列をセットしています。
Intentを受信するレシーバはこのcom.example.broadcast.MY_NOTIFICATION1の部分を確認し、受け取るべき情報なのかを判断します。

そのため一意であればどんな文字列でも構いませんが、作法として「パッケージ名 + 任意の文字列」とするのがベターです。
(Android 端末内のアプリケーションのパッケージ名は基本的に一意であるためです。)

また、IntentputExtra()を利用することで情報を追加登録することができます。
今回はdataという名前でそれぞれ文字列をセットしました。



ちなみに以下のactivity_main.xmlstrings.xmlは画面にボタンを設置するために記述しているだけで、ブロードキャスト処理には関係ありません。
上記のソースを動かす際のサンプル用として参考してください。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="50dp"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button1" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button2" />
</LinearLayout>
strings.xml
<resources>
    <string name="app_name">IntentBroadcast</string>
    <string name="button1">Button1</string>
    <string name="button2">Button2</string>
</resources>

2. IntentReceiver アプリについて

こちらは Android 端末内にブロードキャストされているIntentを受信します。
受信の方法はまず、BroadcastReceiverクラスを継承した、ブロードキャストを実際に受信するクラス(MyBroadcastReceiverクラス)を作成します。

そしてメイン画面に、先ほど作成したMyBroadcastReceiverクラスを利用するレシーバを準備することでIntent受信が可能となります。

MyBroadcastReceiver.kt
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast

class MyBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        // インテントに登録されている、名前"data"に対応する文字列をトーストで表示する
        Toast.makeText(context, intent?.getStringExtra("data"), Toast.LENGTH_SHORT).show()
    }
}
MainActivity.kt
import android.content.IntentFilter
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

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

        // BroadcastReceiverを継承したクラス(MyBroadcastReceiver)を指定する
        // IntentFilterは何を呼び出すかを振り分けるもの
        // 2020/03/20: links_2_3_4 さん指摘反映
        // 画面回転時にメモリリークに関する警告表示を阻止
        this.applicationContext.registerReceiver(
            MyBroadcastReceiver(),
            IntentFilter("com.example.broadcast.MY_NOTIFICATION1")
        )
        this.applicationContext.registerReceiver(
            MyBroadcastReceiver(),
            IntentFilter("com.example.broadcast.MY_NOTIFICATION2")
        )
    }
}

ここでMainActivity.ktでは、Intentの Action 文字列によってどのブロードキャストレシーバクラスを利用するかを設定しています。
(今回は同じブロードキャストレシーバクラスMyBroadcastReceiver()を設定しています。)

そしてIntentの Action 文字列(com.example.broadcast.MY_NOTIFICATION1)が、IntentFilter()内でで記述した文字列と同じであれば、設定したブロードキャストクラス内の処理が実行されます。

ブロードキャストレシーバクラスMyBroadcastReceiver.ktでは、Intentに登録された名前dataの文字列をトーストで表示するようにしました。

挙動確認

  1. IntentBroadcast アプリと IntentReceiver アプリを Android 端末にインストールします。
  2. IntentReceiver アプリを起動し、ホームボタンを選択するなどしてバックグラウンドに移動させます。
  3. IntentBroadcast アプリを起動し、画面上に設置したボタンをタップします。

うまくブロードキャスト送信・受信ができればIntentに登録したdataの文字列がトースト表示されます。
スライド1.JPG

これで、IntentBroadcast アプリで操作した情報が IntentReceiver アプリに受け渡され、2 つのアプリ間連携ができました。

最後に

今回は、1 つのアプリケーションでIntentをブロードキャストを行い、もう 1 つのアプリケーションでIntentを受け取ってトースト表示する仕組みを紹介しました。
本記事ではとても単純なケースを紹介しましたが、このIntentを使えば、複数の Android アプリケーションでの連携が可能になります。
今後余裕があれば、このIntentのやり取りを利用した他のサンプルも実装・紹介できたらなと思います。

最後まで閲覧いただき、ありがとうございました。

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

GIFアニメーションをImageViewで表示する

はじめに

需要があるかはわからないですが、Gifアニメをこの度自プロジェクトでAndroidアプリ上で動かしたいという要望があったので調べました。

調べてみた

まずは、「Gif animation android imageview」などでググってみました。
すると・・・glideというライブラリを使うといいよ!
という記事がたくさん出てきました。

https://qiita.com/maehara08/items/eef63804c52b2e0bb404

それ以外だと、Movieクラスを使ってCanvasに一枚ずつ書き出して・・・みたいなゴリ押しパターンがありました。
https://tomorrowkey-2.hatenadiary.org/entry/20100419/1271678022

弊社では、ライブラリを使うにはちょっと時間がかかるのと、外部の信用できる会社のものでないと結構たいへんなので、glideライブラリを使うというのは避けたかったので、ゴリ押しパターンを実装してみようということになりました。

準備

Gifアニメのダウンロード

とりあえず、フリー素材のGifアニメをダウンロード。
下記のサイトからもらいました。ありがとう。
https://sozai-good.com/illust/gifanimation/

assetsフォルダの作成

とりあえず表示するだけなので、assetsにダウンロードしたGifアニメを置くことに。

projectツリーのappあたりで右クリックして
[New]->[Folder]->[Assets Folder]をすると簡単につくれます。
image.png

assetsフォルダがapp->src->mainの下にできていたら、ダウンロードしたGifアニメを入れます。
Gifアニメのファイル名は適当に。
image.png

レイアウトファイル

今回は、Activityの画面にImageViewを配置するだけにします。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

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

        <ImageView
            android:id="@+id/image_view"
            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" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Gifアニメを表示させる

Gifアニメを表示する場合、Movieがversion28から非推奨になっているため、
コードをOSごとに分ける必要があります。

Android P以上の場合

リファレンス通りに、MovieはdeprecatedなのでAnimatedImageDrawable
Movieのリファレンス
AnimatedImageDrawableのリファレンス

AnimatedImageDrawableを、assetsからGifアニメを取得して表示します。

@RequiresApi(Build.VERSION_CODES.P)
    private fun getGifAnimationDrawable(): AnimatedImageDrawable {
        val source = ImageDecoder.createSource(assets, "gif_anim_toy_poodle.gif")
        return ImageDecoder.decodeDrawable(source) as? AnimatedImageDrawable
            ?: throw ClassCastException()
    }

手順は簡単です。
1. ImageDecoder.createSourceで、第一引数にassetManager、第二引数にassetsに入っているファイル名を指定してください。
ちなみに、このImageDecoder.createSourceは他にもFileを指定したり、res & resIDを指定したり、ContentResolver & URIなど、複数のファイル取得に対応していそうです。
2. 1で取得したImageDecoder.SourceインスタンスをImageDecoder.decodeDrawable()に渡してDrawableを取得します。
3. AnimatedImageDrawableにキャストします。

取得ができたら、ImageViewに設定します。

// AnimatedImageDrawableを取得する
val drawable = getGifAnimationDrawable() 

// アニメーションをセットする
binding.imageView.setImageDrawable(drawable)

// アニメーションを開始する
drawable.start()

Android P未満の場合

Android P未満の場合だと、ImageDecoderAnimatedImageDrawableが利用できません。
なので、従来の通りMovieクラスを使った方法を一応書いておきます。

参考にしたサイトはこちらで、基本的にこちらの写経です。
https://end0tknr.hateblo.jp/entry/20120714/1342247240

カスタムのDrawableを用意する

MovieクラスでのGifアニメーション起動の方法は、onDrawでMovieを動かす方法になるので、
カスタムクラスを作る必要があります。
参考にしたサイトではViewを継承していましたが、今回はImageViewに設定したかったので、Drawableを継承しました。

CustomAnimatedDrawable.kt
class CustomAnimatedDrawable(
    private val inputStream: InputStream
): Drawable() {
    private val _movie by lazy {
        Movie.decodeStream(inputStream)
    }

    private var _movieStart = 0
    private var _loop = true
    private var _stop = false
    private var relativeMillisecond = 0

    override fun draw(canvas: Canvas) {
        canvas.apply {
            drawColor(Color.TRANSPARENT)
            scale(width / _movie.width().toFloat(),
                height / _movie.height().toFloat() )
        }
        val now = SystemClock.uptimeMillis()

        if (_movieStart == 0) {
            _movieStart = now.toInt()
        }

        relativeMillisecond = when {
            _stop -> {
                _movieStart = 0
                relativeMillisecond
            }
            _loop -> ((now - _movieStart) % _movie.duration()).toInt()
            else -> (now - _movieStart).toInt()
        }

        _movie.apply {
            setTime(relativeMillisecond)
            draw(canvas, 0f, 0f)
        }

        invalidateSelf()
    }

    override fun setAlpha(alpha: Int) {
    }

    override fun getOpacity(): Int = PixelFormat.UNKNOWN

    override fun setColorFilter(colorFilter: ColorFilter?) {}

    fun stop() {
        _stop = true
    }

    fun start() {
        _stop = false
        _movieStart = 0
    }

    fun isRunning() = !_stop
}

assetsからファイルをopenする

あとはassetsからファイルを取得して、カスタムクラスにinputStreamを渡してあげればOKです。

    private fun getGifAnimationDrawableLessThanP(): CustomAnimatedDrawable {
        val inputStream = assets.open(ANIMATION_GIF_FILE_NAME)
        return CustomAnimatedDrawable(inputStream)
    }

これでOKです。

キャプチャ

recordで動画を撮ったんですが、いつからかわからないですが、.webm方式での保存になってしまい、かつadb shell screen record..みたいなのもうまく動かないので、泣く泣くキャプチャを取るしかできませんでした。
image.png
この子がペケペケ動きます。

コード

サンプルのコードはここにおいておきました。
なんかpushの仕方間違えちゃったので一階層無駄があります。
https://github.com/keikyukyun/gif_animation_sample

最後に

久しぶりにgitにもアップしたけど、webviewなら一撃という噂も。

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

FlutterでURLSchemeを使って他のアプリ(例: Twitter)を開く(開けない場合の回避策も)

Flutterで個人開発をしている村松龍之介と申します。
(仕事ではiOSアプリのネイティブアプリ開発を行っています)

今回は、アプリに自分のTwitterアカウントのリンクを載せようと思いました。
最初は普通にURLを貼って、ブラウザで開くようにしていたのですが、アプリ入ってるならアプリが開いた方が良いよね…と思い実装した備忘録です。

URLを開くためにurl_launcherパッケージを使う

まず、URLを開くために必要な url_launcherパッケージを導入済みでなければ導入します。
url_launcher | Flutter Package

簡単に書きますと、pubspec.yamlファイルのdependenciesに1行追記します。
VS Codeですと、⌘ + S で保存すればflutter pub getが自動で走りますのでインストールできます。
Android Studioでもおそらく同じ?

dependencies:
  url_launcher: ^5.4.2 # <-導入時点で最新のバージョンで良いかと思います

他のアプリを開きたいファイル(クラス)に実装します

ここではTwitterのプロフィールページを例に用います。

url_launcherパッケージをインポート

import 'package:url_launcher/url_launcher.dart';

URLを開く

launch(url)関数で引数に設定したURLを開けます。
非同期な関数なのでawaitを使っています。

final url = 'twitter://user?screen_name=riscait' // <-Twitterアプリのユーザープロフ画面を開くURLScheme
await launch(url);

開くことのできないURLが入ってくる可能性がある場合は canLaunch(url)で調べることができます。

if (await canLaunch(url)) {
  await launch(url);
} else {
    // 任意のエラー処理
}

URLを開けなかったときのためのセカンドURLを用意

final url = 'twitter://user?screen_name=riscait' // <-Twitterアプリのユーザープロフ画面を開くURLScheme
final secondUrl = 'https://twitter.com/riscait' // <-Twitterアプリのユーザープロフ画面を開くURL

if (await canLaunch(url)) {
  await launch(url);
} else if (secondUrl != null && await canLaunch(secondUrl)) {
    // 最初のURLが開けなかった場合かつセカンドURLが有って開けた場合
  await launch(secondUrl);
} else {
    // 任意のエラー処理
}

メソッド化

/// 第2引数のURLを開く。開けないURLだった場合は第2引数のURLを開く
Future _launchURL(String url, {String secondUrl}) async {
    if (await canLaunch(url)) {
      await launch(url);
    } else if (secondUrl != null && await canLaunch(secondUrl)) {
      await launch(secondUrl);
    } else {
      // 任意のエラー処理
    }
}

ボタンで使用する一例

RaisedButton(
  child: const Text('Twitterを開く'),
  onPressed: () => _launchURL(
    'twitter://user?screen_name=riscait',
    secondUrl: 'https://twitter.com/riscait',
  ),
),

iOSのためにinfo.plistを編集する

これでAndroidではTwitterアプリがインストールされていれば開かれることを確認しました!
しかし、iOSに対応する場合にはもう一手間必要です。

Android StudioやVS Codeで編集する場合

info.plistを開きます。
下記のように1行追加します。

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>itms</string>
    <string>twitter</string> <!-- この1行を追加 -->
</array>

僕の環境下では LSApplicationQueriesSchemesが既に存在しましたが、なかった場合は、5行全部追記しましょう!

<plist version="1.0">
<dict>
<!-- この間に追記すればOK -->
</dict>

Xcodeで(Property Listとして)開いて編集する場合

LSApplicationQueriesSchemesの一要素としてtwitter等のアプリコードを追加しましょう。

以上、これでiOSでもアプリがインストールされていればアプリが開くようになります。

TwitterのURL Schemeについてはこちらのサイトに詳しく載っておりました?
【2019】Twitter公式Appのスキーム一覧 │ えぐぷと!

蛇足

ご覧いただきありがとうございました!
蛇足ですが、Flutterアプリをリリースできたので良かったらインストールしてみてもらえると嬉しいです?‍♂️
iOS: ‎「レストル-有給休暇管理」をApp Storeで
Android: レストル-有給休暇管理 - Google Play のアプリ

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

onConfigurationChangedで回転時のレイアウトを変更

やりたかったこと

ImageViewの画像を画面いっぱい(アスペクト比はそのまま)に表示してかつ、ボタンを真下に置きたかったのですが、、、
図1.png
横回転するとボタンが隠れてしまうし、横画面に合わせてレイアウト変えると縦回転したときにボタンの位置がずれてしまってました。
レイアウト用のxmlは1つだけにしたかったので、ソース内で動的にImageViewのサイズを変えることにしました。

やったこと

レイアウト

  • レイアウトはConstraintLayoutを使う
  • ImageViewの設定:
    • サイズ(縦向き時):
      • height: wrap_content
      • width: 0dp(match constraint)
    • サイズ(横向き時):
      • height: 0dp(match constraint)
      • width: wrap_content
    • 描画の縦横比を維持するためにandroid:adjustViewBounds = trueを指定
    • ボタンをくっつけるためにapp:layout_constraintVertical_chainStyle="packed"を指定

Activity

  • マニフェストで、Activityにandroid:configChanges="orientation|screenSize"を追加。縦横の回転を検出できるようにする。
  • 縦横の向きが変わるとonConfigurationChangedがコールされる。その中で、ImageViewのレイアウトを更新。

ソース

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

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        android:padding="8dp"
        app:layout_constraintBottom_toTopOf="@+id/button"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0"
        app:layout_constraintVertical_chainStyle="packed"
        app:srcCompat="@drawable/ic_launcher_background" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />

</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
    ImageView imageView;
    Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //UI取得
        imageView = findViewById(R.id.imageView);
        button = findViewById(R.id.button);
        //一応、起動時の向き取得
        int orientation = getResources().getConfiguration().orientation;
        UpdateLayout(orientation); //レイアウト更新
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        int orientation = newConfig.orientation; //向き取得
        UpdateLayout(orientation); //レイアウト更新
    }

    /**
     * レイアウト更新
     * @param orientation 向き
     */
    protected void UpdateLayout(int orientation){
        //ImageViewのLayoutParam取得
        LayoutParams param = imageView.getLayoutParams();
        if (orientation == Configuration.ORIENTATION_PORTRAIT) {
            // 縦向きの場合
            Log.d("UpdateLayout", "縦向き");
            param.height = LayoutParams.WRAP_CONTENT;
            param.width = 0;
        }
        else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
            // 横向きの場合
            Log.d("UpdateLayout", "横向き");
            param.height = 0;
            param.width = LayoutParams.WRAP_CONTENT;
        }
        imageView.setLayoutParams(param);
    }
}

参考記事

Androidで横向きか縦向きかを判定する
Androidで画像の縦横比を保ったまま画面幅いっぱいに表示する
https://techblog.zozo.com/entry/constraint_layout

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

Android単体でADBを使う。(迫真AARCH64部、Linuxの裏技)

注意

  • 完全に単体でやるには開発者向けオプションにネットワーク経由でデバッグがないと無理。
  • ネットワーク経由でデバッグがないと再起動する度tcpipを、使わざるを得なくなる…GoogleはAndroid12で改善してくれるのか!?

本編:
プロローグ
(Y)ぬわああああああん疲れたもおおおおおおおん
(M)チカレタ…
(Y)じゃけん風呂入って寝ましょーねー
(Y)おっそうだな(自問自答)
ここからベッドシーンへ…
(Y)ファッ!このアプリADB権限いるのか!?
(Y)やめたくなりますよー……
(M)あっそうだ!Androidにadbぶち込んでやれば解決だゾ〜
(Y)それができれば苦労して ないです(断言) aarch64バイナリは提供されてない、はっきりわかんだね!
(K)や!、せんぱい! aarch64バイナリがありましたよ!
(Y)ファッ!(デデドン!) クゥーン?
(M)よし!じゃあ打ち込んでやるぜ!ゾ

キューピー3分クッキングの曲の始まり

本編

今回は中に入れます!(Archlinuxの!)

なのでじゃけん環境構築していきましょーねー(ベッドで書いてるからここもベッドシーン)

Requirements

ぶち込んで!(語彙力欠損)
そのためのRDME??

Termux

FDroid (Recommend) GPlay

TermuxArch

https://github.com/TermuxArch/TermuxArch

ADB

やべぇよやべぇよ…こんなとこにaarch64のadbバイナリがぁあああ……あ、

https://github.com/qhuyduong/arm_adb/releases

こいつをwget (俺が好きなだけ。好きなラーメンの味と一緒)

あとは簡単麺(なんちって。(激寒))

  1. ネットワーク経由のデバッグをオンにする'
    1. この項目がない場合はtcpipを用いる
  2. ??⇩見て⇩???
termuxarch
chmod a+x ./adb
./adb connect ipaddr

localhostも……イキスギィ!あー満足
Screenshot_20200320-011810.png

や、やりますねぇ!

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