📜 ⬆️ ⬇️

Some pitfalls of Android development

Recently, our team completed the development of an application for Android. In the development process and then support, we encountered some technical problems. Some of them are our bugs, which we could have avoided, the other part are quite 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 a few real bugs that have arisen among our users and talk about ways to solve them.

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

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


Since the development of Android should be borne in mind that your application can work on a huge number of different devices, then you need to think about the problems of their compatibility.

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

java.lang.NoClassDefFoundError: android.util.Patterns 


And the reason is simple, according to the documentation, the android.util.Patterns class is available starting from 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 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 fact 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 flow, as this may take some time and the main flow is blocked and does not respond to other events. We tried to avoid such behavior, but in one of the places there was still a simple network call. This problem was solved by the fact that we moved this code to another thread.

Rotate the device - what could be easier 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 loads a list of something and displays it on the screen. Those. when the Activity starts (in the onCreate() method), we start the stream (not to block the UI stream), which will load the data. This thread runs for a while, so we will display the boot process in the ProgressDialog . Everything is simple and works great.

But, after loading, the user turned the device and here we find out that ProgressDialog appeared again and we are loading our data again. But nothing has changed? It's just easier for a person to look at this list by turning the device.

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

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

Do not handle screen rotation with onConfigurationChanged() ! This is not true! Official documentation clearly states that “ using this attribute should not be avoided. “ 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 turning the screen, Android will call the onRetainNonConfigurationInstance() method of your Activity . And you can return data (for example, a list of loaded objects) from this method, which will be saved between 2 calls of the onCreate() method of your Activity . Then when you call onCreate() you can call getLastNonConfigurationInstance() which will return the saved data to you. Those. when creating an Activity you call getLastNonConfigurationInstance() and if it returned data to you, then this data has already been loaded and you only need to display it. If you did not return the data, then start the download.

But in practice, the situation is as follows. We can have 2 options when the user turns the device. The first option is when data is being loaded (our stream is working which loads data and ProgressDialog displayed) or the data has already been loaded and we saved it in the list for display. It turns out that in the first case, when turning, we must save the link to the working thread, and in the second case, the link to the list that has already been downloaded. We do that.

But this complicates our code and does not seem to me simple and intuitive. Moreover, when the screen orientation changes and we save the data load stream, we are then forced to restore ProgressDialog again with onCreate() ! And if we add here that in our application the user can download data from different places and we have more than one data download stream, but several — the amount of code that serves a simple screen rotation becomes huge.

Honestly, I do not understand why it was made so difficult.

A little more detail about threads or using AsyncTask.


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

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

Its essence is that it already has ready methods onPreExecute() and onPostExecute(Result) , which are executed in the main UI thread and which serve to display something and there is a method doInBackground (Params ...) within which the main work and it starts in a separate thread automatically. Here is an example code like this:

 private class MyTask extends AsyncTask<Void, Void, Void> { 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 the error from the 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 onPreExecute() method, this ProgressDialog created with reference to MyActivity already inactive and is not on the screen! Well, I still understand how this can be when calling onPostExecute() . Those. for example, while we were loading data, the user clicked Back and our ProgressDialog left the screen. But how can this be immediately when a load is called, when this code starts right after the activity is closed - this is unclear to me.

But in any case, we have to handle such situations, so we decided to do this with a shell of the spinner.show spinner.show() and spinner.dismiss() methods in try/catch . The solution is certainly not very beautiful, but in our case it is quite functional.

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

Add here another problem that was described earlier. That when turning the device, it is necessary to recreate ProgressDialog if there was a turn during data loading. This will also add auxiliary code here.

And it is worth remembering that the doInBackground() method is executed in a separate thread and therefore if an error occurred while loading data, then you cannot issue an 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 in the onPostExecute(Void result) method, you can already show something.

Those. again a lot of auxiliary code and not everything is so simple ...

Alarmmanager


And then there are moments that are not described in the documentation at all. 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 some time when our application itself is already closed.

This AlarmManager is a very useful thing, but the 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 himself kills the application through the task manager (which is possible and completely normal), then ALL notifications for this application in the AlarmManager REMOVE! What a surprise! Especially, I liked one of the comments there: “Re Alarm canceled: Thanks for the clarification. I’ve asked you to see what the team was doing. Is it documented anywhere? "

So now when the application starts, we are forced to re-create customized notifications, since even there is no API in the AlarmManager to check if there are such notifications or not.

Sometimes it happens...


Here are some more errors that we have registered with our users. In our application, we use OAuth authentication for various social networks and therefore have to launch a regular browser on Android (through which OAuth should work). At the same time, he 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) 


One more:

 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 all of the above, I liked the development of Android. Basically, the API is quite thoughtful and convenient.

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

And more ... Be sure, no - not so, but like this - MANDATORY 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!

Source: https://habr.com/ru/post/146632/


All Articles