This codelab covers how to optimize existing applications for Android N. We'll cover some of the most important developer-facing features being released, and what steps you'll need to take to make sure your application is ready.

What you'll learn

What you'll need

How will you use this tutorial?

Read it through only Read it and complete the exercises

How would rate your experience with building Android apps?

Novice Intermediate Proficient

To start, download the sample code by cloning the GitHub repository from the command line:

$ git clone https://github.com/googlecodelabs/getting-ready-for-android-n

You'll also need to obtain an API key from OpenWeatherMap before proceeding, as this sample uses that service to download weather data. You can request an API key by visiting: https://home.openweathermap.org/

We'll set up the API key in the next step.

First, let's see what the finished sample app looks like. With the code downloaded, the following instructions describe how to open the completed sample app in Android Studio.

  1. Open Android Studio, and select "Open Existing Android Studio project".
  2. Select the n-overview directory that you just downloaded
  3. Wait for the initial import and indexing to complete. If necessary, click on the "Project" button on the left-hand side of Android Studio to open up the project's file list.
  4. Important: Add your OpenWeatherMap API key to app/build.gradle, replacing the text that says <TODO: PASTE YOUR API KEY HERE>.
  5. Enable USB debugging on your Android device.
  6. Plug in your Android device and click the Run button. You should see the Sunrise home screen appear after a few seconds.
  1. Verify that a weather forecast appears in the sample app.

Frequently Asked Questions

Android N adds support for displaying more than one app at the same time. Supporting multi-window starts by making sure our layouts work well in both small and large windows but there are a couple of features that you should familiar with.

The sample app is already configured to use the N developer preview and you'll need a emulator or device running this version of Android.

Decide what Activities are resizeable

By default, all apps and all Activities are resizeable and can be used with the multi-window feature. Multi-window will work for most apps out of the box. However, this behavior can be disabled with the resizeableActivity attribute in the manifest:

AndroidManifest.xml

        <activity
            android:name=".MainActivity"
            ...
            android:resizeableActivity="false">

        </activity>

Let's start by trying out both modes:

  1. Start the Sunshine app
  2. Start multi-window mode by long-pressing the Overview button
  3. The multi-window mode is disabled and the app is covering the full screen
  4. Change resizeableActivity to true (or remove the attribute altogether, since it's the default) and try again:

Adapt layouts for small windows

To support split-screen usage, viewable content should be scaled to an appropriate size and density.

In multi-window mode, our sample app uses too many pixels to show today's weather.

Let's reduce the size of that blue "today" item. There's nothing specific to multi-window mode in this step, only regular resource qualifiers magic:

1. Create a new refs.xml file in a new resource directory called values-sw220dp. This qualifier will kick in if the smallest dimension is between 220dp and the next qualifier dimension, which is 400dp.

sw220dp/refs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Replace today's forecast layout with the same layout used for every day -->
    <item type="layout" name="list_item_forecast_today">@layout/list_item_forecast</item>
</resources>

2. Compare it with the "default" value:

sw400dp/refs.xml

<item type="layout" name="list_item_forecast_today">@layout/list_item_forecast_today_big</item>

This is going to show a special list_item_forecast_today_big layout if we have enough space, or a regular list_item_forecast if we don't.

Now list_item_forecast_today will refer to two different layouts, depending on the window's dimensions. The "art" icon for today's item is kept, which also increases the top and bottom margins.

Add a window background

When resizing, the window background is shown and can be customized. This gives the appearance that your application remains responsive, even as it is being restarted by the system.

This will use the current windowBackground but can be overridden by adding the windowBackgroundFallback property to the application theme:

    <style name="AppTheme" parent="@style/Theme.AppCompat.Light.DarkActionBar">
        ...
        <item name="android:windowBackgroundFallback">@color/sunshine_dark_blue</item>
    </style>

Open new adjacent activity

Sometimes it makes sense to open a new activity in an adjacent window, when the user is in split-window mode. This is hinted to the system by adding the FLAG_ACTIVITY_LAUNCH_ADJACENT flag to the Intent. Let's open the Location map (from the overflow menu) in the adjacent window:

  1. Open the ForecastFragment class and navigate to the openPreferredLocationInMap method.
  2. Add the flag to the Intent:
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
  1. Try it out!


More Information

Android 6.0 (Marshmallow) introduced two features to save battery power:

In Android N, we're continuing these efforts by taking Doze a step further, allowing the device to save battery while on the go. Any time the screen is off for a period of time and the device is unplugged, Doze applies a subset of the Marshmallow's CPU and network restrictions to apps. This means users can save battery even when carrying their devices in their pockets.

While in this lightweight Doze mode, network access, JobScheduler jobs, and SyncAdapter activities are all deferred until timed maintenance windows. This diagram explains the process:

Note that, if the device is stationary, the full set of Doze restrictions will eventually be applied as well. In this mode wakelocks, alarms, and GPS/Wi-Fi scans are all deferred to timed maintenance windows:

App Standby works in a similar manner, though in this case maintenance windows are typically provided only while the device is connected to a charger.

In all these cases, the important thing to be aware of is that an application will not have full background capabilities while outside of a maintenance window. Network bound activity is especially restricted. And with the new version of Doze, the device will be entering these low-power modes more often.

In order to ensure your application continues to run properly in the background, some services may have to be modified. Most :

  1. For background work that isn't time critical (like checking email, or backing up photos), migrate to the JobScheduler or GcmNetworkManager APIs. These APIs provide greater flexibility for scheduling jobs, and ensure your background work is handled during a maintenance window.
  2. For real-time notifications (like receiving an instant message), use high-priority GCM notifications. These have the ability to immediately wake up an app that's in a low-power state.
  3. For applications which can't use either of these options, whitelisting may be an option as a last resort. See the whitelist documentation for more details.

Our sample app uses AlarmManger to periodically wake up and fetch weather data while running in the background. In this section, we'll upgrade it to use GcmNetworkManager instead, allowing the OS to better control when our application wakes up, and ensuring that our application has network access during this events.

Let's review the existing implementation...

Our sample app is using a repeating alarm to periodically wake up and synchronize weather data from the Internet. Even though this is accomplished using an inexact repeating alarm, it's still inefficient as network requests aren't scheduled to occur at the same time as other network traffic, causing additional overhead in radio power consumption.

Before we begin, locate the existing alarm-based scheduling code in SunshineSyncService.java:

sync/SunshineSyncService.java

public static void ScheduleAlarm(Context context) {
    Log.i(TAG, "Scheduling alarm, interval: " + AlarmManager.INTERVAL_HALF_HOUR / 60000 + " min");
    Intent intent = new Intent(context, SyncAlarmReceiver.class);
    final PendingIntent pIntent = PendingIntent.getBroadcast(
            context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    long firstMillis = System.currentTimeMillis(); // Perform initial sync immediately
    AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    // Perform subsequent syncs every 30 minutes
    alarm.setInexactRepeating(AlarmManager.RTC_WAKEUP, firstMillis,
            AlarmManager.INTERVAL_HALF_HOUR, pIntent);
}

public static class SyncAlarmReceiver extends BroadcastReceiver {
    private static final String TAG = "SyncAlarmReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.i(TAG, "Broadcast received, sending sync intent to SunshineSyncService");
        Intent i = new Intent(context, SunshineSyncService.class);
        context.startService(i);
    }
}

We'll be replacing all of this with an implementation based on GcmNetworkManager, which will take care of automatically scheduling our jobs for optimal execution, based on a set of scheduling constraints which we'll provide

Import dependencies

Since GcmNetworkManager is provided by Google Play Services, start by adding the play-services-gcm dependency to app/build.gradle:

app/build.gradle

dependencies {
    // ...
    compile 'com.google.android.gms:play-services-gcm:8.4.0'
}

Click the Gradle sync button to apply the changes, automatically downloading the new library and adding it to your classpath.

Create a task service and set constraints

Create a new class called SunshineJobScheduler inside the sync folder. This class should extend GcmTaskService, which provides a framework for encapsulating a background task.

sync/SunshineJobScheduler.java

public class SunshineJobScheduler extends GcmTaskService {

    @Override
    public int onRunTask(TaskParams taskParams) {
        return 0;
    }

}

As mentioned earlier, both GcmNetworkManager and JobScheduler work by defining tasks or jobs, and a set of constraints that determine when they should be run. Let's define two constraints for this task — one that runs frequently while connected to a charger, and one that runs less frequently when on battery.

Note: When on battery, Doze and App Standby may be invoked, which will further reduce the frequency of how often your application will run. However, it's still important to be a good citizen and be conservative of how often your application is running.

sync/SunshineJobScheduler.java

public static final int MINUTES_AS_SEC = 60;
public static final int HOURS_AS_SEC = 60*60;
public static final String TASK_TAG_CHARGING = "sync_charging";
public static final String TASK_TAG_BATTERY = "sync_battery";

/**
 * Task specification for running jobs while connected to a charger. In this case, we allow
 * execution to occur frequently, as power usage is not constrained.
 */
private static final Task CHARGING_TASK = new PeriodicTask.Builder()
        .setService(SunshineJobScheduler.class)
        .setPeriod(30*MINUTES_AS_SEC)
        .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED)
        .setRequiresCharging(true)
        .setPersisted(true)
        .setUpdateCurrent(true)
        .setTag(TASK_TAG_CHARGING)
        .build();

/**
 * Task specification for running jobs while on battery. In this case, we execute jobs less
 * frequently to conserve battery life.
 *
 * Note that additional restrictions may be put on this task by the system if the device is
 * in Doze or App Standby mode due to inactivity. However, we should still try to be friendly
 * to the battery even when these aren't in effect.
 */
private static final Task BATTERY_TASK = new PeriodicTask.Builder()
        .setService(SunshineJobScheduler.class)
        .setPeriod(6 * HOURS_AS_SEC)
        .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED)
        .setRequiresCharging(false)
        .setPersisted(true)
        .setUpdateCurrent(true)
        .setTag(TASK_TAG_BATTERY)
        .build();

Notice that we set an approximate frequency for how often this task should run, as well as additional constraints around network connectivity and power status. The setPersisted(true) call ensures that this task will be preserved across device reboots.

Now that we've defined our tasks, we need to actually schedule them with the system. Create a new ScheduleTasks() static method, and call GcmNetworkManager.schedule() for each of the tasks we defined.

sync/SunshineJobScheduler.java

private static final String TAG = "SunshineJobScheduler";

/**
 * Method to schedule tasks. Called from either MainActivity.onCreate() for interactive
 * sessions, or onInitializeTasks() in the event Play Services is restarted.
 *
 * Note that since all jobs are flagged as "persisted" in their specifications (above), these
 * will automatically persist across reboots.
 *
 * @param context
 */
public static void ScheduleTasks(Context context) {
    GoogleApiAvailability googleAvailability = GoogleApiAvailability.getInstance();
    int resultCode = googleAvailability.isGooglePlayServicesAvailable(context);
    if (resultCode == ConnectionResult.SUCCESS) {
        Log.i(TAG, "Scheduling tasks");
        GcmNetworkManager gcmNetworkManager = GcmNetworkManager.getInstance(context);

        // Run every 30 minutes while the device is on a charger
        gcmNetworkManager.schedule(CHARGING_TASK);

        // ... And run every 6 hours while the device is on battery power
        gcmNetworkManager.schedule(BATTERY_TASK);
    } else {
        // We display a user actionable error inside MainActivity. We'll just abort and log the
        // error here.
        Log.e(TAG, "Google Play Services is not available, unable to schedule jobs");
    }
}


Bootstrap tasks

In our MainActivity class, we need to invoke our ScheduleTasks() method, in order to bootstrap the scheduling of our tasks. We'll do that at the end of onCreate().

This would also be a good place to perform a check to ensure that Google Play Services is actually available and ready to receive requests, by calling GoogleApiAvailability.isGooglePlayServicesAvailable().

Note: If Google Play Services is not available, you can still use the JobScheduler API directly on devices running API 21 or later.

MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    // ...
    GoogleApiAvailability gaa = GoogleApiAvailability.getInstance();
    int resultCode = gaa.isGooglePlayServicesAvailable(this);
    if (resultCode == ConnectionResult.SUCCESS) {
        // Schedule background sync via our GcmTaskService. This will also register these jobs
        // so that they persist across restarts.
        SunshineJobScheduler.ScheduleTasks(this);
    } else if (gaa.isUserResolvableError(resultCode)) {
        Log.e(TAG, "Google Play Services is not available, background sync not scheduled");
        gaa.showErrorDialogFragment(this, resultCode, 0);
    } else {
        Log.e(TAG, "Google Play Services is not available, background sync not scheduled");
    }
}

In the event that Google Play Services is restarted, any scheduled tasks will be removed. To correct this, onInitializeTasks() inside our task service will be automatically called to give your app a chance to re-schedule these jobs.

Create the onInitializeTasks() method and call the ScheduleTasks() function that was created earlier.

sync/SunshineJobScheduler.java

// In the event Google Play Services is restarted, this method will be called.
@Override
public void onInitializeTasks() {
    Log.i(TAG, "onInitializeTasks() called");
    super.onInitializeTasks();
    ScheduleTasks(this);
}

Perform work

Now that our tasks are scheduled, we need to actually give the job service work to do. This is done by overriding the onRunTask() method.

In our case, onRunTask() should call SunshineSyncEngine.performUpdate().

app/sync/SunshineJobScheduler.java

// Actual work is done here
@Override
public int onRunTask(TaskParams taskParams) {
    switch (taskParams.getTag()) {
        case TASK_TAG_CHARGING:
        case TASK_TAG_BATTERY:
            Log.i(TAG, "Scheduled sync task executing");
            SunshineSyncEngine syncEngine = new SunshineSyncEngine(getApplicationContext());
            syncEngine.performUpdate();
            return GcmNetworkManager.RESULT_SUCCESS;
        default:
            Log.wtf(TAG, "Unrecognized task tag: " + taskParams.getTag());
            return GcmNetworkManager.RESULT_FAILURE;
    }
}

Finally, because we don't want our app's UI to be empty for 30 minutes (or 6 hours) after being launched the first time, let's add a manual (immediate) sync when MainActivity launches, by calling SunshineSyncService.SyncImmediately() at the end of onCreate().

MainActivity.java

@Override
protected void onCreate(Bundle savedInstanceState) {
    // ...
    // Since we are running interactively, trigger an immediate sync as well. If this fails,
    // we'll still have cached data from our GcmTaskService.
    //
    // Note: We can't invoke SunshineSyncEngine directly, since we're running inside the
    // main thread. The SyncImmediately() method spawns a background service to perform
    // the network request.
    SunshineSyncService.SyncImmediately(this);
}

Add the service to the manifest

To complete our migration, edit AndroidManifest.xml to add the new SunshineJobScheduler service. An intent filter needs to be added for com.google.android.gms.gcm.ACTION_TASK_READY as well, in order to allow persisted tasks to be recreated after a device restart.

AndroidManifest.xml

<!-- GcmJobService instance used for background sync -->
<service
    android:name=".sync.SunshineJobScheduler"
    android:exported="true"
    android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
    <intent-filter>
        <action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
    </intent-filter>
</service>

At this point, our GcmNetworkManager implementation should work, and we can remove the old implementation.

Delete SunshineSyncService.SyncAlarmReceiver and SunshineSyncService.BootBroadcastReceiver, as well as their associated entries in AndroidManfiest.xml.

To test, try launching the app and watching logcat to see when syncs occur. You may want to try varying the value used for setPeriod() in CHARGING_TASK and BATTERY_TASK to shorter values, so you don't need to wait long. If using an emulator, you can use the emulator controls to simulate different charging and network states.

Frequently Asked Questions

More Information

Android N enhances system notifications in a few ways:

This section of the codelab will walk you through the changes made in Android N while also showing you how to fix some common UX notification problems in the Sunshine app. Most fixes are implemented using the NotificationCompat support library allowing you to support Android N, when available, without impacting users who do not have an Android N device.

Let's start by taking a quick look at the notifications the Sunshine app is showing users. You've likely noticed the issues while working through previous steps of this codelab, but we'll go through them step-by-step. Re-deploy the app and open the system notification shade. It's often handy to use the red square "Stop" button at the top of Android Studio to ensure a full restart of the app. You will see something similar to this:

Notice that Sunshine is taking up quite a bit of space in the system bar and notification shade with notifications. The weather forecast might be very important to the users who have downloaded the app, but many applications are installed and if each one did this it would be a general user experience issue for users. With the changes to the NotificationCompat support library introduced for Android N we can more easily fix this user experience by grouping the notifications. Let's do that!

Notification Groups

The first thing we need to do is upgrade the appcompat support library in use. You must be using a support library version greater than 24.0.0-alpha2. If you are not using at least this version of the library your code will still compile successfully because the methods we will use exist in earlier versions (supporting Android Wear). Unfortunately, the functionality we will show will not work on phone and tablet form factors, so don't skip this step!

  1. Navigate to the app/build.gradle file in the project and find the dependency listed below.
compile 'com.android.support:appcompat-v7:21.0.2'
  1. Update the dependency to version 24.0.0-alpha2 and then click the "Sync Now" action at the top of the file editor window to updated Android Studio with the changes from the Gradle build file. "Sync Now" updates the Android Studio project to match the gradle files.
compile 'com.android.support:appcompat-v7:24.0.0-alpha2'

Now we can update the code to use notification groups. Open the SunshineSyncEngine.java file to edit the notifications implementation. The quickest way find this file is to press the <Shift> key on your keyboard quickly two times anywhere in Android Studio. This action will bring up a universal search; simply type "SunshineSyncEngine" and hit enter.

  1. Add a string member constant to the SunshineSyncEngine class that can be used as a grouping key for all Forecast notifications. That code would look similar to this:

SunshineSyncEngine.java

private static final String FORECAST_NOTIFICATION_GROUP = "FORECAST_NOTIFICATION_GROUP";
  1. Locate the existing code that creates notifications by searching within the same file for "new NotificationCompat.Builder".
  2. Add the group indicator to the Notification by calling the setGroup(String) method on the Builder. That line would like this and there are two locations where this should be placed in this class (although you're only using one of them right now).

SunshineSyncEngine.java

.setGroup(FORECAST_NOTIFICATION_GROUP)
  1. All notification groups require a group summary so create an extra summary notification with code similar to this just below the while loop that creates the notifications:

SunshineSyncEngine.java

if (numNotifications > 0) {
    // Always build a summary if you have notifications in a group.
    // The summary will be displayed by the system when needed.
    NotificationCompat.Builder summaryBuilder =
        createSummaryNotificationBuilder(context,
            "Sunshine Forecasts", "");
    summaryBuilder.setGroupSummary(true);
    mNotificationManager.notify(WEATHER_NOTIFICATION_ID,  
        summaryBuilder.build());
}

Now deploy this code and you will see the notifications nicely grouped and taking up only a single icon in the system bar. The single summary notification is expandable and the summary notification disappears when the number of notifications within the group falls below 2.

Remote Input

The next advancement in Android N that we'll look at is the ability for phone and tablet users to respond to a notification by inputting text directly into the notification within the notification shade. This feature is primarily targeted towards messaging apps, but we'll adjust Sunshine to allow for temporary forecast notes to be recorded by the user from the notification.

To implement Remote Input for both Android Wear and Android for phones and tablets you can execute the following steps.

  1. First, you need to declare a class that will receive the an Intent. The Intent will contain information about the text that was input by the user. For Sunshine, we're going to re-use a BroadcastReceiver that is already in the app. Search for the MessageReplyReceiver class using universal search (tapping the shift key twice in Android Studio).
  2. Within MessageReplyReciever.java you will see the following Intent extra defined. This is the key that will be used to store the reply text within the Intent forwarded to the receiver.

MessageReplyReceiver.java

    public static final String EXTRA_REMOTE_REPLY = "extra_remote_reply";
    // public static final String REPLY_ACTION = BuildConfig.APPLICATION_ID + ".ACTION_MESSAGE_REPLY";
    // public static final String EXTRA_FORECAST = "extra_forecast";
  1. Go ahead and uncomment both the REPLY_ACTION and EXTRA_FORECAST static class variables. The REPLY_ACTION will be used to declare the RemoteInput action on the notification. Other notification actions can be declared on the same notification that don't allow input so this will ensure we can differentiate the correct action. The EXTRA_FORECAST contains the Forecast information which will be needed to rebuild the notification.
  2. Next uncomment the code within the onReceive method of the MessageReplyReceiver. This code will process the user's RemoteInput. Right now it simply logs the message rather than processing it. There is a helper method called getMessageReplyIntent within this class that should also be uncommented. This utility method will retrieve an Intent that the Receiver can process.
  3. Now let's switch back to editing code in SunshineSyncEngine.java, open that file. Declare a remote input object using the EXTRA_REMOTE_REPLY constant we were just looking at. This code should be located next to where the forecast notifications are created, inside the createForecastNotificationBuilder method. Remember, in Android Studio you can put your mouse over red statements and type <Alt>-Enter to receive a selection of automatic fixes for things like missing class or method imports.

SunshineSyncEngine.java

RemoteInput remoteInput = new RemoteInput.Builder(
                             MessageReplyReceiver.EXTRA_REMOTE_REPLY)
                             .setLabel("Take note")
                             .build();
  1. Create a Pending Intent to be called when the notification reply is submitted by the user. Luckily there is an IntentService already setup to handle the requests so we'll just reuse that.

SunshineSyncEngine.java

PendingIntent replyIntent = PendingIntent.getBroadcast(context,
    // This field should be unique per notification.
    forecast.mDaysSinceEpoch,
    MessageReplyReceiver.getMessageReplyIntent(forecast),
    PendingIntent.FLAG_UPDATE_CURRENT);
  1. Create an Action for the notification based on the RemoteInput object.

SunshineSyncEngine.java

NotificationCompat.Action actionReplyByRemoteInput =
    new NotificationCompat.Action.Builder(
        R.mipmap.ic_launcher,
        "Take note",
        replyIntent)
        .addRemoteInput(remoteInput)
        .build();
  1. Now add the Remote Input action to the notification.

SunshineSyncEngine.java

builder.addAction(actionReplyByRemoteInput);

Deploy and run your app. You'll notice each of the forecast notifications within the group has a "Take note" action. You'll need to expand the notification to see the action, and you will get a text input area if you select the action. Once you've sent input on its way you'll notice the progress indicator never stops. This is because we have one last step to finish the implementation: we need to update the notification.

  1. Open MesageReplyReceiver.java again.
  2. Within the onReceive message you'll notice the NotificationManager variable isn't used. Unless you update the notification that was used by the user the system will not know that the Remote Input was received. Add that implementations like this:
NotificationManager notificationManager =
    (NotificationManager) context.getSystemService(
         Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder notificationBuilder =
    SunshineSyncEngine.createForecastNotificationBuilder(
        context, forecast);
notificationManager.notify(forecast.mDaysSinceEpoch,
    notificationBuilder.build());

Templates Updates

The final notification change in Android N we'll look at in this codelab isn't one every developer will need to code for; the change is a user experience one. The standard templates for notifications were updated in Android N and refresh the presentation of the notifications you've already been seeing as you develop Sunshine Sometimes a picture is worth a thousand words.

The re-format of the layout also results in a more clear set of touch targets for users. This next image shows this more clearly.

If the app you develop isn't using custom views for notifications (like Sunshine) then the notifications will automatically get these presentation updates. If your own app is using Custom Views then you'll need to use Notification.DecoratedCustomViewStyle() or Notification.DecoratedMediaCustomViewStyle()to decorate the view set in Builder.setCustomContentView(). This will apply the updated UI elements you see above and allow your custom notifications to match other applications.

Well, that's it for notifications!

More Information

Your app is now ready for Android N. Congratulations!

What we've covered

Next Steps

Learn More