2018/10/19

Android: ExoPlayer DownloadManager, DownloadService

ExoPlyerのDownloadManager, DownloadServiceを調べた時のメモ.

中断されたアクションを読み込むタイミング

保存されたActionFileの読み込みタイミングはDownloadManagerを初期化したタイミングとなります.

プロセスの強制終了などでActionを完遂できなかった場合, 保存されたActionFileをプロセス再開後に読み込んでダウンロード処理を再開する必要があります.
保存されたActionFileは DownloadManager.loadActions によってバックグラウンドスレッド上で読み込まれ, これはDownloadManagerのコンストラクタで実行されます.

コンテンツの削除はバックグラウンド?

コンテンツを削除するにはremove flagをtrueにしたアクションを発行します.
アクションはDownloadServiceで実行されるので, フォアグラウンドサービスとして実行することが可能です.
削除中の通知も DownloadService.getForegroundNotification で返すNotification objectをカスタマイズすることができます.
また, 通知の雛形として DownloadNotificationUtil.buildProgressNotification が用意されています.
buildProgressNotification は引数のタスクステートに応じて通知の表示内容を変えるため, ダウンロード中通知や削除中通知もこのメソッドで生成できます.

ダウンロード進捗率を取得する

DownloadManager.Listenerの onTaskStateChanged コールバック引数のTaskStateから間近のダウンロード進捗率が取得できます.
TaskState.downloadPercentage がダウンロード進捗率を格納したフィールドです.
ダウンロード進捗率が未定/不明 あるいは 削除タスクである場合は com.google.android.exoplayer2.C#PERCENTAGE_UNSET がセットされます.
DownloadServiceでは, このコールバックを受けて通知の進捗率を更新するようになっています.

タスクの実行順序とダウンロードのキャンセル

タスクはDownloadManagerによってArrayListで管理されており, 新しいタスクはタスクキュー(タスクリスト)の最後尾に追加され, 先頭から順に実行されます.
DownloadManager.handleAction は新しいダウンロード/削除アクションアクションからタスクを生成してタスクキューの最後尾に追加します.
新しく削除アクションがリクエストされた場合, 既に同じメディアファイルのダウンロードタスクがタスクキューに存在するなら, そのダウンロードを即座にキャンセルします.
つまり, ダウンロード中やダウンロードリクエストをキューイングした後にこれをキャンセルしたい場合は削除アクションを投げるとキャンセルできます.

ダウンロードの開始条件

サービスによっては”従量制ネットワーク接続時にはダウンロードしたくない”といった要件があるかもしれません.
あるいは, “NWが瞬断されてダウンロード中断されたけれど, NW接続が回復したら自動再開したい” 要件があるかもしれません.
ExoPlayer Downloaderではデバイスの状態を監視して, こうした要件に応える機能があります.

RequirementsHelper はデバイスの状態を監視し, 特定の条件を満たした場合にダウンロードを開始/再開するヘルパークラスです. ここで指定できる”特定の条件” は Requirements クラスで表現され, Requirementsに指定できる条件は次の通りです.

  1. ネットワーク種別 ( NW接続済み, 従量制NWに接続済み, ローミング中, etc. )
  2. 充電中かどうか
  3. アイドル状態かどうか

また, JobSchedulerによる監視もサポートされています.
JobSchedulerを使用する場合は, Requirementsの情報がJobInfoに変換されてスケジューリングされます.

API Lv.によっては判定できるNW種別の種類や, アイドル状態と判定する条件に差異があるので, JobSchedulerRequirementsのコードを確認した方がよいです.

ダウンロードの開始プロセス

ダウンロードを開始するにはDownloadManagerを初期化して, DownloadServiceを起動し, タスク(アクション)を追加する必要があります.
DownloadServiceは起動されるとRequirementsHelperを起動してデバイス状態を監視し始めます.
デバイスの状態がダウンロード開始条件を満たした場合, いよいよダウンロード処理が開始されます.

RequirementsHelperとスケジューラの生存区間

アプリのプロセスが生きている間はRequirementsHelperが動的ブロードキャストレシーバーを使ってデバイス状態を監視し, ダウンロードを開始/再開させます.
デバイスの監視はDownloadServiceによって開始されますが, ダウンロードが中断されてDownloadServiceが停止してもこの監視は続きます.
これは, デバイスの状態を監視するRequirementsHelperをDownloadServiceのstaticフィールドで保持しているためです.

DownloadServiceがgetSchedulerでスケジューラを指定している場合は, スケジューラでもデバイス状態が監視されます.
スケジューラはAndroid標準のJobSchedulerを使用することができ, これによってアプリのプロセスが停止している場合にもダウンロードを開始/再開させることが可能になります.
スケジューラはダウンロード開始条件が満たされていないと判断された場合にスケジューリングされます.

ダウンロード開始条件が満たされた場合, RequirementsHelperによる監視が続いている(アプリのプロセスが生きている)状態であればRequirementsHelperがダウンロードを開始/再開させて, スケジューラのスケジューリングをキャンセルします.
RequiermentsHelperによる監視がされていない(アプリのプロセスが停止している)状態であればスケジューラによる開始/再開が行われます.

スケジューラだけでデバイス監視しないのは, RequirementsHelper(staticフィールドと動的ブロードキャストレシーバー)を使った方がデバイス状態の検知からダウンロードの開始/再開までを素早く行えるというメリットがあります.

RequirementsとSchedulerとDownloadService

RequirementsとSchedulerは, DownloadServiceのgetSchedulergetRequirementsをオーバーライドして指定します.

  @Override
  protected Requirements getRequirements() {
    return new Requirements(Requirements.NETWORK_TYPE_ANY, false, false);
  }

  @Override
  protected PlatformScheduler getScheduler() {
    return Util.SDK_INT >= 21 ? new PlatformScheduler(this, JOB_ID) : null;
  }

JobScheduler を使ったデバイスの監視を実現するため PlatformScheduler クラスが用意されています. PlatformSchedulerを使うにはAndroidManifest.xmlに次の定義を追加します.

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

<service android:name="com.google.android.exoplayer2.util.scheduler.PlatformScheduler$PlatformSchedulerService"
    android:permission="android.permission.BIND_JOB_SERVICE"
    android:exported="true"/>

あるいはFirebaseJobDispatcherを使ったスケジューラ JobDispatcherScheduler も用意されています. JobDispatcherSchedulerを使うにはAndroidManifest.xmlに次の定義を追加します.

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

  <service
      android:name="com.google.android.exoplayer2.ext.jobdispatcher.JobDispatcherScheduler$JobDispatcherSchedulerService"
      android:exported="false">
    <intent-filter>
      <action android:name="com.firebase.jobdispatcher.ACTION_EXECUTE"/>
    </intent-filter>
  </service>