Some “pitfalls” of development for Android

Our team recently completed the development of an Android application. In the process of development and then support, we encountered some technical problems. Some of them are our bugs, which we could avoid, another part is completely unobvious features of Android, which are either poorly described in the documentation or not described at all.

In this article, I would like to consider several real bugs that have occurred among our users and talk about ways to solve them.

The article does not pretend to be a detailed analysis of potential problems, it's just a story from the life of one real Android application.


RTFM ( http://en.wikipedia.org/wiki/RTFM )


Since when developing for Android, you need to keep in mind that your application can run on a huge number of different devices, you need to think about compatibility issues.

For example, one of the errors that occurred to our users:

java.lang.NoClassDefFoundError: android.util.Patterns


And the reason is simple, according to the documentation, the class android.util.Patternsis available starting with API version 8 (Android 2.2.x), and the user had version 2.1. We decided this of course by wrapping this code in try/catch.

Here is another similar problem caused by inattentive reading of the documentation:

android.os.NetworkOnMainThreadException
at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1077)
at java.net.InetAddress.lookupHostByName(InetAddress.java:477)
at java.net.InetAddress.getAllByNameImpl(InetAddress.java:277)
at java.net.InetAddress.getAllByName(InetAddress.java:249)


The thing is that strict mode ( http://developer.android.com/reference/android/os/StrictMode.html ) has been enabled by default in Android since version 3.0. This means that your application cannot access the network directly from the main UI thread, as this can take some time and the main thread is blocked and does not respond to other events. We tried to avoid such behavior, but in one place a simple network call remained. The problem was solved by the fact that we moved this code to another thread.

Rotate the device - what could be simpler and more familiar to the user?


It would seem that for mobile devices, changing the screen orientation is a thing so often used and familiar that it should be reflected in the API. Those. this situation should be handled very simply. But no. There are many nuances.

Suppose we need to make an application that downloads a list of something and displays it on the screen. Those. at startup Activity (in the method onCreate()) we start the stream (so as not to block the UI stream), which will load the data. This thread has been running for some time, so we will display the download process in ProgressDialog. Everything is simple and wonderful.

But, after loading, the user turned the device and here we find that it reappearedProgressDialog and we upload our data again. But nothing has changed? It's just more convenient for a person to look at this list by turning the device.

And the thing is that the method onCreate()is called not only during creation Activity, but also when the screen is rotated! But we do not want to force the user to wait again for the download. All we need to do is to show the already loaded data again.

If we search the Internet, we will find many links to a description of this problem and also a huge number of examples of how to solve this problem. And the worst part is that most of these links offer the wrong solution. Here is an example of this - http://codesex.org/articles/33-android-rotate

Don't do screen rotation processing through onConfigurationChanged()! This is not true! The official documentation clearly states that “Using this attribute should be avoided and used only as a last-resort. Http://developer.android.com/guide/topics/manifest/activity-element.html#config

But the correct approach is described here - http://developer.android.com/guide/topics/resources/runtime-changes .html And as practice shows, it is not easy to implement.

The idea is that before the screen is rotated, Android will call onRetainNonConfigurationInstance()your method Activity. And you can return data (for example, a list of loaded objects) from this method, which will be saved between 2 calls to onCreate()your method Activity. Then, when called, onCreate()you can call getLastNonConfigurationInstance()which and will return the saved data to you. Those. when creating Activity you callgetLastNonConfigurationInstance()and if he returned the data to you, then this data has already been downloaded and you only need to display it. If you did not return the data, then start the download.

But in practice, the situation looks like this. We can have 2 options when the user rotates the device. The first option is when the data is loading (our stream is working which loads the data and is displayed at the same time ProgressDialog) or the data is already loaded and we saved them in the list for display. It turns out that in the first case, when turning, we must save a link to a working stream, and in the second case, a link to an already loaded list. We do so.

But this complicates our code and does not seem simple and intuitive to me. Moreover, when there is a change in screen orientation and we have saved the data loading stream, we are then forced toonCreate()restore again ProgressDialog! And if you add here that in our application the user can download data from different places and we have not one data loading stream, but several - the amount of code that serves a simple screen rotation becomes simply huge.

Honestly, I don’t understand why this was done so hard.

A bit more about streams or using AsyncTask.


Let's look at loading data in another stream in a little more detail, since at the same time unexpected “surprises” awaited us.

A bit of theory: to facilitate the creation and operation of a thread other than the main UI thread, a special class was made - AsyncTask ( http://developer.android.com/reference/android/os/AsyncTask.html )

The essence of it is that it contains there are already ready methods onPreExecute()and onPostExecute(Result)which are executed in the main UI thread and which serve to display something and there is a doInBackground (Params ...) method inside which the main work is done and it starts automatically in a separate thread. Here is sample code for what it looks like:


        private class MyTask extends AsyncTask {
                private ProgressDialog spinner;
                @Override
                protected void onPreExecute() {
                        // Вначале мы покажем пользователю ProgressDialog 
                        // чтобы он понимал что началась загрузка
                        // этот метод выполняется в UI потоке
                        spinner = new ProgressDialog(MyActivity.this);
                        spinner.setMessage("Идет загрузка...");
                        spinner.show();
                }
                @Override
                protected Void doInBackground(Void... text) {
                        // тут мы делаем основную работу по загрузке данных 
                        // этот метод выполяется в другом потоке
                }
                @Override
                protected void onPostExecute(Void result) {
                        // Загрузка закончена. Закроем ProgressDialog.
                        // этот метод выполняется в UI потоке
                	spinner.dismiss();
                }
        }


Everything is simple and beautiful.

But now, a little practice. Here is an error from a real user:

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@40515bd0 is not valid; is your activity running?
        at android.view.ViewRoot.setView(ViewRoot.java:534)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:177)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:91)
        at android.view.Window$LocalWindowManager.addView(Window.java:424)
        at android.app.Dialog.show(Dialog.java:241)
        at ru.reminded.android.social.SocialDataLoader.onPreExecute(SocialDataLoader.java:106)
        at android.os.AsyncTask.execute(AsyncTask.java:391)
        at ru.reminded.android.util.SocialAdapterUtils.loadAdapterData(SocialAdapterUtils.java:52)
        at ru.reminded.android.util.SocialAdapterUtils.access$0(SocialAdapterUtils.java:50)
        at ru.reminded.android.util.SocialAdapterUtils$1.onComplete(SocialAdapterUtils.java:41)


And this error means that when we call spinner.show()in the method onPreExecute(), this one ProgressDialog created with the link to is MyActivity already inactive and it is not on the screen! Okay, I still understand how this can be when called onPostExecute(). Those. for example, while we were loading data, the user clicked Back and ours ProgressDialogleft the screen. But how this can happen right away when a load is called, when this code starts immediately when it Activityis turned off , is not clear to me.

But in any case, we must handle such situations, so we decided this with the help of methods spinner.show()and spinner.dismiss()c try/catch. The solution, of course, is not very beautiful, but in our case it is quite functional.

By the way, there is the same code for example in the Facebook SDK for Android, which was developed by other experienced developers. And there, too, we had situations when the application crashed when closed ProgressDialog. We had to add processing to their code. So this problem is not only in us.

Add here the problem that was described earlier. That when turning the device it is necessary to recreate ProgressDialogif the turn was during data loading. This will also add helper code here.

And it’s worth remembering that the method doInBackground()is executed in a separate thread, and therefore if an error occurred while loading the data, then you cannot issue Alert directly from there, since this is not a UI stream. It is necessary to save the error, and then after exiting the load stream onPostExecute(Void result), you can already show something in the method .

Those. again, a lot of supporting code and not so simple ...

Alarmmanager


And there are moments that are not described at all in the documentation. For example, in our application we use AlarmManager ( http://developer.android.com/reference/android/app/AlarmManager.html ) which helps us to issue messages to the user after a while when our application itself is already closed.

This AlarmManager is a very useful thing, the only problem is that sometimes it “loses” the notifications created in it! We spent a lot of time to understand how and why this happens, rummaged through all the documentation and found nothing. Quite by chance, we “came across” this discussion - http://stackoverflow.com/questions/9101818/how-to-create-a-persistent-alarmmanager .

It turns out that if the application crashes, or the user kills the application through the task manager (which is possible and quite usual), then ALL notifications for this application in the AlarmManager are DELETED! What a surprise! I especially liked one of the comments there: “Re Alarm canceled: Thanks for the clarification. I asked this on the Android team office hours g + hangout and they even seemed confused about this behavior. Is it documented anywhere?

So now at the start of the application we are forced to recreate the configured notifications, since there is not even an API in AlarmManager to check whether there are such notifications or not.

Sometimes it happens...


Here are a few more errors that we registered with our users. In our application, we use OAuth authentication for various social networks and therefore are forced to launch a full-time browser in Android (through which OAuth should work). At the same time, it periodically “falls”


android.util.AndroidRuntimeException: { what=1004 when=-14ms arg2=1 } This message is already in use.
        at android.os.MessageQueue.enqueueMessage(MessageQueue.java:187)
        at android.os.Handler.sendMessageAtTime(Handler.java:457)
        at android.os.Handler.sendMessageDelayed(Handler.java:430)
        at android.os.Handler.sendMessage(Handler.java:367)
        at android.os.Message.sendToTarget(Message.java:349)
        at android.webkit.WebView$5.onClick(WebView.java:1250)
        at com.android.internal.app.AlertController$ButtonHandler.handleMessage(AlertController.java:172)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:130)
        at android.app.ActivityThread.main(ActivityThread.java:3703)
        at java.lang.reflect.Method.invokeNative(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:507)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:841)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:599)
        at dalvik.system.NativeStart.main(Native Method)


Another:


java.lang.NullPointerException
        at android.os.Message.sendToTarget(Message.java:348)
        at android.webkit.WebView$4.onClick(WebView.java:1060)
        at com.android.internal.app.AlertController$ButtonHandler.handleMessage(AlertController.java:158)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:123)
        at android.app.ActivityThread.main(ActivityThread.java:4627)
        at java.lang.reflect.Method.invokeNative(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:521)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:860)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:618)
        at dalvik.system.NativeStart.main(Native Method)


And further:


java.lang.IndexOutOfBoundsException: setSpan (-1 ... -1) starts before 0
        at android.text.SpannableStringBuilder.checkRange(SpannableStringBuilder.java:949)
        at android.text.SpannableStringBuilder.setSpan(SpannableStringBuilder.java:522)
        at android.text.SpannableStringBuilder.setSpan(SpannableStringBuilder.java:514)
        at android.text.Selection.setSelection(Selection.java:74)
        at android.text.Selection.setSelection(Selection.java:85)
        at android.widget.TextView.performLongClick(TextView.java:8621)
        at android.webkit.WebTextView.performLongClick(WebTextView.java:617)
        at android.webkit.WebView.performLongClick(WebView.java:4471)
        at android.webkit.WebView$PrivateHandler.handleMessage(WebView.java:8285)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:150)
        at android.app.ActivityThread.main(ActivityThread.java:4389)
        at java.lang.reflect.Method.invokeNative(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:507)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:849)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:607)
        at dalvik.system.NativeStart.main(Native Method)


Conclusion


Despite everything described above, I liked developing Android. Basically, the API is well thought out and convenient.

That's just everywhere worth using try/catch. Even where it is not at all obvious.

And yet ... Necessarily, no - not like that, but like this - ALWAYS collect information about errors from users. We use the free ACRA library ( http://code.google.com/p/acra/ ) for this . Thank you very much to its developers!

Also popular now: