1

Here is the basic life cycle of my application. It targets SDK version 8 by now, since I am still running Android 2.3.3 on my device.

  • The application starts, onResume() is called
    The method show() is called to display cached data.
  • A background service gets started which downloads and stores data. It uses AsyncTask instances to accomplish its work.
  • One of the tasks stores downloaded data in a SQLite database.
  • A broadcast intent is sent in onPostExecute() when the storing task has finished.
  • The MapActivity receives the intent and handles it.
    The method show() is called to display cached and new data.

Within the method show() the map view gets invalidated after the overlay has been added. This works fine when show() has been called from the MapActivity itself. It raises an exception, however, when the asynchonous task is the source of the method call (indirectly).

As far as I understand, I am at the UI thread when I trigger show() in both cases. Is this true?

    public class CustomMapActivity extends MapChangeActivity {

        private boolean showIsActive = false;

        private BroadcastReceiver mReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (intent.getAction().equals(IntentActions.FINISHED_STORING)) {
                    onFinishedStoring(intent);
                }
            }
        };

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            registerReceiver(mReceiver, new IntentFilter(IntentActions.FINISHED_STORING));
        }

        @Override
        protected void onResume() {
            super.onResume();
            show();
        }

        @Override
        protected void onMapZoomPan() {
            loadData();
            show();
        }

        @Override
        protected void onMapPan() {
            loadData();
            show();
        }

        @Override
        protected void onMapZoom() {
            loadData();
            show();
        }

        private void onFinishedStoring(Intent intent) {
            Bundle extras = intent.getExtras();
            if (extras != null) {
                boolean success = extras.getBoolean(BundleKeys.STORING_STATE);
                if (success) {
                    show();
                }
        }

        private void loadData() {
            // Downloads data in a AsyncTask
            // Stores data in AsyncTask
        }

        private void show() {
            if (showIsActive) {
                return;
            }
            showIsActive = true;
            Uri uri = UriHelper.getUri();
            if (uri == null) {
                showIsActive = false;
                return;
            }
            Cursor cursor = getContentResolver().query(uri, null, null, null, null);
            if (cursor != null && cursor.moveToFirst()) {
                List<Overlay> mapOverlays = mapView.getOverlays();
                CustomItemizedOverlay overlay = ItemizedOverlayFactory.getCustomizedOverlay(this, cursor);
                if (overlay != null) {
                    mapOverlays.clear();
                    mapOverlays.add(overlay);
                }
            }
            cursor.close();
            mapView.invalidate(); // throws CalledFromWrongThreadException
            showIsActive = false;
        }

    }

Here is the stack trace ...

android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
    at android.view.ViewRoot.checkThread(ViewRoot.java:3020)
    at android.view.ViewRoot.invalidateChild(ViewRoot.java:647)
    at android.view.ViewRoot.invalidateChildInParent(ViewRoot.java:673)
    at android.view.ViewGroup.invalidateChild(ViewGroup.java:2511)
    at android.view.View.invalidate(View.java:5332)
    at info.metadude.trees.activities.CustomMapActivity.showTrees(CustomMapActivity.java:278)
    at info.metadude.trees.activities.CustomMapActivity.onMapPan(CustomMapActivity.java:126)
    at info.metadude.trees.activities.MapChangeActivity$MapViewChangeListener.onChange(MapChangeActivity.java:50)
    at com.bricolsoftconsulting.mapchange.MyMapView$1.run(MyMapView.java:131)
    at java.util.Timer$TimerImpl.run(Timer.java:284)

Note: I use the MapChange project in order to receive notifications on map events.


EDIT:

From what I now read in the documentation about AsyncTask (scroll down a bit), I am not sure if I use it the correct way. As previously mentioned I start AsyncTask instances from within a Service class. In contrary, the documentation states ...

AsyncTask allows you to perform asynchronous work on your user interface. It performs the blocking operations in a worker thread and then publishes the results on the UI thread, without requiring you to handle threads and/or handlers yourself.

... which sounds as if AsyncTask should only be used within an Activity not within a Service?!

JJD
  • 50,076
  • 60
  • 203
  • 339
  • You are correct that 99% of the time `onReceive()` is called on the main thread, but that 1% depends on how it was registered. Can you show the `registerReceiver()` section of the code? – devunwired Jul 12 '12 at 22:31
  • @Devunwired I added the `onCreate()` method to show `registerReceiver()`. – JJD Jul 12 '12 at 23:09
  • I'm still interested in helping you figure out why this happened. Can you post the stack trace from the wrong thread exception? It will help enlighten where the faulty call came from. – devunwired Jul 13 '12 at 14:14
  • @Devunwired Sorry for the delay. I added the stacktrace. – JJD Jul 14 '12 at 11:29
  • So take a close look at that stack trace and note that this call has nothing to do with your `BroadcastReceiver`. Your code is being executed from a `Timer`, which executes tasks on various threads, from inside that library. That library calls the listener directly from the background thread rather than posting callbacks on the main thread like the Android framework does. – devunwired Jul 14 '12 at 17:00
  • @Devunwired Sorry! I admit I did not give enough details in order to keep the question simple. I edited the question and added information about the service and tasks I use. I hope this helps. – JJD Jul 14 '12 at 21:31

2 Answers2

1

If it's getting called on the wrong thread, then it's likely not on the UI thread. Have you tried this:

runOnUiThread(new Runnable() {
    public void run() {
        mapView.invalidate();
    }});
skUDA
  • 316
  • 1
  • 8
  • It works, however, I do not understand on what thread it is running then. Could you explain why this is neccessary? Am I having a design fault? – JJD Jul 12 '12 at 22:31
  • Likely something to do with your async tasks. Take a look at http://developer.android.com/reference/android/os/AsyncTask.html See the part about background thread computation? Since mapView is a view created by the UI thread, it can only be modified on the UI thread. If the call to modify it is made on a background thread, it throws an error. It's likely bad practice, but if there's a chance a method editing a view may get called on a background thread, call the runOnUiThread function to make sure it's being run on the UI thread. – skUDA Jul 12 '12 at 22:57
  • The task should have no direct connection to the activity. I send a broadcast intent when the background process has finished. The activity handles the intent then. – JJD Jul 12 '12 at 23:01
  • 1
    I found this little tidbit here: http://developer.android.com/reference/android/content/BroadcastReceiver.html "starting an Activity with an Intent is a foreground operation that modifies what the user is currently interacting with; broadcasting an Intent is a background operation that the user is not normally aware of." If I'm interpreting it correctly, it's running in the background. Perhaps it is by definition a background thread? I'm not well versed with broadcast intents and async tasks, so you probably want to do more research to find a correct answer. – skUDA Jul 13 '12 at 00:21
  • No offense! I revoked the "answer grant" because after I read the [documentation on Threads](http://developer.android.com/guide/components/processes-and-threads.html) and feel that `runOnUiThread` here is more of a workaround. It is my fault since I did not add enough information using a *Service* and *AsyncTask*. However, the specification clearly states that *AsyncTask "publishes the results on the UI thread, without requiring you to handle threads and/or handlers yourself."* Please correct me if I understand this wrong! I really would like to sort out the base problem of my implementation. – JJD Jul 15 '12 at 15:19
  • None taken. It is a work around and not the complete solution to your problem. I cannot help you with the actual root of your problem since I really don't know much about async tasks and I do not use them. It looks like you're getting a good bit of help from Devunwired, so I wish you the best of luck and hope you figure it out. – skUDA Jul 16 '12 at 14:56
1

The reason for your crash is because of the way that the MapChange library you are using is implemented. Under the hood, this library uses Timer and TimerTask implementations to delay firing the change event and reduce the number of calls your application gets to onMapChanged(). However, you can see from the docs on Timer that it runs its tasks in created threads:

Each timer has one thread on which tasks are executed sequentially. When this thread is busy running a task, runnable tasks may be subject to delays.

Since the MapChange library does nothing to ensure that callbacks are posted to your application on the main thread (a serious bug IMO, especially on Android), you have to protect the code you call as a result of this listener. You can see this in the example MyMapActivity bundled with the library, everything from that callback gets funneled through a Handler which posts the calls back to the main thread for you.

In your application, the code inside onMapPan() and subsequently showTrees() is being called on a background thread so it is not safe to manipulate the UI there. Using either a Handler or runOnUiThread() from your Activity will guarantee your code is called in the right place.

With regards to your second questions about AsyncTask, there is nothing stopping you from using it inside of any application component, not just Activity. Even though it's a "background" component, by default a Service is still running on the main thread as well, so AsyncTask is still necessary to offload long-term processing to another thread temporarily.

devunwired
  • 62,780
  • 12
  • 127
  • 139
  • Damn. In the beginning I actually used the implementation as given in `MyMapActivity` but it did not make sense to me to use `Handler`. Do you see room for improvement in the lib getting rid of the timers? Do you know of better way to receive map change events of that kind? Thank you for the fantastic research!!! To summarize: my broadcasts are totally secure and have not been the reason for the exception but the `loadData()` call from within `onMapPan()`, `onMapZoom()`, ...? Putting `runOnUiThread()` does not affect the `show()` call originating from the broadcast receiver, right? – JJD Jul 17 '12 at 08:40
  • I opened a [question](http://stackoverflow.com/q/11519381/356895) to find alternative solutions to the [MapChange library](https://github.com/bricolsoftconsulting/MapChange). – JJD Jul 17 '12 at 09:13
  • 2
    I would take a look at the revised library. The developer has incorporate my suggestion and the result looks much cleaner. – devunwired Jul 17 '12 at 15:45
  • Thank you again! I checked it out after I realized your improvement suggestion. It works great! Thank you for spending your time, I really appreciate! – JJD Jul 17 '12 at 20:58