Be My Valentine (or my first real Android app)
It all started with trying to find a way to top the most memorable Valentine’s Day with my wife. She always talks about the first one after we were married in which I hired a barbershop quartet to serenade her at the front door of our apartment. She claims they sang a song that was a key fixture in our wedding itself, a Victorian song that is also popular with barbershop quartets around Valentine’s Day. I consider it a lucky break.
I managed to (not so subtly) pry the name of the song from her a few weeks ago and found (what I hope) is an mp3 recording of a barbershop quartet performing it. Of course, I can’t verify this because I can’t let her listen to it without tipping my hand. However, even if it’s not the right one, it’s at least a romantic song sung by a barbershop quartet with the same name.
So, what to do with it, then? I finally came up with the idea to write an Android app using the song and a few other elements to create a customized Valentine’s Day greeting card app.
The kids got new Nintendo DSi portable gaming devices for Christmas with an app called Flipnote which allows them to create animations and share them with their friends. This has been the biggest fun for them and I found out that you can export the frames of the animations as individual GIF files to an SD card. It just so happens that Android supports the AnimationDrawable which is defined in XML like so:
android:oneshot="true">
<item android:drawable="@drawable/b1001" android:duration="1" />
<item android:drawable="@drawable/b1002" android:duration="200" />
<item android:drawable="@drawable/b1003" android:duration="200" />
.
.
.
<item android:drawable="@drawable/b1068" android:duration="20000" />
</animation-list>
The “@drawable/b1004″ strings will resolve the the individual GIF files in the res/drawables directory in the android package. In this case they are called b1001.gif, b1002.gif, b1003.gif, etc. Also, the animation defined in the XML can be addressed as a @drawable itself and can be controlled in code like so:
public void onCreate(Bundle savedInstanceState) {
Log.d(TAG,"Beginning onCreate");
Log.d(TAG,"Calling super");
super.onCreate(savedInstanceState);
Log.d(TAG,"Allocating ImageView animation");
animationImage = (ImageView) findViewById(R.id.imageView1);
animationImage.setBackgroundResource(R.drawable.animation);
Animation = (AnimationDrawable) animationImage.getBackground();
Log.d(TAG,"Allocating AnimationDrawable Animation");
Log.d(TAG,"Ending onCreate");
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG,"Starting onTouchEvent");
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// Start animation
Log.d(TAG,"Stopping Animation playback using touch event");
Animation.stop();
Log.d(TAG,"Starting Animation playback using touch event");
Animation.start();
return true;
}
Log.d(TAG,"Calling Super");
Log.d(TAG,"Ending onTouchEvent");
return super.onTouchEvent(event);
}
The next resource I need is the actual song itself to play. For this, I placed the mp3 file into the res/raw folder and now I can reference it using the generated R class (in this case R.raw.songname in place of res/raw/songname.mp3).
Log.d(TAG,"Starting playAudio");
Log.d(TAG,"Allocating MedaPlayer");
mp = MediaPlayer.create(this.getApplication(), R.raw.song);
Log.d(TAG,"Starting playback");
mp.start();
Log.d(TAG,"Ending playAudio");
}
The playAudio() function is called anytime in the application lifecycle that the the music should start playing. It’s important to note here that at this point in the development I started to run into numerous OutOfMemory exceptions and that’s the reason there are so many calls to Log.d which outputs to the debug console in Eclipse. At first I thought the cause of these was the mediaplayer, but it turned out to be the animation.
The garbage collector cannot reclaim memory unless all references to an object are removed and with a 16MB heap, it looks like all of those GIF images was using up a pretty big chunk. There wasn’t much room for a reference to be left behind.
As I soon discovered through a lot reading of SDK documentation and forums, the ImageView and the animation itself requires a little bit of extra handling in onPause() and onDestroy(). The ImageView itself retains a reference to the animation resource in it’s callback and so the callbacks must be set to null and garbage collection initiated:
protected void onDestroy() {
Log.d(TAG,"Beginning onDestroy");
Log.d(TAG,"Calling Super");
super.onDestroy();
if (mp != null) {
Log.d(TAG,"Releasing mediaplayer resource");
mp.release();
mp = null;
}
if (animationImage != null) {
Log.d(TAG,"Release animationImage resource");
animationImage.setBackgroundDrawable(null);
animationImage = null;
}
if (Animation != null) {
Log.d(TAG,"Stopping and freeing up Animation resource");
Animation.stop();
Animation.setCallback(null);
Animation = null;
}
Log.d(TAG,"Initiating Garbage Collection");
System.gc();
Log.d(TAG,"ending onDestroy");
}
The final resource I need to manage is an HTML-escaped string displayed in a ScrollView which contains a heartfelt message to the love of my life.
TextView notes = (TextView) findViewById(R.id.textView1);
Log.d(TAG,"Getting String Resources");
Resources res = getResources();
Log.d(TAG,"Formatting note with HTML");
String text = String.format(res.getString(R.string.note));
CharSequence styledText = Html.fromHtml(text);
Log.d(TAG,"Assigning formatted text to TextView");
notes.setText(styledText);
At this point, the application is completely functional. I can install it on a device using a debug key and launch it from the icon in the application manager. I can tap the animation and it plays, the music plays when the application is on-screen, and the text is scrollable. The application also functions in both orientations. However, keep in mind, that I developed the app on my Motorola Droid X for my wife’s Motorola Droid and the app is not for general release so I probably made a lot of assumptions about device capabilities.
There is one final feature that I really wanted to add to make it fully functional. At a given date and time (February 14, 2011 7:00am), I wanted a notification to appear with a heart icon and the text “Be My Valentine”. When she sees that and selects the notification, I want the app to start and do its thing. When the day is over, I want the notification to no longer show up. As it turns out, implementing this one feature was nearly as much work as all the entire app itself.
There are several pieces to this: an alarm, a service, a receiver, and a binder.
First in the UI class’s onCreate(), I need to start the service I created to handle notifications using an alarm at a particular time:
// with the alarm manager.
Log.d(TAG,"Creating AlarmSender");
mAlarmSender = PendingIntent.getService(this,
0, new Intent(BeMyValentine.this, BeMyValentineService.class), 0);
Log.d(TAG,"Setting time for alarm");
Calendar calendar = GregorianCalendar.getInstance();
calendar.set(Calendar.YEAR,2011);
calendar.set(Calendar.MONTH,Calendar.FEBRUARY);
calendar.set(Calendar.DAY_OF_MONTH,14);
calendar.set(Calendar.HOUR_OF_DAY,7);
calendar.set(Calendar.MINUTE,00);
Log.d(TAG,calendar.getTime().toString());
// Schedule the alarm!
Log.d(TAG,"Scheduling alarm");
// Schedule the alarm!
AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE);
am.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), mAlarmSender);
I also need a broadcast receiver to handle broadcasts sent to my application. Specifically, the broadcast generated by the alarm which will start the service.
private static final String TAG = "BeMyValentine";
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG,"Receiver: starting onReceive");
// we start notification service on start up
Log.d(TAG, "Receiver: creating intent for service");
Intent startActivityIntent = new Intent(context, BeMyValentineService.class);
Log.d(TAG, "Receiver: starting service");
context.startService(startActivityIntent );
Log.d(TAG, "Receiver: ending onReceive");
}
}
Now for the real workhorse of this feature, the BeMyValentineService. In the service’s onCreate(), I call a function to display the notification by creating an intent to launch the app and calling the system notification service to display it (this will quickly disappear if the date not February 14, 2011):
* Show a notification while this service is running.
*/
private void showNotification() {
// Set up Notification
Log.d(TAG, "Service: Creating intent");
Intent intent = new Intent(this,BeMyValentine.class);
mManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Log.d(TAG,"Service: Creating notification");
Notification notification = new Notification(R.drawable.icon,
"Notify", System.currentTimeMillis());
notification.setLatestEventInfo(this,
"Be My Valentine","Select for a special message",
PendingIntent.getActivity(this.getBaseContext(), 0, intent,
PendingIntent.FLAG_CANCEL_CURRENT));
Log.d(TAG,"Service: Sending notification notification to manager");
mManager.notify(APP_ID, notification);
Log.d(TAG,"Service: ending onCreate");
}
Next, I create and start a thread whose job it is to wait until a specific time and tell the service to shut itself down:
* The function that runs in our worker thread
*/
Runnable mTask = new Runnable() {
public void run() {
Log.d(TAG,"Setting time for alarm");
Calendar cal = GregorianCalendar.getInstance();
cal.set(Calendar.YEAR,2011);
cal.set(Calendar.MONTH,Calendar.FEBRUARY);
cal.set(Calendar.DAY_OF_MONTH,14);
cal.set(Calendar.HOUR_OF_DAY,23);
cal.set(Calendar.MINUTE,59);
Log.d(TAG,cal.getTime().toString());
long endTime = cal.getTimeInMillis();
while (System.currentTimeMillis() < endTime) {
synchronized (mBinder) {
try {
mBinder.wait(endTime - System.currentTimeMillis());
} catch (Exception e) {
}
}
}
// Done with our work... stop the service!
BeMyValentineService.this.stopSelf();
}
};
Of course, part of the service shutting down is to cancel the notification:
public void onDestroy() {
Log.d(TAG,"Service: starting onDestroy");
mManager.cancel(APP_ID);
Log.d(TAG,"Service: calling Super");
super.onDestroy();
Log.d(TAG, "Service: ending onDestroy");
}
Finally, the glue that holds it all together is the binder object which synchronizes the work of the thread (it’s returned in the service’s onBind() function).
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply,
int flags) throws RemoteException {
return super.onTransact(code, data, reply, flags);
}
};
One final note. I’ve only posted sections of code use to highlight particular challenges I had to solve. There are a lot of smaller pieces that are used to put it all together and I relied heavily on the SDK documentation, the SDK sample code, StackOverflow, and other forums.
Feel free to contact me if you want more information or help with solving a similar problem.
