Androidの通知領域でRemoteViewsの支配から卒業してアニメーションするには

[このブログ](http://bhind.site/info/2749/)のコピーです。

## 概要
iOS10ではリッチ通知が実装され、[動画も通知領域に表示（ただし音声のみ）](http://bhind.site/info/2744/)できるようになったのに、Androidができないだなんて不公平だ！と思って、人類解放戦線を戦うことを決意しました。
人類を支配しているのは[RemoteViews](https://developer.android.com/reference/android/widget/RemoteViews.RemoteView.html)。支配からの開放なるか…！
…とか言いながら実は既に先人の知恵が。主にApp Widgets界の話なのだけど、RemoteViewsと戦うことには変わりなし。
[Animating Android home-screen widget](https://andytsui.wordpress.com/2010/06/06/animating-android-home-screen-widget/)

## 環境
- macOS Sierra 10.12.3
- Android Studio 2.2(2.3にするとGradleがﾀﾋぬので…
- Android 6.0

## 構成
描画部分は[Animating Android home-screen widget](https://andytsui.wordpress.com/2010/06/06/animating-android-home-screen-widget/)のとおりRemoteViews#setImageViewBitmapにBitmapを指定して、フレーム毎描画するという力技。
描画リソースはアニメーションGifとしてそのパーサを以下から拝借。
[AndroidでGIFアニメーションが動いたんだもんね（バグフィックス）](http://d.hatena.ne.jp/tomorrowkey/20100426/1272267480)
redraw部分はWidgetsのようにイベントがとんでこないのでループするためだけのServiceを実装しました。
リモートPushするのがめんどかったので、ローカルプッシュで実装しました。
コードは以下の通りです。

## コード
```ｘｍｌ:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.xxxxx.animationgifnotificationsample">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
...
        </activity>
        <service android:name=".ImageProcService"/>
    </application>

</manifest>
```

```java:MainActivity.java
package com.xxxx.animationgifnotificationsample;

...

public class MainActivity extends AppCompatActivity implements View.OnClickListener, IRedrawable {

    public static final String IMAGE_PROC_ACTION = "com.xxxx.animationgifnotificationsample.IMAGE_PROC_ACTION";
    public static final String IMAGE_FINISH_ACTION = "com.xxxx.animationgifnotificationsample.IMAGE_FINISH_ACTION";
    private static final int TIME_PERIOD = 30000;
    private static final int TIME_DELAY = 100;
    private static final int NOTIFICATION_ID = 0;

    private Context mContext;
    private BroadcastReceiver mReceiver;
    private IntentFilter mFilter;
    private NotificationCompat.Builder mBuilder;
    private GifDecoder mDecoder;
    private RemoteViews mRemoteViews;
    private NotificationManagerCompat mManager;
    private Bitmap mCanvas;
    private int mFrameCount;

....
    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button_notification:
                this.sendNotification();
                break;
        }
    }
    private void sendNotification() {
        try {
            this.mDecoder = new GifDecoder();
            this.mDecoder.read(this.mContext.getResources().openRawResource(R.raw.nyancat));
        } catch (Throwable e) {
            // TODO
            Log.d("xxxx", e.toString());
            return;
        }
        this.mBuilder = new NotificationCompat.Builder(mContext);
        this.mBuilder.setSmallIcon(R.mipmap.ic_launcher);
        this.redrawImage();
        this.setRedrawSchedule();
    }

    /**
     * IRedrawable
     */
    synchronized public void redrawImage() {
        this.mCanvas = this.mDecoder.getFrame(this.mFrameCount);

        this.mRemoteViews = new RemoteViews(this.mContext.getPackageName(), R.layout.activity_notification);
        this.mRemoteViews.setImageViewBitmap(R.id.gifView, this.mCanvas);
        Notification notification = this.mBuilder.build();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            notification.bigContentView = this.mRemoteViews;
        }

        this.mManager = NotificationManagerCompat.from(mContext.getApplicationContext());

        this.mManager.notify(NOTIFICATION_ID, notification);
        this.mFrameCount = ++this.mFrameCount % this.mDecoder.getFrameCount();

    }
    private void setRedrawSchedule() {
        Intent intent = new Intent(this, ImageProcService.class);
        this.mReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                final IRedrawable redrawable = (IRedrawable) mContext;
                switch(intent.getAction()) {
                    case MainActivity.IMAGE_PROC_ACTION:
                        redrawable.redrawImage();
                        break;
                    case MainActivity.IMAGE_FINISH_ACTION:
                        redrawable.finish();

                }
            }
        };
        this.mFilter = new IntentFilter();
        this.mFilter.addAction(IMAGE_PROC_ACTION);
        this.mFilter.addAction(IMAGE_FINISH_ACTION);
        this.registerReceiver(this.mReceiver, this.mFilter);
        intent.putExtra("loop_period", TIME_PERIOD);
        intent.putExtra("loop_interval", TIME_DELAY);
        this.startService(intent);
    }

    /**
     * IRedrawable
     */
    public void finish() {
        this.unregisterReceiver();
    }
    synchronized private void unregisterReceiver() {
        if(this.mReceiver != null) {
            this.unregisterReceiver(this.mReceiver);
            this.mReceiver = null;
        }

    }
}
```
```java:IRedrawable.java
package com.xxxx.animationgifnotificationsample;

public interface IRedrawable {
    void redrawImage();
    void finish();
}
```
```java:ImageProcService.java
package com.xxxx.animationgifnotificationsample;

...

public class ImageProcService extends IntentService {
    static final private int DEFAULT_LOOP_PERIOD = 10000;
    static final private int DEFAULT_LOOP_INTERVAL = 100;
    public ImageProcService(String name) {
        super(name);
    }
    public ImageProcService() {
        super("ImageProcService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        final long now = System.currentTimeMillis();
        long endtime = now + DEFAULT_LOOP_PERIOD;
        long interval = DEFAULT_LOOP_INTERVAL;
        final Bundle extras = intent.getExtras();
        final Integer _loop_period_buf = (Integer)extras.get("loop_period");
        if(_loop_period_buf != null) {
            endtime = now + _loop_period_buf;
        }
        final Integer _loop_interval_buf = (Integer)extras.get("loop_interval");
        if(_loop_interval_buf != null) {
            interval = _loop_interval_buf;
        }

        while(System.currentTimeMillis() < endtime) {
            try {
                Thread.sleep(interval);

                final Intent broadcastintent = new Intent();
                broadcastintent.setAction(MainActivity.IMAGE_PROC_ACTION);
                this.getBaseContext().sendBroadcast(broadcastintent);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}
```

## 結果
できたー。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">RemoteViewsの支配から人類が自由を勝ち取った瞬間の映像です <a href="https://twitter.com/hashtag/%E7%B5%B6%E5%AF%BEreject%E3%81%95%E3%82%8C%E3%82%8B?src=hash">#絶対rejectされる</a> <a href="https://t.co/bzEKKvbiLA">pic.twitter.com/bzEKKvbiLA</a></p>&mdash; あおれい - 月が綺麗かどうかおいていま (@bhind13) <a href="https://twitter.com/bhind13/status/847367925392588800">2017年3月30日</a></blockquote>

## ハマったこと・課題など
- Android Engineerには常識だとはおもうのだけど、RemoteViewsに自分で作ったViewクラス派生のGifAnimationView的なのをRemoteViewsに何回も何回もCouldn't Expand RemoteViews 言われたのがきっかけ
- ViewFlipperでもできるらしいけどやってない
- ちなみにリモートPushでやっても反映の際にローカルNotifyしないとだめだから非現実的

## github
https://github.com/bhind/AnimationGifNotificationSample

## 参考
- [Animating Android home-screen widget](https://andytsui.wordpress.com/2010/06/06/animating-android-home-screen-widget/)
- [AndroidでGIFアニメーションが動いたんだもんね（バグフィックス）](http://d.hatena.ne.jp/tomorrowkey/20100426/1272267480)