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.
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();
}
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.