Friday, January 23, 2015

Ollie and your Android Activity Lifecycle

Welcome to my second tutorial on connecting your Ollie to your own custom Android application. Last week, I alluded to needing to properly handle the Ollie application lifecycle. I'll finally shed some light on what I meant!
If you have not yet paired Ollie with your phone, I recommend you read my last article. I will not cover setting up your project or application manifest here.

Now that I've verified that I can connect to an Ollie, I want to do so in a manner that I can eventually ship in a game. To do this, I have to perform the following tasks:
  1. I want to wrap connecting to Ollie so I just say "findRobot()" and everything will kick off.
  2. I want to handle the user backgrounding my Activity. When this happens, I will let go of my Ollie connection so another app can connect in my place.
  3. I want to be notified when Ollie disconnects on its own accord, and let my app reconnect in a reasonable way.

Application Architecture

A quick overview of why I'm writing this article. I wanted to create a game similar to "Boppit" where the user performs a series of actions dictated by a possibly malevolent robot overlord. If you fail to perform this series of instructions, you will be horribly punished with a lower score! I did this as part of "Hack Friday" at Sphero where I decided to vet our new public SDK against apps other than our demo applications and production driving app. In the process, I discovered that there were no internally developed instructions and took it upon myself to fill the gap whilst developing a fun game prototype.

To handle the connection lifecycle for this game, I've created a class "BoppitRobotProvider" to handle robots coming online and offline as well as echoing important application lifecycle events down to the DiscoveryAgent. This has an "IRobotManager" that I wrote a DiscoveryAgentLE version named "OllieRobotManager" and a fake implementation for testing I named "FakeRobotManager." I will only walk you through "OllieRobotManager" here. The unit tests I'll hide for now, as I don't want to write an article on Unit Testing in Android (in fact, I'm also taking this as an opportunity to develop my own TDD skillset, so it will be far less interesting than other internet resources on the subject).

Now that my disclaimers are out of the way, lets describe the basic interfaces!


public interface IRobotManager {
 void addRobotConnectionHandler(IRobotConnectionHandler robotConnectionHandler);

 void startDiscovery();
 void stopDiscovery();

 void disconnectAllRobots();

 public interface IRobotConnectionHandler {
  void robotConnected(IRobot robot);
  void robotDisconnected(IRobot robot);
 }
}

As I mentioned above, I moved DiscoveryAgentLE into an interface (this interface!) so I could mock the basic lifecycle events. A quick description of what's happening:

  • addRobotConnectionHandler will add an interface to handle connecting and disconnecting of robots
    • QuickNote: I should add a "removeRobotConnectionHandler" function, but I haven't used it yet so I won't write it. Never write code you don't use, and delete it when you find dead code!
  • startDiscovery will look for robots
  • stopDiscovery will stop looking
  • disconnectAllRobots will disconnect everyone currently connected
  • IRobotConnectionHandler is used for notifying listeners of connection events. We only care about:
    • robotConnected - when a robot connects
    • robotDisconnected - when that robot disconnects
One thing you might notice, I'm passing around something called an "IRobot." This is just an empty interface at the current point in time. I wrote it during unit testing, and will grow it or remove it when necessary.

Activity Lifecycle

Now that we have an interface to target, lets start handling the application lifecycle first! Hopefully the IRobotManager will make the underlying logic much more straightforward to you. Lets start writing the "BoppitRobotProvider"!

Lets start by saying that we'll handle IRobotConnectionHandler events, as we want to know when the robot come online and goes offline.


public class BoppitRobotProvider implements IRobotManager.IRobotConnectionHandler

If you were to build now, you'd get errors due to your not fulfilling the interface. If it really bugs you, you can throw an empty implementation for now. We'll also store some variables here for use later:


 private IRobotManager mRobotManager;
 private IRobot mRobot;

 private Vector<IRobotConnectionHandler> mRobotConnectionHandlers = new Vector<>();

mRobotManager is self explanatory, this is the manager we're going to use. mRobot is the robot we'll have eventually, and mRobotConnectionHandlers is a list of IRobotConnectionHandlers we expose for the eventual Boppit application to respond to only the most basic connection events!

We'll need a constructor:


 public BoppitRobotProvider(IRobotManager robotManager) {
  mRobotManager = robotManager;
  mRobotManager.addRobotConnectionHandler(this);
 }

And a few properties:


 public IRobot getRobot() {
  return mRobot;
 }

 public void addConnectionHandler(IRobotConnectionHandler robotConnectionHandler) {
  mRobotConnectionHandlers.add(robotConnectionHandler);
 }

 public void removeConnectionHandler(IRobotConnectionHandler robotConnectionHandler) {
  mRobotConnectionHandlers.remove(robotConnectionHandler);
 }

Before we get to the real meat and potatoes of this class!

So, what will the first thing we want to do in our app? Find a robot of course! Your first instinct would probably just be to throw connection into the onCreate of your activity. You will almost inevitably move it somewhere else, whether you wait for streaming assets to load or you feel like making Ollie an extra optional step. For this reason, rather than just saying something like "handleOnCreate," I'll name this method a more generic "findRobot."


 public void findRobot() {
  mRobotManager.startDiscovery();
 }

That was easy!

Now we want to handle the most important part of the application lifecycle. You should never maintain a connection to Ollie in the background. The most important reason is that Android may decide to terminate your Activity at any time, and without warning, when it's not visible! The other reason is that you want to be a good citizen, it's rather inconsiderate to steal the Ollie connection all to yourself when another developer may be reading through this very tutorial and banging his head against the table whilst his Ollie is failing to connect! I'd recommend this page for more information on the Android Activity lifecycle.


 public void handleOnPause() {
  mRobotManager.stopDiscovery();
  mRobotManager.disconnectAllRobots();
 }

The goal would be to simply call this method when onPause is invoked. In onResume, you'd attempt to reconnect with "findRobot" if desired. Or perhaps you'd load another fragment first? As I said, you very rarely end up calling findRobot in onCreate in your final app. Another minor detail, the order of the function calls is minimally important. I call stopDiscovery() first so I don't get a robot connection come in after calling disconnectAllRobots(), on the backend this is all happily threaded.

So now, lets handle a robot connecting.


 @Override
 public void robotConnected(IRobot robot) {
  if (mRobot == null) {
   mRobot = robot;
   for (IRobotConnectionHandler connectionHandler : mRobotConnectionHandlers) {
    connectionHandler.robotConnected(robot);
   }
   mRobotManager.stopDiscovery();
  }
 }

The idea behind this is that you'll have one robot you'll care about. If you already have a robot, ignore the new one! If you actually get a robot, then you'll stop looking and play with your happy Ollie. The SDK acts like this by default for now, but there are no guarantees in the future (and it's still recommended that you stop discovery).

Now, one last detail and you've reached the end of handling the basic Activity lifecycle.


 @Override
 public void robotDisconnected(IRobot robot) {
  if (robot == mRobot) {
   mRobot = null;
   for (IRobotConnectionHandler connectionHandler : mRobotConnectionHandlers) {
    connectionHandler.robotDisconnected(robot);
   }
  }
 }

This simply lets go of a robot we've connected to and informs the application. We don't have to clear the robot anywhere else as "disconnectAllRobots" will raise this message (as you'll see soon).

Now, a quick interface. This is identical to the one in IRobotManager, but I like not having a user of this class having to know about IRobotManager.


 public interface IRobotConnectionHandler {
  void robotConnected(IRobot robot);
  void robotDisconnected(IRobot robot);
 }

And you're ready to actually hook it up into DiscoveryAgentLE!

Discovery Agent

Until now, you've written no SDK code. This is because the SDK itself is incredibly generic. It lets you connect to Ollie and Sphero, and it lets you connect one to many robots of any single type. All this generalization is a bit complicated if you just start slinging it around your Activity all willy nilly!

So, to start things off, it's time to create our OllieRobotManager:


public class OllieRobotManager implements IRobotManager, RobotChangedStateListener

You may remember IRobotManager from before, it represents the basic operations you'll perform on a DiscoveryAgentLE. The RobotChangedStateListener you may remember from my last tutorial. This is how we know what's happening with all these robots flying around the room!

Now lets get the basic private fields out of the way. I always hate it when other tutorials skip these!


 private static final String LOG_TAG = "OllieRobotManager";

 private Context mContext;
 private DiscoveryAgent mDiscoveryAgent;
 private RobotWrapper mRobot;

 private Vector<IRobotConnectionHandler> mRobotConnectionHandlers = new Vector<>();

A quick roundup off all these crazy variables I just dumped on your head:

  • LOG_TAG is just the first parameter I pass to the Android Logging subsystem. I dislike seeing any sort of magic numbers in my code, even string literals!
  • mContext is the application context, it's necessary to start the DiscoveryAgent (as well as virtually anything else in Android).
  • mRobot is the robot I'm getting. You'll see later on that this implements IRobot, and pretty much just provides you with a ConvenienceRobot. I highly recommend that your final application does not upcast IRobot (this and any other form of reflection should generally be discouraged in any code), but I wrote this tutorial the moment I had the basic Android lifecycle under control!
  • mRobotConnectionHandlers is, once again, our friendly neighborhood vector of callbacks.
Whew that was a mouth... err... keyboard full! Lets get to acquiring our DiscoveryAgent:


 public OllieRobotManager(Context context) {
  mContext = context;
  mDiscoveryAgent = DiscoveryAgentLE.getInstance();
  mDiscoveryAgent.addRobotStateListener(this);
 }

As you can see, we're opting for a DiscoveryAgentLE. This is the DiscoveryAgent to use for Ollie. We're also choosing to add ourselves as a state listener, but I won't get to that implementation until the end of this tutorial (you know, save the best for last and all that jazz).

There is pretty much no reason for me to even post this other than preventing you from tearing your hair out for a second when you hit build and see nothing but errors:


 @Override
 public void addRobotConnectionHandler(IRobotConnectionHandler robotConnectionHandler) {
  mRobotConnectionHandlers.add(robotConnectionHandler);
 }

As you can guess, we need to register our event handlers.

So our next task is to start discovery.


 @Override
 public void startDiscovery() {
  try {
   mDiscoveryAgent.startDiscovery(mContext);
  } catch (DiscoveryException e) {
   Log.e(LOG_TAG, "Failed to start discovery!");
   e.printStackTrace();
  }
 }

I would like to handle this error better, but even the example code in the SDK does this! The most I could really do is add a boolean to the return type if it fails to start discovery, but that's a task for future me. Future me doesn't like past me...

This is another self explanatory line:


 @Override
 public void stopDiscovery() {
  mDiscoveryAgent.stopDiscovery();
 }

Simply stop discovery when we want to stop discovery.

This line is pretty important. The SDK doesn't have a "disconnect everybody" call at the moment, but you really should perform this task to maintain good citizen status amongst your users.


 @Override
 public void disconnectAllRobots() {
  for (Robot robot: mDiscoveryAgent.getConnectedRobots()) {
   robot.sleep();
  }
 }

As you can see, we go through and ensure that everyone's disconnected. I do this just for my own self assurance that, even if one robot slips through and connects without me taking note, everyone is put to sleep when my app exits (you can connect to more than one robot).

The other item of note is my choice of sleep() over disconnect(). disconnect() will terminate your connection to the robot, but the robot will be left on (and colored magenta) afterwards. To your users, this will look like an error. If you were to call sleep() then disconnect(), you'd still be left in the disconnected but on state due to the way threading and message handling works. Do not fret, when the robot goes to sleep you'll get a disconnection message none the less!

Now time for the actual connection! Be warned, this is quite the mouthful (keyboard full?):


 @Override
 public void changedState(Robot robot, RobotChangedStateNotificationType robotChangedStateNotificationType) {
  switch (robotChangedStateNotificationType) {
   case Online:
    mRobot = new RobotWrapper(new Ollie(robot));
    notfyRobotConnected(mRobot);
    break;

   case Disconnected:
    notifyRobotDisconnected(mRobot);
    mRobot = null;
    break;
  }
 }

 private void notfyRobotConnected(RobotWrapper robot) {
  for(IRobotConnectionHandler connectionHandler: mRobotConnectionHandlers) {
   connectionHandler.robotConnected(robot);
  }
 }

 private void notifyRobotDisconnected(RobotWrapper robot) {
  for(IRobotConnectionHandler connectionHandler: mRobotConnectionHandlers) {
   connectionHandler.robotDisconnected(robot);
  }
 }

Despite being a wall of text, I'm sure you can easily tease apart the meaning. I'll start at the bottom as that's the easiest:
notifyRobotConnected and notifyRobotDisconnected simply tell all our listeners about what's going on!

So, now lets jump up into changedState. As you may recall, this method is invoked whenever the state of a connecting robot changes. There are many more that can be returned (and if you want to make a fancy connection screen, you'll care about all of them), but Online and Disconnected are the two most important.

The Online case happens after we've successfully connected to a robot, it tells us that we have a new buddy we can start talking to! I quickly wrap it up in something that conforms to the IRobot interface (an empty interface for now), cache it, and tell the world about my shiny new present!

Disconnected is raised whenever a robot disconnects. We forget about the robot we've been maintaining a connection to, and tell the world about our tragic loss.

And that's it! You can connect to a robot! Well, outside of my simple RobotWrapper:


 public class RobotWrapper implements IRobot {
  private final ConvenienceRobot mRobot;

  public RobotWrapper(ConvenienceRobot robot) {
   mRobot = robot;
  }

  public ConvenienceRobot getRobot() {
   return mRobot;
  }
 }

And I suppose I should actually show you where to drop this into an activity...

Activity

And now for the grand finale! You can shove this right into your Activity from the last tutorial. A quick breakdown of what I'll show you:
  • in onResume we'll connect to a robot. I know I told you that you'll rarely do this by the end of your project, but this is a lowly tutorial!
  • in onPause, we'll kick off the pausing logic we wrote previously.
  • when a robot connects, we'll turn it green. This is simply because green is the best color.
  • when a robot disconnects AND we're not paused, we'll find a new one!
Lets start by creating the variables I first mentioned:


 private IRobot mRobot;
 private BoppitRobotProvider mRobotProvider;

 private boolean mPaused;

And starting to work in onCreate:


 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_connection);

  mRobotProvider = new BoppitRobotProvider(new OllieRobotManager(this));
  mRobotProvider.addConnectionHandler(...

But I'll hold off on actually handling the robot connection for the moment.

Our onResume is simple:


 @Override
 protected void onResume() {
  super.onResume();
  mPaused = false;

  IRobot robot = mRobotProvider.getRobot();
  if (robot != null) {
   setRobot(robot);
  }
  else {
   mRobotProvider.findRobot();
  }
 }

I check to see if we have a robot already (we shouldn't in this app, but imagine if there were able to share this BoppitRobotProvider between activites...), then set it! Otherwise, it's time to start looking for one. Also, we must remember to store the fact that we're no longer paused.

This brings us to onPause. There isn't much to be done here:


 @Override
 protected void onPause() {
  super.onPause();
  mPaused = true;

  mRobotProvider.handleOnPause();
 }

As you can see, we just store the fact that we are paused and call into all that fancy code we wrote earlier.

Of course, you're probably wondering why we're storing whether or not we're paused as well as what I hid behind the "..." above. So, lets finish adding out connection handler!


  mRobotProvider.addConnectionHandler(new BoppitRobotProvider.IRobotConnectionHandler() {
   @Override
   public void robotConnected(IRobot robot) {
    setRobot(robot);
   }

   @Override
   public void robotDisconnected(IRobot robot) {
    setRobot(null);

    if (!mPaused) {
     mRobotProvider.findRobot();
    }
   }
  });

robotConnected is relatively straightforward, we just set the robot when we get one.

robotDisconnected is where the fun is. We forget about our robot, and try to find a new one. But here's where it gets tricky: if you remember back to the onPause handler we wrote, we simply put all our robots to sleep when the Activity pauses. Some time after that, the robot goes to sleep and we get a callback saying that it's gone. If we're paused, we don't want to try to connect again (the SDK will let us until the activity is actually destroyed). So we store a flag to remind ourselves that we're no longer interested in connecting.

And that's everything!

Ok, fine, I'll also turn the robot green.


 private void setRobot(IRobot robot) {
  mRobot = robot;
  if (mRobot != null) {
   ConvenienceRobot convenienceRobot = ((OllieRobotManager.RobotWrapper)robot).getRobot();
   convenienceRobot.setLed(0.f, 1.f, 0.f);
  }
 }

As I stated before, the IRobot interface needs to be actually created or replaced. We'll see where my unit tests take me as I get ready to make a game.

Happy hacking, and may the source be with you!

No comments:

Post a Comment

Kotlin Game Programming