A Programmer's Ride

Tuesday, July 19, 2011

On most devices, using AdMob in an obfuscated application (using ProGuard) works perfectly fine. However, on a few devices (I don't know which ones), you'll get a java.lang.NoSuchMethodError and your users will be angry. Here's the full stack trace:
java.lang.RuntimeException: An error occured while executing doInBackground()
at android.os.AsyncTask$3.done(AsyncTask.java:200)
at java.util.concurrent.FutureTask$Sync.innerSetException(FutureTask.java:273)
at java.util.concurrent.FutureTask.setException(FutureTask.java:124)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:307)
at java.util.concurrent.FutureTask.run(FutureTask.java:137)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1068)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:561)
at java.lang.Thread.run(Thread.java:1102)
Caused by: java.lang.NoSuchMethodError: com.google.gson.Gson.a
at c.a(Unknown Source)
at c.doInBackground(Unknown Source)
at android.os.AsyncTask$2.call(AsyncTask.java:185)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:305)
... 4 more
When I first got this report I quickly realized it wasn't my fault, since I don't use gson. To cut to the chase, here's what I put in my procfg.txt to fix it (at least for now):
-keep public class com.google.ads.** {
public protected *;
}
-keep public class com.google.gson.** {
public protected *;
}

Friday, October 29, 2010

One of the key features of Android is the ability for apps to provide notifications that pop up in the status bar. I'll try to explain in this post how you should implement a Service component that polls a web service regularly to check for updates.

The first thing you need is a preferences activity, where the user can set what notifications they want and how often they want your app to check for them. This is easily achieved using a PreferenceActivity, and is not the subject of this post. If you want to learn more on how to create such an activity, click on the aforementioned link.

Once you have your preferences activity, it's time to write your Service. Here are a few key points that you should always respect. I will go into more detail on each one shortly.

Do your work in a separate thread. It can happen that your Service will run on the UI thread as one of your activities.

Obtain a partial wake lock when starting, and release it when you're done. If you don't do this, the system might (and will) kill your service while your background thread is running.

Call stopSelf() when you're done, in order to free resources.

Tell the system to not start your Service again if it's killed for more resources.

To explain all of these, I'll provide a skeleton Service that does everything mentioned above. All you have to do is fill in the work specific to your project:

publicclassNotificationServiceextendsService{privateWakeLockmWakeLock;/** * Simply return null, since our Service will not be communicating with * any other components. It just does its work silently. */@OverridepublicIBinderonBind(Intentintent){returnnull;}/** * This is where we initialize. We call this when onStart/onStartCommand is * called by the system. We won't do anything with the intent here, and you * probably won't, either. */privatevoidhandleIntent(Intentintent){// obtain the wake lockPowerManagerpm=(PowerManager)getSystemService(POWER_SERVICE);mWakeLock=pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,Const.TAG);mWakeLock.acquire();// check the global background data settingConnectivityManagercm=(ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE);if(!cm.getBackgroundDataSetting()){stopSelf();return;}// do the actual work, in a separate threadnewPollTask().execute();}privateclassPollTaskextendsAsyncTask<Void,Void,Void>{/** * This is where YOU do YOUR work. There's nothing for me to write here * you have to fill this in. Make your HTTP request(s) or whatever it is * you have to do to get your updates in here, because this is run in a * separate thread */@OverrideprotectedVoiddoInBackground(Void...params){// do stuff!returnnull;}/** * In here you should interpret whatever you fetched in doInBackground * and push any notifications you need to the status bar, using the * NotificationManager. I will not cover this here, go check the docs on * NotificationManager. * * What you HAVE to do is call stopSelf() after you've pushed your * notification(s). This will: * 1) Kill the service so it doesn't waste precious resources * 2) Call onDestroy() which will release the wake lock, so the device * can go to sleep again and save precious battery. */@OverrideprotectedvoidonPostExecute(Voidresult){// handle your datastopSelf();}}/** * This is deprecated, but you have to implement it if you're planning on * supporting devices with an API level lower than 5 (Android 2.0). */@OverridepublicvoidonStart(Intentintent,intstartId){handleIntent(intent);}/** * This is called on 2.0+ (API level 5 or higher). Returning * START_NOT_STICKY tells the system to not restart the service if it is * killed because of poor resource (memory/cpu) conditions. */@OverridepublicintonStartCommand(Intentintent,intflags,intstartId){handleIntent(intent);returnSTART_NOT_STICKY;}/** * In onDestroy() we release our wake lock. This ensures that whenever the * Service stops (killed for resources, stopSelf() called, etc.), the wake * lock will be released. */publicvoidonDestroy(){super.onDestroy();mWakeLock.release();}}

You now have a Service that works as it should, awesome. But how do you start it? You use the AlarmManager, of course (I suggest you read it's documentation if you haven't already). But how and when do you schedule it? How do you handle app updates? How do you handle device reboots? Well, here are some guidelines that I hope will make answering these questions much easier:

Use repeating alarms so that you don't have to always re-schedule yourself in your Service.

Use inexact repeating alarms so that the system can group together multiple alarms in order to preserve battery life.

Always cancel an alarm before resetting it.

When receiving the BOOT_COMPLETED signal, don't immediately start your Service. This would slow down the device too much (it already has a lot of work to do when it boots). Instead, set an alarm.

What I personally do is set an alarm whenever onResume() is called on my main Activity (the one that is launched when the user clicks on the app icon in their launcher), and in a BroadcastReceiver that handles the BOOT_COMPLETED event. Here's some code to help you understand:

BootReceiver.java

publicvoidonReceive(Contextcontext,Intentintent){// in our case intent will always be BOOT_COMPLETED, so we can just set// the alarm// Note that a BroadcastReceiver is *NOT* a Context. Thus, we can't use// "this" whenever we need to pass a reference to the current context.// Thankfully, Android will supply a valid Context as the first parameterSharedPreferencesprefs=PreferenceManager.getDefaultSharedPreferences(context);intminutes=prefs.getInt("interval");AlarmManageram=(AlarmManager)context.getSystemService(Context.ALARM_SERVICE);Intenti=newIntent(context,NotificationService.class);PendingIntentpi=PendingIntent.getService(context,0,i,0);am.cancel(pi);// by my own convention, minutes <= 0 means notifications are disabledif(minutes>0){am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,SystemClock.elapsedRealtime()+minutes*60*1000,minutes*60*1000,pi);}}

Finally, you've got yourself a fully functional notifications service that stops and starts whenever it should, that behaves the way the user wants it to behave and that plays nicely with the system. This took some researching, so if I saved a bit of your time, leave a comment :)

Friday, August 13, 2010

While working on an app, I had an activity that downloaded an image from the Internet and displayed it to the user (amongst other things). Because all of the data presented to the user is downloaded from the Internet, it's mandatory for me to save this data in onSaveInstanceState() and restore it in onCreate().

At first, I had a problem. I was downloading the image to a Drawable, using Drawable.createFromStream(). Now, because a Drawable is dependent on its Context, it can't be serialized or parceled.

The solution was to use a Bitmap instead, which thankfully implements Parcelable. Downloading a Bitmap is easy:

Friday, July 30, 2010

I recently wanted the title of my activity to scroll like a marquee whenever it was too long to fit in its space (because it is fetched from the net and can be pretty much anything). I didn't want to use a custom multi-line title view because that would waste screen space and because I want to preserve the native look and feel. The solution I've found is rather hackish, but it works. Here goes the code, with explanatory comments and all:

// make the title scroll!// find the title TextViewTextViewtitle=(TextView)findViewById(android.R.id.title);// set the ellipsize mode to MARQUEE and make it scroll only oncetitle.setEllipsize(TruncateAt.MARQUEE);title.setMarqueeRepeatLimit(1);// in order to start strolling, it has to be focusable and focusedtitle.setFocusable(true);title.setFocusableInTouchMode(true);title.requestFocus();

Saturday, June 5, 2010

Here we go, another Java vs. Python comparison (I just can't help myself). This time it's about standard library usefulness in doing certain tasks. Fetching the contents of a URL should be a trivial one, but in Java, it's not. Especially if the contents of that URL are gzipped and use a nice charset such as UTF-8.

Not only is Java slower, it's also a lot more code. And I'm not only talking about lines of code here (which, still, are 6 times more; really it's 1 line of Python and 6 of Java -- if you remove the whitespace), but compiled code, too. Look:

Monday, May 24, 2010

I used to have a home server which I playfully named kitty, mainly because it was not very powerful, yet it was very fun to play with. Today, I shut it down to replace it with a newer machine. These were its last words:

Broadcast message from felix@kitty
(/dev/pts/0) at 12:00 ...
The system is going down for power off NOW!
Meow? :(
felix@kitty:~$
Connection to kitty closed by remote host.