Sunday, June 29, 2014

Introduction to Official Android Wear Development Tutorial

    In this tutorial, I will go over writing a program for Android Wear physical devices. This app will allow the user to select a time interval on the Wear, and then set off a vibration on the user's wrist when the timer goes off before repeating. The goal of this app is to hopefully assist drivers on long road trips to stay alert (an idea that I had while driving back along I80 from San Francisco to Denver) and has been tested on the Samsung Gear Live physical device (which is where all of the screen shots have come from). The source code for this app is available on GitHub.

    For those interested, the app is available on the Play Store for free and working now that Play Services 5.0 is available.

    Also, before I begin, I should also throw out a thank you to ReadWrite, as I ended up getting to I/O last minute with a registration code through a contest on their awesome site.


    The first step is to download and install the latest Android Studio with SDK packages, then follow this documentation for putting together the broiler plate code for the mobile and wear version of the app. For this tutorial I named the main activity for the Wear portion 'IterationActivity', as this will be the activity screen that is displayed when the user opens the app on their Wear device. In the broiler plate code as of the current release, this activity extends WatchActivity, which is not available in the SDK, so we can safely change this to a regular Activity class. Next we can create an IntentService called TimerService that handles all of the background operations. We'll get back to filling this out once the infrastructure is ready to go.

    In the Wear and Mobile (it's important to have it in both, or the wear portion will not unpackage onto the user's device. I learned this the hard way with a lot of frustration :) ) sections, open AndroidManifest.xml and add the vibrate permission:

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

    Then add the service tag for our TimerService.class in the Wear manifest:

<service android:name=".Services.TimerService" />

    Next, under the Mobile section in build.gradle, add the following line to dependencies in order to package the Wear portion of the app with the Mobile:

wearApp project(':wear')

    Now that this app should compile and run directly on a Wear device, it's time to make it actually do something. The layout xml file used by IterativeActivity is a simple list view with some text above it, and should look like this:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Select an interval"
    />

    <ListView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
    />

</LinearLayout>

    Under rect_activity_iteration and round_activity_iteration layout files, we can replace the text view with a GridViewPager for containing our view:

<GridViewPager
    android:id="@+id/view_pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    Back to the IterationActivity file, we can start adding content to onCreate(). The following block is added when the activity is started. If the SharedPreference for a selected duration is set, then the timer is started with that value, otherwise we continue with the block.

if( ( getIntent().getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK ) == Intent.FLAG_ACTIVITY_NEW_TASK ) {
    SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences( this );
    long duration = pref.getLong( SAVED_STATE_SELECTED_DURATION, 0 );
    if( duration != 0 ) {
        setupTimer( duration );
        finish();
        return;
    }
}

    If the activity hasn't been started before and there is no preselected interval time, then we can set up the array of times that the user can select from:

private void setupIterationArray() {
    int[] minutes = getResources().getIntArray( R.array.interation_minutes );
    for( int i = 0; i < minutes.length; i++ ) {
        IterationListItem item = new IterationListItem( getResources().getQuantityString( R.plurals.label_minutes, minutes[i], minutes[i] ),
                minutes[i]  * 60 * 1000 );
        mIterationTimes.add( item );
    }
}

    Next we set the content view to activity_iteration.xml:

setContentView(R.layout.activity_iteration);

    After the view is set, we can create the list of labels and times that the user can select from:

private void setupIterationArray() {
    int[] minutes = getResources().getIntArray( R.array.interation_minutes );
    for( int i = 0; i < minutes.length; i++ ) {
        IterationListItem item = new IterationListItem( getResources().getQuantityString( R.plurals.label_minutes, minutes[i], minutes[i] ),
                minutes[i]  * 60 * 1000 );
        mIterationTimes.add( item );
    }
}

    Finally under onCreate() we can initialize the list view and GoogleAPIClient. This will provide the activity for selecting durations in Wear:

private void initList() {
    mListView = (ListView) findViewById( R.id.list_view );
    mListView.setAdapter( new ArrayAdapter<IterationListItem>( this,
            android.R.layout.simple_list_item_1, mIterationTimes ) );
    mListView.setOnItemClickListener( this );
}

private void initGoogleApiClient() {
    mGoogleApiClient = new GoogleApiClient.Builder(this)
            .addApi( Wearable.API )
            .addConnectionCallbacks( this )
            .addOnConnectionFailedListener( this )
            .build();
}


    When one of these list items is clicked, the value is saved in a shared preference and the duration is sent to the setupTimer method:

@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences( this );
    SharedPreferences.Editor editor = pref.edit();
    editor.putLong( SAVED_STATE_SELECTED_DURATION, mIterationTimes.get( position ).getDuration() );
    editor.commit();
    setupTimer( mIterationTimes.get( position ).getDuration() );
}

    setupTimer accepts a duration in milliseconds and cancels any notifications that this app may have created before, then builds and launches a new timer notification and registers an alarm before closing the list activity:

private void setupTimer( long duration ) {
    NotificationManagerCompat notificationManager = NotificationManagerCompat.from( this );
    notificationManager.cancel( 1 );
    notificationManager.notify( 1, buildNotification( duration ) );
    registerAlarmManager( duration );
    finish();
}

    The buildNotification method is pretty standard and similar to the three other notification posts I wrote about when going over the developers preview. The important part to note here is the PendingIntent that goes to TimerService and closes down the timer:

private Notification buildNotification( long duration ) {
    Intent removeIntent = new Intent( ACTION_REMOVE_TIMER, null, this, TimerService.class );
    PendingIntent pendingRemoveIntent = PendingIntent.getService( this, 0, removeIntent, PendingIntent.FLAG_UPDATE_CURRENT );

    return new NotificationCompat.Builder( this )
            .setSmallIcon( R.drawable.ic_launcher )
            .setContentTitle( "Stay Awake" )
            .setContentText(TimeUtil.getTimeString( duration ) )
            .setUsesChronometer( true )
            .setLargeIcon( BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher ) )
            .setWhen( System.currentTimeMillis() + duration )
            .addAction( R.drawable.ic_launcher, "Remove Timer", pendingRemoveIntent )
            .setDeleteIntent( pendingRemoveIntent )
            .setLocalOnly( true )
            .build();
}


    The final thing to do in IterationActivity is register an alarm that fires off an intent to TimerService once the duration has finished:

private void registerAlarmManager( long duration ) {
    AlarmManager alarmManager = (AlarmManager) getSystemService( Context.ALARM_SERVICE );
    Intent intent = new Intent( ACTION_SHOW_ALARM, null, this, TimerService.class );
    PendingIntent pendingIntent = PendingIntent.getService( this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT );

    long time = System.currentTimeMillis() + duration;
    alarmManager.setExact( AlarmManager.RTC_WAKEUP, time, pendingIntent );
}

    Now that IterationActivity is set, we can focus on TimerService.class. HandleIntent listens for two types of actions: starting an alarm and removing an alarm.

@Override
protected void onHandleIntent(Intent intent) {
    String action = intent.getAction();

    if( IterationActivity.ACTION_SHOW_ALARM.equals( action ) ) {
        showAlarm();
    } else if( IterationActivity.ACTION_REMOVE_TIMER.equals( action ) ) {
        removeAlarm();
    }
}

    showAlarm() is only called after the first timer has ended, so it simply vibrates the Wear on call and fires an intent back to IterationActivity with a flag stating that the next notification timer should be posted.  It should be noted that after a lot of time and frustration, I switched to using a pattern here rather than simply passing in the duration, as Wear won't stop the vibration on the watch unless the screen is active if a pattern isn't used.

private void showAlarm() {
    final Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
    long[] pattern = { 0, getResources().getInteger( R.integer.vibration_duration ) };
    v.vibrate( pattern, -1 );
    Intent intent = new Intent( this, IterationActivity.class );
    intent.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
    startActivity( intent );
}


    removeAlarm() cancels any notification timers that may be visible, any running alarm manager for our specified PendingIntent and sets the selected duration shared preference to 0.

private void removeAlarm() {
    NotificationManagerCompat notificationManager = NotificationManagerCompat.from( this );
    notificationManager.cancel( 1 );

    AlarmManager alarmManager = (AlarmManager) getSystemService( Context.ALARM_SERVICE );

    Intent intent = new Intent( IterationActivity.ACTION_SHOW_ALARM, null, this, TimerService.class );
    PendingIntent pendingIntent = PendingIntent.getService( this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT );

    alarmManager.cancel( pendingIntent );

    SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);
    SharedPreferences.Editor editor = pref.edit();
    editor.putLong( IterationActivity.SAVED_STATE_SELECTED_DURATION, 0 );
    editor.apply();
}

    And with that we should have a fully functioning basic Android Wear application. Devices go on sale on the 7th, and I can honestly say from my few days using one it has been useful and will only get more useful as developers make additional apps.

11 comments:

  1. Could you present the tutorial as a video to clarify more

    ReplyDelete
    Replies
    1. No, I'll leave that as an exercise for someone else with more time. What part isn't clear for you?

      Delete
    2. Part of Android Studio, also some confusion about place of some lines such as calling the methods presented in the tutorial

      Delete
    3. What do you mean by "part of Android Studio"? The whole thing is in Android Studio, so you need to be a bit more specific :) As for where the method calls are done, the files are mentioned before the code snippets, but you can also find them in the source that is presented in the GitHub link in the tutorial or here:

      https://github.com/PaulTR/AndroidDemoProjects/tree/master/StayAwake

      Delete
    4. Github! nice, github will solve my confusion, sorry I had not noticed
      Thank you

      Delete
  2. What is the benefit from rect_activity_iteration and round_activity_iteration?

    ReplyDelete
    Replies
    1. Nothing in _this_ app, the rect layout file is used for watches with the rectangular form factor, and round is used for round watches (like the Moto 360). If you don't have both, you won't be able to install the app on devices with that form factor. It's also important to differentiate them when you have more style intensive apps. This app is just a card notification and a listview, so it's not as big of a deal.

      Delete
  3. Thanks for your post. i want to develop app for android wear device this post very useful for i get knowledge about how to create a app in android wear device

    ReplyDelete
  4. Introduction to Official Android Wear Development Tutorial - Nice article.

    ReplyDelete
  5. Excellent and helpful tutorial on Android wear like android watch . This has been a so interesting read, would love to read more here….

    ReplyDelete