teaching machines

CS 491 Lecture 11 – Lonely Phone

October 11, 2011 by . Filed under cs491 mobile, fall 2011, lectures.

Agenda

Goal

Our goal today is to write an app I call Lonely Phone. It’s a prank app that you should install on other people’s phones. It’s functionality is to beep whenever the phone is set down flat but stop when you pick it up. Annoyance is the secondary goal; encouraging people to lock their phones is the primary.

Android Components

We’ve met Activity, which is only one of the four components that make up our apps. In short, the components are:

Today we’ll explore Service and BroadcastListener in more detail.

Processes

All components of an app run in a single process. This process, once started, will persist until the OS decides to stop it. The priority of which processes are retained fall in this spectrum, from most important to least:

  1. Foreground: processes required for what the user is actively doing; includes running Activity, a foreground Service, a service bound to a running Activity, a Service running a callback, a BroadcastReceiver catching a signal.
  2. Visible: processes affecting UI; includes paused but not stopped Activity and Service bound to such an Activity.
  3. Service: processes not in 1 or 2 but explicitly marked as needing longevity; includes Services started with startService (not bindService).
  4. Background: processes with no visible components; including stopped (but not destroyed) Activitys. These aren’t immediately killed in the event that they will soon be restored.
  5. Empty: processes with no active components; including destroyed Activitys.

Services

One of the compelling features of a Service is that it is “alive” even when not focused. If you’ve got a long-running operation that you need to perform that is not tied to the user interface, then a Service is the way to go. You might think a thread in your Activity is sufficient to perform background tasks, but if your Activity is paused, it and the containing process may be destroyed to free resources for other apps. A thread tied to a Service is less likely to get killed. The service falls into priority level 3, while the paused Activity is in priority level 4.

There are a number of system services available to us. Like the SENSOR_SERVICE, which lets us register listeners for certain sensor events. Today, we want to detect a horizontal phone, so we need a couple of sensor events. Well, really, we want to ask the phone its orientation. SensorManager.getOrientation can do that for us. It’ll give you yaw, pitch, and roll, but you need to give it a rotation matrix. SensorManager.getRotationMatrix will give you the matrix, but you need to give it some readings from two sensors: the accelerometer and the magnetic field sensors. Don’t ask me how these sensors work. We can get their values by registering a listener with the sensor service:

final SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
sensorManager.registerListener(LonelyActivity.this, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_GAME);
sensorManager.registerListener(LonelyActivity.this, sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD), SensorManager.SENSOR_DELAY_GAME);

Where do we register this? In an Activity, we don’t really want to listen for events when the Activity is paused. That’ll drain the battery. So, we should start it up in onResume and cancel it in onPause. In a Service, we only have a couple lifecycle callbacks. We’re going to drain the battery. Serves ’em right for leaving their phone unlocked.

Our callback can get the yaw, pitch, and roll like so:

@Override
public void onSensorChanged(SensorEvent event) {
  if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
    mags = event.values;
  } else if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
    accels = event.values;
  }

  if (accels != null && mags != null) {
    SensorManager.getRotationMatrix(rotation, inclination, accels, mags);
    SensorManager.getOrientation(rotation, ypr);
    ((TextView) findViewById(R.id.ypr)).setText(String.format("%f\n%f\n%f", ypr[0], ypr[1], ypr[2]));
  }
}

Let’s add a couple of features to make development of this app easier: a) let’s set the background color of our test Activity to red when the phone is flat, and b) let’s add a SeekBar to adjust the sensitivity of “flatness.”

LonelyService

Okay, we’ve done some testing with a plain old Activity. Let’s now move our code to a Service, so that folks who get this installed on their phone will not have a UI to fiddle with and turn beeping off. We need an onCreate and an onDestroy, which turn on and off SensorEvent listening. In our listener, we want to recognize two significant events: a phone goes flat and a phone goes unflat. In between these two events, we want the phone to beep. Here’s our TODO list to make this work right:

We can determine the prior state by looking to the handler and seeing if it has a task it’s running. If so, the phone was flat. Here’s one go at this:

if (task == null && Math.abs(ypr[1]) < sensitivity && Math.abs(ypr[2]) < sensitivity) {
  player = MediaPlayer.create(this, R.raw.beep);
  task = new Runnable() {
    @Override
    public void run() {
      player.seekTo(0);
      player.start();
      handler.postDelayed(this, PAUSE);
    }
  };
  handler.post(task);
} else if (task != null && (Math.abs(ypr[1]) > sensitivity || Math.abs(ypr[2]) > sensitivity)) {
  handler.removeCallbacks(task);
  player.release();
  task = null;
}

Starting a service

Before a service can be run, it must be declared in the manifest:

<service android:name=".LonelyService"></service>

It’d be swell if we could start the service immediately upon installation. Unfortunately, I don’t know of a way to do so. We can trigger it to run at boot time, and we can also have an Activity turn it on. There are two ways to do this:

  1. Using bindService, which pairs the Activity and Service for interaction.
  2. Using startService, which spawns the service for an independent task. A Service started with startService has less priority than a bound one, unless it’s explicitly marked really super-duper important with startForeground.

Stopping a service is important if you’re going to be writing an annoying prank application. If we bind, we must unbind. If we start, we must stop.

Communication between Activity and Service

It’d be nice to set the sensitivity of flatness. Let’s add a SeekBar to do just that.

In its listener, we get the new values. But this is UI stuff. The Service needs it. How do get it? Through a Binder. If we bind the service, a binder is given to us, we lets us call communicate with the Service through an interface it publishes.

Binders take on two forms. If other apps can make use of our service, we’ll need to define the interface in this language called AIDL. For app-local services, we can have the Binder return the service:

public class LocalBinder extends Binder {
  LonelyService getService() {
    return LonelyService.this;
  }
}

Then in onBind, we can return an instance:

@Override
public IBinder onBind(Intent intent) {
  return binder;
}

Back in the Activity, we bind the service:

bindService(new Intent(LonelyActivity.this, LonelyService.class), connection, 0);

That connection variable is where we get the Binder from whom we can retrieve the Service. It has two callbacks:

private ServiceConnection connection = new ServiceConnection() {
  @Override
  public void onServiceDisconnected(ComponentName name) {
    service = null;
  }

  @Override
  public void onServiceConnected(ComponentName name,
                                 IBinder binder) {
    service = ((LonelyService.LocalBinder) binder).getService();
  }
};

Now, we can do service-y things to our service. Like set the sensitivity.

Starting a Service at boot

The third component is BroadcastReceiver. These are short-lived handlers of system signals. A number of standard signals are defined, but custom ones can be added. As there’s no lifecycle to a BroadcastReceiver, our subclass is short and sweet:

public class LonelyReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context,
                        Intent intent) {
    if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
      context.startService(new Intent(context, LonelyService.class));
    }
  }
}

We have to add an intent-filter when we declare our receiver in the manifest:

<receiver android:name=".LonelyReceiver">
  <intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED" />
  </intent-filter>
</receiver>

Starting things up on boot is a great example of the tragedy of the commons. If everybody did it, more phones would end up smashed on the sidewalk in frustration. We want fast boot times. Accordingly, we have to declare our bootiness in the manifest:

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

And there we have it.