Getting the Command Result with FCM

The project in the android_fcm directory implements a functionality to receive push notifications using FCM. The source code in the fcm folder includes the functionality developed based on the steps described in Android (FCM) Push Notification Tutorial. See the FCM push notification tutorial for the flow of receiving push notifications.

This topic explains how to add the push notification feature to Hello Thing-IF based on the FCM tutorial.

Overview

First, let us grasp the entire flow of getting the command result via push notification. The processes in the pale purple boxes are different from those in the FCM push notification tutorial.

The following processes are different from those in the FCM sample.

  • Initializing push notification: The FCM device token is acquired and installed to Kii Cloud when CommandFragment is displayed. The process to install the device token is changed for the Thing-IF SDK.
  • Commonalizing the initialization process of push notification: The initialization process of push notification is commonalized. When MyFirebaseInstanceIDService detects a token update in FCM, it calls the initialization method used for the first initialization in MainActivity by broadcasting.
  • Receiving a message: MyFirebaseMessagingService now informs the broadcast receiver of CommandFragment that it received a push notification.
  • Receiving an event on the command screen: The broadcast receiver of CommandFragment receives the command result.

Description of each process is as follows:

Initializing push notification

You need to associate the device token obtained from FCM with the owner in the ThingIFAPI instance in order to initialize push notification with FCM. Hello Thing-IF achieves it when the login screen is switched to the command screen.

The process to open the command screen appears twice in MainActivity as indicated in Screen Transition with Fragments. Both use the code below.

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

initializePushNotification();

The first three lines switch the fragment to the command screen as explained previously. Just after the switch, the initializePushNotification() method is called to initialize FCM.

See below for the initializePushNotification() method implementation.

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();
    }
  });
}

The above sample code calls the registerFCMToken() method of PromiseAPIWrapper and displays a successful or failed result in the toast.

See below for the registerFCMToken() method implementation.

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;
    }
  });
}

The above sample code calls the FCM API to get and pass the device token to the installPush() method of ThingIFAPI for device installation. Specify PushBackend.GCM with the method to indicate the device token is for FCM. As with the developer portal, the API does not distinguish FCM from GCM.

The FCM device token has been generated in the background by the FCM SDK. Get it with the getToken() method.

Usually, the device token should be available before you attempt to get it. Otherwise, the getToken() method returns a null. The sample code returns an IllegalStateException and displays the toast in FailCallback of the promise to simplify the tutorial code. In a real app, it would be appropriate to retry getting the token or prompt to restart the mobile app.

Commonalizing the initialization process of push notification

The device token can be updated on the FCM server and you need to register it to Kii Cloud.

The onTokenRefresh() method of MyFirebaseInstanceIDService notifies the update of the device token. When the token is updated, an installation process equivalent to the initializePushNotificastion() method should be executed. However, MyFirebaseInstanceIDService, which is an Android service, cannot directly call the method in MainActivity. Use broadcasting to relay the task to MainActivity.

MyFirebaseInstanceIDService broadcasts TOKEN_REFRESH when the onTokenRefresh() method is called.

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);
  }
}

When MainActivity receives TOKEN_REFRESH with onReceive(), it calls the initializePushNotification() method to initialize push notification.

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();
      }
    };
    ......
  };
}

When MainActivity is resumed or paused, it enables or disables the broadcast receiver 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();
}

Receiving a message

When the mobile app receives a message from FCM, it calls onMessageReceived() in MyFirebaseMessagingService.

The command execution is notified as a Direct Push message, which is one of the three types of push notification (Push to App, Push to User, and Direct Push) of Kii Cloud. The payload includes the commandID parameter.

The below code gets the command 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;
    }
  }
}

If bundle.getString() can get commandID, it means Thing Interaction Framework notified that the command had been executed. This notification is forwarded to the command screen by the broadcast of INTENT_COMMAND_RESULT_RECEIVED. commandID is included as the broadcast parameter.

Receiving an Event on the Command Screen

The command screen receives the event of push notification via the broadcast.

The following code enables and disables the broadcast receiver mCommandResultBroadcastReceiver in CommandFragment. The receiver is enabled when the command screen is activated and disabled when the screen is stopped.

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();
  }
}

See below for mCommandResultBroadcastReceiver implementation. It gets commandID as a parameter of onReceive().

See Getting the command result for implementation of onPushMessageReceived().

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

Getting the Command Result

The push message from Thing Interaction Framework eventually calls the onPushMessageReceived() method with the command ID as the argument.

This method gets the entire command which has the specified command ID and evaluates the command result.

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());
    }
  });
}

First, it calls the getCommand() method of PromiseAPIWrapper. This method gets the entire command in a worker thread. It calls the onDone() method with the obtained command as the argument when the command is successfully obtained.

getActionResults() in onDone() gets an array of ActionResult from the command. This is an array of the subclasses of ActionResult prepared in Defining a state and actions. The array should have TurnPowerResult and SetBrightnessResult instances according to the sent actions. If you turned off the power, the array should have only a TurnPowerResult instance. Each instance is processed with the for statement one by one.

Hello Thing-IF outputs error messages in one batch with StringBuilder. You can get the action name defined with getActionName() of ActionResult, the flag of success or failure, and the error message at failure from each ActionResult.

Finally, the concatenated error messages or the success message The command succeeded is displayed on the screen via mCommandResult.

See below for getTargetState() implementation in PromiseAPIWrapper. It wraps getCommand() of ThingIFAPI.

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);
    }
  });
}