Sunday, April 27, 2014

Notifications Part 2: Expanded Notifications with Controls for Jelly Bean+

Since Android Jelly Bean, notifications have had the ability to be expanded into a larger view with a custom layout. One of the most common uses for this comes from media applications, such as Pandora and YouTube, that allow the user to interact with buttons in the custom layout in order to control their media services without being in the app. Given the usefulness of this technique, I have put together a demo service that creates a notification with a custom layout containing buttons, and added listeners for those buttons in order to call functions within the service. As with all of my other posts, the demo project can be found on my GitHub account here.

The entry point for this demo (MainActivity) uses a simple layout containing a single button that creates an intent with an action to start the background service that will be doing all of the work in our example.

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView( R.layout.activity_main );

    mLaunchNotificationButton = (Button) findViewById( R.id.launch_notification );
    mLaunchNotificationButton.setOnClickListener( new View.OnClickListener() {
        @Override
        public void onClick( View view ) {
            Intent intent = new Intent( getApplicationContext(), CustomNotificationService.class );
            intent.setAction( CustomNotificationService.ACTION_NOTIFICATION_PLAY_PAUSE );
            startService( intent );
        }
    });
}

Once the intent for the service is fired, onStartCommand is called in the service, which is where I pass the intent to a function that handles filtering out the action and calling the appropriate methods.

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    handleIntent( intent );
    return super.onStartCommand(intent, flags, startId);
}

private void handleIntent( Intent intent ) {
    if( intent != null && intent.getAction() != null )
    {
        if( intent.getAction().equalsIgnoreCase( ACTION_NOTIFICATION_PLAY_PAUSE ) )
        {
            mIsPlaying = !mIsPlaying;
            showNotification(mIsPlaying);
        } else if( intent.getAction().equalsIgnoreCase( ACTION_NOTIFICATION_FAST_FORWARD ) )
        {
             //fast forward function
        } else if( intent.getAction().equalsIgnoreCase( ACTION_NOTIFICATION_REWIND ) )
        {
             //rewind action
        }
    }
}

If the action is to play or pause the service, then the service would perform these actions and then display a new notification with the updated UI from the showNotification function.

private void showNotification( boolean isPlaying ) {
        Notification notification = new NotificationCompat.Builder( getApplicationContext() )
                .setAutoCancel( true )
                .setSmallIcon( R.drawable.ic_launcher )
                .setContentTitle( getString( R.string.app_name ) )
                .build();

        if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN )
            notification.bigContentView = getExpandedView( isPlaying );

        NotificationManager manager = (NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE );
        manager.notify( 1, notification );
    }

The code here follows the same convention as my previous post for standard notifications, with the exception of the Jelly Bean code to create a bigContentView Remote Views. The code for pre-Jelly Bean devices creates a notification that looks like this:


The getExpandedView function returns a RemoteViews object that consists of an inflated custom view that has PendingIntents associated with each of the buttons that will be sent to the existing service in order to control the operations within with the service. The layout is fairly standard with a fixed size of 128dp and three buttons under some custom text:

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/notification_expanded_height">

    <ImageView
        android:id="@+id/large_icon"
        android:layout_width="@dimen/notification_expanded_height"
        android:layout_height="@dimen/notification_expanded_height"
        android:scaleType="centerCrop"
        android:layout_alignParentLeft="true"
        android:layout_alignParentBottom="true"
    />
    <LinearLayout
        android:id="@+id/buttons_row"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_toRightOf="@id/large_icon"
        android:orientation="horizontal">

        <ImageButton
            android:id="@+id/ib_rewind"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="@dimen/notification_button_height"
            android:scaleType="fitCenter" />

        <ImageButton
            android:id="@+id/ib_play_pause"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="@dimen/notification_button_height"
            android:scaleType="fitCenter" />

        <ImageButton
            android:id="@+id/ib_fast_forward"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="@dimen/notification_button_height"
            android:scaleType="fitCenter" />
    </LinearLayout>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@string/app_name"
        android:textSize="@dimen/notification_text_size"
        android:layout_gravity="center"
        android:gravity="center"
        android:layout_toRightOf="@+id/large_icon"
        android:layout_above="@+id/buttons_row"/>

</RelativeLayout>

which in turn looks like this when it is created:


In the getExpandedView method, each image in the notification is set using the setImageViewResource method

customView.setImageViewResource( R.id.ib_rewind, R.drawable.ic_rewind );

and each button has a pending intent with action associated with it

Intent intent = new Intent( getApplicationContext(), CustomNotificationService.class );
intent.setAction( ACTION_NOTIFICATION_PLAY_PAUSE );
PendingIntent pendingIntent = PendingIntent.getService( getApplicationContext(), 1, intent, 0 );
customView.setOnClickPendingIntent( R.id.ib_play_pause, pendingIntent );

Since each of these pending intents goes back to the service, they are filtered through the handleIntent method and actions can be carried out based on the button clicks from this notification. Since the notification and operations are controlled through a service, the notification can control media when the app has been closed out or the Android device is locked. As this demo is not using an actual audio service to determine what controls should be shown, the mIsPlaying flag is set and passed to the custom view creation method in order to show either the play or pause button.


And that's how simple it is to create a custom expanded view with buttons! 

Monday, April 14, 2014

Notifications Part 1: Introduction

One of the most useful techniques for any developer's mobile toolkit is building notifications. They allow you to quickly get information to your user, bring them into your app, provide controls for media and do a variety of other pretty cool things. They are also the basis for interacting with the new Google Wear hardware. The first part of my notification posts will go over the basics of using the NotificationCompat builder, which is compatible back to Android v4, to display a notification in the status drawer, enable vibrations, show icons and fire an intent to open a specified activity. As with the other posts, all source code for this project can be found on GitHub.

First and foremost, the demo project that I made allows the user to populate different information and enable different features in a notification, as seen here:


The title, content text, subtext and content info are straight forward and demonstrated here:


 and the ticker text is the text that is displayed in the status bar when the notification comes in:


Notifications are created using the builder pattern with NotificationCombat.Builder. Each characteristic is then added to the notification through a series of functions, followed by returning the built notification to the NotificationManager. An example notification can be built as simply as this:

NotificationCompat.Builder builder = new NotificationCompat.Builder( this );
builder.setContentTitle(getString(R.string.app_name));
builder.setContentText(mNotificationTextEditText.getText());
builder.setSmallIcon( R.drawable.ic_launcher );
NotificationManager manager = 
(NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE );
manager.notify( 1, builder.build() );

where the '1' in the notify function is an int that can be incremented to stack notifications, or use the same number to replace any currently active notifications from your activity.


When only the small icon is set, it fills the roll of the large icon on the left. If both the large and small image are defined, then the large image is the left image, and the image next to the content info is the small image. 

Notification sounds can be triggered using the builder.setSound method. Unless you have a compelling reason, you should use the notification sound defined by the user if your notification is to be audible.

builder.setSound( RingtoneManager.getDefaultUri( RingtoneManager.TYPE_NOTIFICATION ) );

Vibrations can also be set for the notification using the builder.setVibrate method. This method takes an array of longs where every even index is the number of milliseconds that the device should not vibrate, and every odd index is the number of milliseconds that the device should vibrate. In the demo, the notification will vibrate for half of a second, pause for a quarter of a second, and then vibrate a second time for half of a second. The 0 at index 0 means that the vibration will start as soon as the notification is received, rather than waiting.

builder.setSound( RingtoneManager.getDefaultUri( RingtoneManager.TYPE_NOTIFICATION ) );

One of the more useful features of Android notifications is that they can be set with an intent that allows the user to open an activity on click, and then the notification can be cleared from the notification drawer.

Intent intent = new Intent( this, MainActivity.class );
TaskStackBuilder stackBuilder = TaskStackBuilder.create( this );
stackBuilder.addNextIntent( intent );
PendingIntent resultIntent =  
        stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent( resultIntent );
builder.setAutoCancel( true );

The time section of the notification can be overwritten to count the time since the notification posted by using the builder.setUsesChronometer method.


The last feature I want to go over that comes with basic notifications is the ability to use some predefined styles. In the demo project, I apply the Big Picture style and apply an image for the picture area

NotificationCompat.BigPictureStyle style = new NotificationCompat.BigPictureStyle();
style.bigPicture(BitmapFactory.decodeResource(this.getResources(), R.drawable.ic_launcher));
builder.setStyle(style);

This creates a notification that contains a large image under all of the standard information:


Aside from the basic features that I have just gone over, notifications allow for custom views that can contain items, such as buttons, to perform special actions. They can also be used with the new Google Wear hardware. I plan to go over these features in a later post, as notifications are one of the most powerful tools in the Android SDK.