Question

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?!

Was it helpful?

Solution

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.

OTHER TIPS

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();
    }});
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top