I register a broadcast receiver in a preference activity and send it a (sync) broadcast from a (Wakeful) IntentService. Apparently onReceive runs on the service's thread. Is this a fault of my part ? Is it documented behavior ?
Preference activity:
public final class SettingsActivity extends BaseSettings {
private static CharSequence sMasterKey;
private CheckBoxPreference mMasterPref;
// Receiver
/** If the master preference is changed externally this reacts */
private BroadcastReceiver mExternalChangeReceiver =
new ExternalChangeReceiver();
public static void notifyMonitoringStateChange(Context ctx,
CharSequence action, boolean isToggling) {
final LocalBroadcastManager lbm = LocalBroadcastManager
.getInstance(ctx);
Intent intent = new Intent(ctx, ExternalChangeReceiver.class);
intent.setAction(action.toString());
intent.putExtra(TOGGLING_MONITORING_IN_PROGRESS, isToggling);
lbm.sendBroadcastSync(intent);
}
@Override
protected void onStart() {
super.onStart();
final LocalBroadcastManager lbm = LocalBroadcastManager
.getInstance(this);
lbm.registerReceiver(mExternalChangeReceiver, new IntentFilter(
ac_toggling.toString()));
}
@Override
protected void onStop() {
// may not be called in Froyo
final LocalBroadcastManager lbm = LocalBroadcastManager
.getInstance(this);
lbm.unregisterReceiver(mExternalChangeReceiver);
super.onStop();
}
private final class ExternalChangeReceiver extends BroadcastReceiver {
ExternalChangeReceiver() {}
@Override
@SuppressWarnings("synthetic-access")
public void onReceive(Context ctx, Intent intent) {
if (sMasterKey == null || mMasterPref == null) return; // if
// onPostReceive has not run this will be null
final String action = intent.getAction();
if (ac_toggling.equals(action)) {
final boolean isToggling = intent.getBooleanExtra(
TOGGLING_MONITORING_IN_PROGRESS, false);
Log.w(ExternalChangeReceiver.class.getSimpleName(),
"isToggling " + isToggling);
mMasterPref.setEnabled(!isToggling); // line 168 !!!
refreshMasterPreference(isToggling);
}
}
}
}
IntentService (LocationMonitor):
SettingsActivity.notifyMonitoringStateChange(this, ac_toggling, true);
Exception (E/AndroidRuntime):
FATAL EXCEPTION: IntentService[LocationMonitor]
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:4746)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:823)
at android.view.View.requestLayout(View.java:15473)
at android.view.View.requestLayout(View.java:15473)
at android.view.View.requestLayout(View.java:15473)
at android.view.View.requestLayout(View.java:15473)
at android.view.View.requestLayout(View.java:15473)
at android.view.View.requestLayout(View.java:15473)
at android.view.View.requestLayout(View.java:15473)
at android.view.View.requestLayout(View.java:15473)
at android.view.View.requestLayout(View.java:15473)
at android.widget.AbsListView.requestLayout(AbsListView.java:1819)
at android.widget.AdapterView$AdapterDataSetObserver.onChanged(AdapterView.java:813)
at android.widget.AbsListView$AdapterDataSetObserver.onChanged(AbsListView.java:5958)
at android.database.DataSetObservable.notifyChanged(DataSetObservable.java:37)
at android.widget.BaseAdapter.notifyDataSetChanged(BaseAdapter.java:50)
at android.preference.PreferenceGroupAdapter.onPreferenceChange(PreferenceGroupAdapter.java:238)
at android.preference.Preference.notifyChanged(Preference.java:1099)
at android.preference.Preference.setEnabled(Preference.java:726)
at gr.uoa.di.monitoring.android.activities.SettingsActivity$ExternalChangeReceiver.onReceive(SettingsActivity.java:168)
at android.support.v4.content.LocalBroadcastManager.executePendingBroadcasts(LocalBroadcastManager.java:297)
at android.support.v4.content.LocalBroadcastManager.sendBroadcastSync(LocalBroadcastManager.java:278)
at gr.uoa.di.monitoring.android.activities.SettingsActivity.notifyMonitoringStateChange(SettingsActivity.java:54)
at gr.uoa.di.monitoring.android.services.Monitor.abort(Monitor.java:241)
at gr.uoa.di.monitoring.android.services.LocationMonitor.doWakefulWork(LocationMonitor.java:103)
at com.commonsware.cwac.wakeful.WakefulIntentService.onHandleIntent(WakefulIntentService.java:94)
at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:65)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.os.HandlerThread.run(HandlerThread.java:60)
On an emulator API 17
sendBroadcastSync()
is indeed run on the thread it is called from (except if there is a race going on). Its implementation should be:The race part I am not sure about enters via the
sendBroadcast()
call which returns true if it found any matching receiver (registered with LBM for this intent), after sending a message to a private handler associated with the main loop - abbreviated:where:
I am not sure if there can be a race between the main thread and the thread
sendBroadcastSync()
is executing - so theexecutePendingBroadcasts()
will run on the main thread. If not thenexecutePendingBroadcasts()
runs on the threadsendBroadcastSync
is run and directly calls the onReceive of the receiversOne of these days I should look into why (in executePendingBroadcasts)
synchronized(mReceivers)
and notsynchronized(mPendingBroadcasts)
.