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 methodshow()is called to display cached data. - A background service gets started which downloads and stores data. It uses
AsyncTaskinstances 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
MapActivityreceives the intent and handles it.
The methodshow()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?!
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
TimerandTimerTaskimplementations to delay firing the change event and reduce the number of calls your application gets toonMapChanged(). However, you can see from the docs onTimerthat it runs its tasks in created threads: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
MyMapActivitybundled with the library, everything from that callback gets funneled through aHandlerwhich posts the calls back to the main thread for you.In your application, the code inside
onMapPan()and subsequentlyshowTrees()is being called on a background thread so it is not safe to manipulate the UI there. Using either aHandlerorrunOnUiThread()from yourActivitywill 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 justActivity. Even though it’s a “background” component, by default aServiceis still running on the main thread as well, soAsyncTaskis still necessary to offload long-term processing to another thread temporarily.