PlaybackTransportControlGlueを理解する

PlaybackTransportControlGlueというクラスをご存知でしょうか？
最近AndroidTV用のアプリを開発したことがある方なら知っているかもしれません。

AndroidTVでは`com.android.support:leanback-v17`というライブラリを使用するのですが、そのライブラリの`v27.0.0`から追加された、比較的新しいクラスです。

現在このクラスを使用して開発しているのですが、あまりに記事が少ないため実装して得た知見をまとめておこうと思います。

## PlaybackTransportControlGlueとは

![glue_side_by_side.png](https://qiita-image-store.s3.amazonaws.com/0/82592/b55f821b-7462-fbe4-0eaa-4f8d90051053.png)

右側の透過されたコントローラを表現するクラスです。
左側のやつはAndroidTVを持っている人なら一度は見たことあるかもしれません。一般的なコントローラです。

## できること

- Titleを設定できる
- Subtitleを設定できる
- 再生・停止・リピート・言語設定等のアクションを設定できる（PrimaryAction）
- PIP・Goodボタン・Badボタン等を設定できる（SecondaryAction）
- シークが簡単に実装できる
- 関連するビデオ一覧を設定できる

![0*EdN-twJCF4O-kSqF..png](https://qiita-image-store.s3.amazonaws.com/0/82592/1a37eb1b-f162-3b5a-f159-9df5952af251.png)


## できないこと

- 上記できる枠を超えたことをしたい場合、苦労する。（どうしてもやりたい場合は覚悟が必要）

## そもそもGlueってなに？
Glue = のり
今回はfragmentとplayerをつなぐもの

## なんのためにあるの？

再生に必要なアクションを管理するため。

## 実装方法

下記の通りにUIとPlayerをGlueを使ってつなげる。

![leanback-control-glue.png](https://qiita-image-store.s3.amazonaws.com/0/82592/faab08ef-e28b-63d0-31bb-9c7fe48824ef.png)


## インスタンス作成

AdapterをGlueに渡して、Hostを設定する。
```
mPlayerGlue = new VideoPlayerGlue(getActivity(), mPlayerAdapter, mPlaylistActionListener);
mPlayerGlue.setHost(new VideoFragmentGlueHost(this));
```

## Titleを設定する

`playerGlue.setTitle("Leanback team at work");`

## Descriptionを設定する

`playerGlue.setSubtitle("Leanback artist");`

## 再生・停止・リピート・言語設定等のアクションを設定する（PrimaryAction）

Glueクラスのコンストラクタでインスタンス化したActionを`onCreatePrimaryActions()`内でadapterにセットする

```
   @Override
   protected void onCreatePrimaryActions(ArrayObjectAdapter adapter) {
       // Order matters, super.onCreatePrimaryActions() will create the play / pause action.
       // Will display as follows:
       // play/pause, previous, rewind, fast forward, next
       //   > /||      |<        <<        >>         >|
       super.onCreatePrimaryActions(adapter);
       adapter.add(mSkipPreviousAction);
       adapter.add(mRewindAction);
       adapter.add(mFastForwardAction);
       adapter.add(mSkipNextAction);
   }
```

### クリックイベントの受け取り

Glueクラスの`onActionClicked()`で受け取る(SecondaryActionも同じ方法)

```
   @Override
   public void onActionClicked(Action action) {
       if (shouldDispatchAction(action)) {
           dispatchAction(action);
           return;
       }
       // Super class handles play/pause and delegates to abstract methods next()/previous().
       super.onActionClicked(action);
   }
```

## PIP・Goodボタン・Badボタン等を設定する（SecondaryAction）

Glueクラスのコンストラクタでインスタンス化したActionを`onCreateSecondaryActions()`内でadapterにセットする

```
   @Override
   protected void onCreateSecondaryActions(ArrayObjectAdapter adapter) {
       super.onCreateSecondaryActions(adapter);
       adapter.add(mThumbsDownAction);
       adapter.add(mThumbsUpAction);
       adapter.add(mRepeatAction);
   }
```

### 設定可能なアクション一覧

こちらを参照
https://developer.android.com/reference/android/support/v17/leanback/widget/Action.html

## シークが簡単に実装できる

![1*dtGGBNKg6fQ6ov3UPld01A.gif](https://qiita-image-store.s3.amazonaws.com/0/82592/92d3ecb6-d169-e9fa-e53a-6cb294292503.gif)

上記のようなサムネイル付き画像付きシークが簡単に実装できる。

### 実装方法

PlaybackSeekDataProviderを作成して、Glueにセットする

```
public class PlaybackSeekDiskDataProvider extends PlaybackSeekAsyncDataProvider {

    final Paint mPaint;
    final String mPathPattern;
    PlaybackSeekDiskDataProvider(long duration, long interval, String pathPattern) {
        mPathPattern = pathPattern;
        int size = (int) (duration / interval) + 1;
        long[] pos = new long[size];
        for (int i = 0; i < pos.length; i++) {
            pos[i] = i * duration / pos.length;
        }
        setSeekPositions(pos);  //Seek可能な幅を設定する
        mPaint = new Paint();
        mPaint.setTextSize(16);
        mPaint.setColor(Color.BLUE);
    }

    protected Bitmap doInBackground(Object task, int index, long position) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException ex) {
            // Thread might be interrupted by cancel() call.
        }
        if (isCancelled(task)) {
            return null;
        }
        String path = String.format(mPathPattern, (index + 1));
        if (new File(path).exists()) {
            return BitmapFactory.decodeFile(path);
        } else {
            Bitmap bmp = Bitmap.createBitmap(160, 160, Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bmp);
            canvas.drawColor(Color.YELLOW);
            canvas.drawText(path, 10, 80, mPaint);
            canvas.drawText(Integer.toString(index), 10, 150, mPaint);
            return bmp;
        }
    }

```

シークの幅で画像が決まっている以上、動的に幅を変更することはできなそう。
例えば、リモコンのロングタップで早送りの幅を変えるような実装のこと。（再設定は可能）


### 関連するビデオ一覧を設定できる

Glueクラスのコントローラに設定するRowAdapterを作成、アイテムをセットする。

```
private ArrayObjectAdapter initializeRelatedVideosRow() {
       ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
       presenterSelector.addClassPresenter(
               mPlayerGlue.getControlsRow().getClass(), mPlayerGlue.getPlaybackRowPresenter());
       presenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter());

       ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(presenterSelector);

       rowsAdapter.add(mPlayerGlue.getControlsRow());

       HeaderItem header = new HeaderItem(getString(R.string.related_movies));
       ListRow row = new ListRow(header, mVideoCursorAdapter);
       rowsAdapter.add(row);

       setOnItemViewClickedListener(new ItemViewClickedListener());

       return rowsAdapter;
   }
```

## 辛いところ

### 00:00にシークすることができない

指摘したところSupportライブラリチームが修正してくれたので、まもなく公開されるはず。。。

### 簡単にシークバーを出したり消したりできない

`adb shell dumpsys activity top`を実行してビューの構成を確かめた後、`findViewById`で参照を取得する必要がある

とりあえずこんな感じシークバーの表示・非表示を制御できる。

```
view?.let { 
           it.findViewById<View>(R.id.playback_progress)?.apply { this.visibility = visibility }
           it.findViewById<View>(R.id.current_time)?.apply { this.visibility = visibility }
           it.findViewById<View>(R.id.separate_time)?.apply { this.visibility = visibility }
           it.findViewById<View>(R.id.total_time)?.apply { this.visibility = visibility }
           }
      }
```

### PlaybackFragmentを呼び出した時点で配信パスが決定していないとUIが崩れる

詳細画面から配信パスを渡して再生するような設計になっている模様。
プレイヤー画面を起動してからAPI等で配信パスを受け取りたい場合は`onCreateView`で空のAdapterと空のGlueを設定後、再度設定することで回避できる。

## 終わりに

いかがでしょうか？PlaybackTransportControlGlueはAndroidTVのアプリでもまだ導入しているアプリが少ないので狙い目だと思います。
ぜひこの機会に使ってくれる人が増えて、もっと使いやすいライブラリになっていけばと思います。