FCM によるコマンドリザルトの取得

android_fcm ディレクトリのプロジェクトでは、FCM を使ってプッシュ通知を受信する処理を実装しています。ソースコードの fcm フォルダ以下の実装は、Android (FCM) プッシュ通知設定チュートリアル の手順をすべて実装した結果です。受信処理の流れは、FCM の プッシュ通知設定チュートリアル で解説しています。

ここでは、FCM のチュートリアルの結果をベースに、 Hello Thing-IF の機能を組み込む方法を説明します。

全体像

はじめに、プッシュ通知によってコマンドリザルトの取得を行うまでの全体像を示します。薄い紫色の箇所が FCM のプッシュ通知設定チュートリアルから作り替える処理です。

FCM サンプルからの変更点は以下のとおりです。

  • プッシュ通知の初期化:FCM のデバイストークンの取得と Kii Cloud へのインストールは、CommandFragment を表示するタイミングに変更します。同時に、デバイストークンをインストールする処理を Thing-IF SDK 用のものに変更します。
  • プッシュ通知の初期化処理の共通化:MyFirebaseInstanceIDService で FCM 側のデバイストークンの更新を検知した際、ブロードキャストによって MainActivity を呼び出すことで、プッシュ通知の初期化処理を共通化します。
  • メッセージの受信:MyFirebaseMessagingService でプッシュ通知を受け取った際、ブロードキャストを使って CommandFragment のブロードキャストレシーバに通知するようにします。
  • コマンド画面でのイベント受信:CommandFragment のブロードキャストレシーバでコマンドリザルトの取得処理を行います。

それぞれ、以下で詳細を説明します。

プッシュ通知の初期化

FCM を使ってプッシュ通知を初期化するには、FCM から取得したデバイストークンと、ThingIFAPI インスタンスが持つオーナーユーザーの情報を紐付ける必要があります。そのため、Hello Thing-IF では、ログイン画面からコマンド画面に切り替わるタイミングで、紐付け処理を実行します。

コマンド画面に切り替える処理は、フラグメントによる画面遷移 に示したとおり、MainActivity で 2 箇所あります。どちらも以下のように実装しています。

FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.main, CommandFragment.newInstance(mApi));
transaction.commit();

initializePushNotification();

はじめの 3 行はフラグメントをコマンド画面に切り替えるもので、すでに説明したとおりです。その直後に initializePushNotification メソッドを呼び出して FCM の初期化処理を行います。

initializePushNotification メソッドの実装は以下のとおりです。

private void initializePushNotification() {
  if (mApi == null) {
    return;
  }

  PromiseAPIWrapper api = new PromiseAPIWrapper(mAdm, mApi);
  mAdm.when(api.registerFCMToken()
  ).then(new DoneCallback<Void>() {
    @Override
    public void onDone(Void param) {
      Toast.makeText(MainActivity.this, "Succeeded push registration", Toast.LENGTH_LONG).show();
    }
  }).fail(new FailCallback<Throwable>() {
    @Override
    public void onFail(final Throwable tr) {
      Toast.makeText(MainActivity.this, "Error push registration:" + tr.getLocalizedMessage(), Toast.LENGTH_LONG).show();
    }
  });
}

ここでは、PromiseAPIWrapper の registerFCMToken メソッドを呼び出して、成功時と失敗時の結果を、それぞれトーストで表示します。

registerFCMToken メソッドの実装は以下のとおりです。

public Promise<Void, Throwable, Void> registerFCMToken() {
  return mAdm.when(new DeferredAsyncTask<Void, Void, Void>() {
    @Override
    protected Void doInBackgroundSafe(Void... voids) throws Exception {
      String fcmToken = FirebaseInstanceId.getInstance().getToken();
      if (fcmToken != null) {
        throw new IllegalStateException("FCM device token is not ready.");
      }
      mApi.installPush(fcmToken, PushBackend.GCM);
      return null;
    }
  });
}

ここでは、FCM の API を呼び出してデバイストークンを取得し、それを ThingIFAPI の installPush メソッドに渡してインストール処理を実行しています。インストール処理では、PushBackend.GCM を指定し、FCMのデバイストークンを渡していることを指定します。開発者ポータルのユーザーインターフェイスと同様、API でも FCM と GCM の区別は行っていません。

FCM のデバイストークンは、FCM SDK によってあらかじめバックグラウンドで生成されており、その結果を getToken メソッドで取得します。

通常は、デバイストークンの生成が先に終っているはずですが、間に合わなかった場合は getToken メソッドから null が返されます。null が返された場合、ここでは簡易的な実装として、IllegalStateException 例外を発生させ、Promise の FailCallback でトーストを表示します。実際のアプリでは再試行を行うか、モバイルアプリの再起動を促すエラーメッセージを表示するなどの対処が適切です。

プッシュ通知の初期化処理の共通化

FCM サーバーでは、デバイストークンが更新されることがあり、その際には、新しいデバイストークンを Kii Cloud に登録する必要があります。

デバイストークンの変更は、MyFirebaseInstanceIDService の onTokenRefresh メソッドで通知されます。変更通知があった場合は、initializePushNotification メソッドと同等のデバイスのインストール処理が必要ですが、MyFirebaseInstanceIDService は Android のサービスのため、MainActivity のメソッドを直接呼び出すことはできません。ここでは、ブロードキャストを使って MainActivity に処理を中継します。

MyFirebaseInstanceIDService では、onTokenRefresh メソッドが呼び出されたとき、TOKEN_REFRESH でのブロードキャストを実行します。

public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {
  public static final String INTENT_TOKEN_REFRESH = "com.example.pushtest.TOKEN_REFRESH";

  @Override
  public void onTokenRefresh() {
    Intent registrationComplete = new Intent(INTENT_TOKEN_REFRESH);
    LocalBroadcastManager.getInstance(this).sendBroadcast(registrationComplete);
  }
}

MainActivity では、TOKEN_REFRESH を受け取ります。onReceive() でブロードキャストを受け取ったとき、initializePushNotification メソッドを呼び出してプッシュ通知の初期化処理を行います。

public class MainActivity extends AppCompatActivity implements LoginFragment.OnFragmentInteractionListener {
  private BroadcastReceiver mTokenRefreshBroadcastReceiver;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ......
    mTokenRefreshBroadcastReceiver = new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        initializePushNotification();
      }
    };
    ......
  };
}

また、MainActivity が再開/停止されたとき、ブロードキャストレシーバ mTokenRefreshBroadcastReceiver の有効/無効を切り替える処理を行います。

@Override
protected void onResume() {
  super.onResume();
  LocalBroadcastManager.getInstance(this).registerReceiver(mTokenRefreshBroadcastReceiver,
          new IntentFilter(MyFirebaseInstanceIDService.INTENT_TOKEN_REFRESH));
}

@Override
protected void onPause() {
  LocalBroadcastManager.getInstance(this).unregisterReceiver(mTokenRefreshBroadcastReceiver);
  super.onPause();
}

メッセージの受信

FCM からメッセージが届くと、MyFirebaseMessagingService の onMessageReceived() が呼び出されます。

コマンドの送信結果は、Kii Cloud の 3 種類あるプッシュ通知(Push to App、Push to User、Direct Push)のうち、Direct Push として通知されます。その際、ペイロードの commandID パラメーターにコマンド ID が含まれています。

コマンド ID を取得する処理は以下のとおりです。

public class MyFirebaseMessagingService extends FirebaseMessagingService {
  private static final String TAG = "MyFirebaseMsgService";
  public static final String INTENT_COMMAND_RESULT_RECEIVED = "com.kii.sample.hellothingif.COMMAND_RESULT_RECEIVED";
  public static final String PARAM_COMMAND_ID = "CommandID";

  @Override
  public void onMessageReceived(RemoteMessage remoteMessage) {
    Map<String, String> payload = remoteMessage.getData();
    Bundle bundle = new Bundle();
    for (String key : payload.keySet()) {
      bundle.putString(key, payload.get(key));
    }

    ReceivedMessage message = PushMessageBundleHelper.parse(bundle);
    KiiUser sender = message.getSender();
    PushMessageBundleHelper.MessageType type = message.pushMessageType();
    switch (type) {
      case PUSH_TO_APP:
        PushToAppMessage appMsg = (PushToAppMessage) message;
        Log.d(TAG, "PUSH_TO_APP Received");
        break;
      case PUSH_TO_USER:
        PushToUserMessage userMsg = (PushToUserMessage) message;
        Log.d(TAG, "PUSH_TO_USER Received");
        break;
      case DIRECT_PUSH:
        DirectPushMessage directMsg = (DirectPushMessage) message;
        Log.d(TAG, "DIRECT_PUSH Received");
        String commandID = bundle.getString("commandID");
        if (commandID != null) {
          Intent registrationComplete = new Intent(INTENT_COMMAND_RESULT_RECEIVED);
          registrationComplete.putExtra(PARAM_COMMAND_ID, commandID);
          LocalBroadcastManager.getInstance(this).sendBroadcast(registrationComplete);
        }
        break;
    }
  }
}

bundle.getString() によって commandID が取得できると、コマンドの実行結果が Thing Interaction Framework から送信されてきたことを意味します。このことを、ブロードキャスト INTENT_COMMAND_RESULT_RECEIVED によってコマンド画面に転送します。ブロードキャストのパラメーターには commandID を含めるようにします。

コマンド画面でのイベント受信

プッシュ通知を受け取ったイベントは、ブロードキャスト経由でコマンド画面が受信します。

以下は CommandFragment でブロードキャストレシーバ mCommandResultBroadcastReceiver の有効/無効を切り替える処理です。コマンド画面がアクティブになったときに有効化し、停止するときに無効化します。

public class CommandFragment extends Fragment {
  @Override
  public void onResume() {
    super.onResume();
    LocalBroadcastManager.getInstance(getContext()).registerReceiver(mCommandResultBroadcastReceiver,
            new IntentFilter(MyGcmListenerService.INTENT_COMMAND_RESULT_RECEIVED));
  }

  @Override
  public void onPause() {
    LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mCommandResultBroadcastReceiver);
    super.onPause();
  }
}

ブロードキャストレシーバ mCommandResultBroadcastReceiver の実装は以下のとおりです。onReceive() でブロードキャストを受け取ったとき、commandID をパラメーターとして取得します。

onPushMessageReceived() の実装は、この後、コマンドリザルトの取得 で示します。

private BroadcastReceiver mCommandResultBroadcastReceiver = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    String commandID = intent.getStringExtra(MyGcmListenerService.PARAM_COMMAND_ID);
    onPushMessageReceived(commandID);
  }
};

コマンドリザルトの取得

Thing Interaction Framework からのプッシュ通知を受け取ると、最終的に、コマンド ID を引数にとって onPushMessageReceived() メソッドが呼び出されます。

ここでは、Thing Interaction Framework から、指定されたコマンド ID を持つコマンドの全体を取得し、コマンドが持つコマンドリザルトを評価します。

private void onPushMessageReceived(String commandID) {
  PromiseAPIWrapper api = new PromiseAPIWrapper(mAdm, mApi);
  mAdm.when(api.getCommand(commandID)
  ).then(new DoneCallback<Command>() {
    @Override
    public void onDone(Command command) {
      if (getActivity() == null) {
        return;
      }
      List<ActionResult> results = command.getActionResults();
      StringBuilder sbMessage = new StringBuilder();
      for (ActionResult result : results) {
        String actionName = result.getActionName();
        boolean succeeded = result.succeeded();
        String errorMessage = result.getErrorMessage();
        if (!succeeded) {
          sbMessage.append(errorMessage);
        }
      }
      if (sbMessage.length() == 0) {
        sbMessage.append("The command succeeded.");
      }
      mCommandResult.setText(sbMessage.toString());
    }
  }).fail(new FailCallback<Throwable>() {
    @Override
    public void onFail(final Throwable tr) {
      showToast("Failed to receive the command result: " + tr.getLocalizedMessage());
    }
  });
}

まず、PromiseAPIWrapper の getCommand() メソッドが呼び出されます。このメソッドは作業スレッドでコマンド全体を取得します。成功時は取得したコマンドを引数として onDone() メソッドが呼び出されます。

onDone() では、getActionResults() によってコマンドから ActionResult の配列を取得します。これは、ステートとアクションの定義 で用意した ActionResult のサブクラスの配列です。送信したアクションに応じて、TurnPowerResult インスタンスと SetBrightnessResult インスタンスを要素として含んでいるはずです(電源オフの場合は TurnPowerResult のみ)。これを for で順番に処理します。

Hello Thing-IF では、エラーメッセージを StringBuilder で 1 つにまとめて出力します。各 ActionResult からは、ActionResult の getActionName() で定義したアクション名、成功したかどうかのフラグ、失敗時のエラーメッセージを取得できます。

最終的に、連結したエラーメッセージ、または、The command succeeded. という成功メッセージを mCommandResult 経由で画面に出力します。

PromiseAPIWrapper での getTargetState() の実装は以下のとおりです。ThingIFAPI の getCommand() をラップしています。

public Promise<Command, Throwable, Void> getCommand(final String commandID) {
  return mAdm.when(new DeferredAsyncTask<Void, Void, Command>() {
    @Override
    protected Command doInBackgroundSafe(Void... voids) throws Exception {
      return mApi.getCommand(commandID);
    }
  });
}