From b06ea706530e6d19eb2a1a9a7ae6c5dd77d80af0 Mon Sep 17 00:00:00 2001 From: Dianne Hackborn Date: Mon, 13 Jul 2009 13:07:51 -0700 Subject: Add reporting of activity movement for search manager. This adds a new API with the activity manager to find out about movement between activities. For my sanity, the old IActivityWatcher is now renamed to IActivityController, and the new activity movement interface is named IActivityWatcher. This changes the search manager itself to use the new API to manage its state. Note that there are still problems when going back to the search dialog after it was hidden -- the suggestions window no longer appears until you explicitly dismiss and re-show it. --- core/java/android/app/Activity.java | 41 +++++++--- core/java/android/app/ActivityManagerNative.java | 50 ++++++++++-- core/java/android/app/ActivityThread.java | 24 ++---- core/java/android/app/ApplicationThreadNative.java | 7 +- core/java/android/app/IActivityController.aidl | 55 +++++++++++++ core/java/android/app/IActivityManager.java | 11 ++- core/java/android/app/IActivityWatcher.aidl | 39 ++-------- core/java/android/app/IApplicationThread.java | 2 +- core/java/android/app/ISearchManager.aidl | 3 +- core/java/android/app/SearchDialog.java | 3 + core/java/android/app/SearchManager.java | 13 +++- core/java/android/content/SyncStorageEngine.java | 1 + core/java/android/os/RemoteCallbackList.java | 24 ++++-- .../android/server/search/SearchDialogWrapper.java | 91 +++++++++++++++++----- .../server/search/SearchManagerService.java | 24 +++++- 15 files changed, 287 insertions(+), 101 deletions(-) create mode 100644 core/java/android/app/IActivityController.aidl (limited to 'core/java') diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 6c2560d..4ac3b9e 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -612,6 +612,7 @@ public class Activity extends ContextThemeWrapper // set by the thread after the constructor and before onCreate(Bundle savedInstanceState) is called. private Instrumentation mInstrumentation; private IBinder mToken; + private int mIdent; /*package*/ String mEmbeddedID; private Application mApplication; /*package*/ Intent mIntent; @@ -789,9 +790,6 @@ public class Activity extends ContextThemeWrapper protected void onCreate(Bundle savedInstanceState) { mVisibleFromClient = mWindow.getWindowStyle().getBoolean( com.android.internal.R.styleable.Window_windowNoDisplay, true); - // uses super.getSystemService() since this.getSystemService() looks at the - // mSearchManager field. - mSearchManager = (SearchManager) super.getSystemService(Context.SEARCH_SERVICE); mCalled = true; } @@ -2531,6 +2529,7 @@ public class Activity extends ContextThemeWrapper */ public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch) { + ensureSearchManager(); mSearchManager.startSearch(initialQuery, selectInitialQuery, getComponentName(), appSearchData, globalSearch); } @@ -3241,6 +3240,24 @@ public class Activity extends ContextThemeWrapper return getSharedPreferences(getLocalClassName(), mode); } + private void ensureSearchManager() { + if (mSearchManager != null) { + return; + } + + // uses super.getSystemService() since this.getSystemService() looks at the + // mSearchManager field. + mSearchManager = (SearchManager) super.getSystemService(Context.SEARCH_SERVICE); + int ident = mIdent; + if (ident == 0) { + if (mParent != null) ident = mParent.mIdent; + if (ident == 0) { + throw new IllegalArgumentException("no ident"); + } + } + mSearchManager.setIdent(ident); + } + @Override public Object getSystemService(String name) { if (getBaseContext() == null) { @@ -3251,6 +3268,7 @@ public class Activity extends ContextThemeWrapper if (WINDOW_SERVICE.equals(name)) { return mWindowManager; } else if (SEARCH_SERVICE.equals(name)) { + ensureSearchManager(); return mSearchManager; } return super.getSystemService(name); @@ -3450,14 +3468,17 @@ public class Activity extends ContextThemeWrapper Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, Object lastNonConfigurationInstance, Configuration config) { - attach(context, aThread, instr, token, application, intent, info, title, parent, id, + attach(context, aThread, instr, token, 0, application, intent, info, title, parent, id, lastNonConfigurationInstance, null, config); } - final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, - Application application, Intent intent, ActivityInfo info, CharSequence title, - Activity parent, String id, Object lastNonConfigurationInstance, - HashMap lastNonConfigurationChildInstances, Configuration config) { + final void attach(Context context, ActivityThread aThread, + Instrumentation instr, IBinder token, int ident, + Application application, Intent intent, ActivityInfo info, + CharSequence title, Activity parent, String id, + Object lastNonConfigurationInstance, + HashMap lastNonConfigurationChildInstances, + Configuration config) { attachBaseContext(context); mWindow = PolicyManager.makeNewWindow(this); @@ -3470,6 +3491,7 @@ public class Activity extends ContextThemeWrapper mMainThread = aThread; mInstrumentation = instr; mToken = token; + mIdent = ident; mApplication = application; mIntent = intent; mComponent = intent.getComponent(); @@ -3554,9 +3576,6 @@ public class Activity extends ContextThemeWrapper final void performPause() { onPause(); - - // dismiss the search dialog if it is open - mSearchManager.stopSearch(); } final void performUserLeaving() { diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index dfa8139..ec7714d 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -881,11 +881,11 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } - case SET_ACTIVITY_WATCHER_TRANSACTION: { + case SET_ACTIVITY_CONTROLLER_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); - IActivityWatcher watcher = IActivityWatcher.Stub.asInterface( + IActivityController watcher = IActivityController.Stub.asInterface( data.readStrongBinder()); - setActivityWatcher(watcher); + setActivityController(watcher); return true; } @@ -1052,6 +1052,22 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM reply.writeNoException(); return true; } + + case REGISTER_ACTIVITY_WATCHER_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IActivityWatcher watcher = IActivityWatcher.Stub.asInterface( + data.readStrongBinder()); + registerActivityWatcher(watcher); + return true; + } + + case UNREGISTER_ACTIVITY_WATCHER_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IActivityWatcher watcher = IActivityWatcher.Stub.asInterface( + data.readStrongBinder()); + unregisterActivityWatcher(watcher); + return true; + } } return super.onTransact(code, data, reply, flags); @@ -2105,13 +2121,13 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); reply.recycle(); } - public void setActivityWatcher(IActivityWatcher watcher) throws RemoteException + public void setActivityController(IActivityController watcher) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(watcher != null ? watcher.asBinder() : null); - mRemote.transact(SET_ACTIVITY_WATCHER_TRANSACTION, data, reply, 0); + mRemote.transact(SET_ACTIVITY_CONTROLLER_TRANSACTION, data, reply, 0); reply.readException(); data.recycle(); reply.recycle(); @@ -2290,5 +2306,29 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); } + public void registerActivityWatcher(IActivityWatcher watcher) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(watcher != null ? watcher.asBinder() : null); + mRemote.transact(REGISTER_ACTIVITY_WATCHER_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + + public void unregisterActivityWatcher(IActivityWatcher watcher) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(watcher != null ? watcher.asBinder() : null); + mRemote.transact(UNREGISTER_ACTIVITY_WATCHER_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + private IBinder mRemote; } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 27e8fb5..f2814f2 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1090,6 +1090,7 @@ public final class ActivityThread { private static final class ActivityRecord { IBinder token; + int ident; Intent intent; Bundle state; Activity activity; @@ -1299,12 +1300,13 @@ public final class ActivityThread { // we use token to identify this activity without having to send the // activity itself back to the activity manager. (matters more with ipc) - public final void scheduleLaunchActivity(Intent intent, IBinder token, + public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident, ActivityInfo info, Bundle state, List pendingResults, List pendingNewIntents, boolean notResumed, boolean isForward) { ActivityRecord r = new ActivityRecord(); r.token = token; + r.ident = ident; r.intent = intent; r.activityInfo = info; r.state = state; @@ -2197,21 +2199,11 @@ public final class ActivityThread { } public final Activity startActivityNow(Activity parent, String id, - Intent intent, IBinder token, Bundle state) { - ActivityInfo aInfo = resolveActivityInfo(intent); - return startActivityNow(parent, id, intent, aInfo, token, state); - } - - public final Activity startActivityNow(Activity parent, String id, - Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state) { - return startActivityNow(parent, id, intent, activityInfo, token, state, null); - } - - public final Activity startActivityNow(Activity parent, String id, Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state, Object lastNonConfigurationInstance) { ActivityRecord r = new ActivityRecord(); r.token = token; + r.ident = 0; r.intent = intent; r.state = state; r.parent = parent; @@ -2335,10 +2327,10 @@ public final class ActivityThread { appContext.setOuterContext(activity); CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager()); Configuration config = new Configuration(mConfiguration); - activity.attach(appContext, this, getInstrumentation(), r.token, app, - r.intent, r.activityInfo, title, r.parent, r.embeddedID, - r.lastNonConfigurationInstance, r.lastNonConfigurationChildInstances, - config); + activity.attach(appContext, this, getInstrumentation(), r.token, + r.ident, app, r.intent, r.activityInfo, title, r.parent, + r.embeddedID, r.lastNonConfigurationInstance, + r.lastNonConfigurationChildInstances, config); if (customIntent != null) { activity.mIntent = customIntent; diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index b052c99..a3c6325 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -119,13 +119,15 @@ public abstract class ApplicationThreadNative extends Binder data.enforceInterface(IApplicationThread.descriptor); Intent intent = Intent.CREATOR.createFromParcel(data); IBinder b = data.readStrongBinder(); + int ident = data.readInt(); ActivityInfo info = ActivityInfo.CREATOR.createFromParcel(data); Bundle state = data.readBundle(); List ri = data.createTypedArrayList(ResultInfo.CREATOR); List pi = data.createTypedArrayList(Intent.CREATOR); boolean notResumed = data.readInt() != 0; boolean isForward = data.readInt() != 0; - scheduleLaunchActivity(intent, b, info, state, ri, pi, notResumed, isForward); + scheduleLaunchActivity(intent, b, ident, info, state, ri, pi, + notResumed, isForward); return true; } @@ -442,7 +444,7 @@ class ApplicationThreadProxy implements IApplicationThread { data.recycle(); } - public final void scheduleLaunchActivity(Intent intent, IBinder token, + public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident, ActivityInfo info, Bundle state, List pendingResults, List pendingNewIntents, boolean notResumed, boolean isForward) throws RemoteException { @@ -450,6 +452,7 @@ class ApplicationThreadProxy implements IApplicationThread { data.writeInterfaceToken(IApplicationThread.descriptor); intent.writeToParcel(data, 0); data.writeStrongBinder(token); + data.writeInt(ident); info.writeToParcel(data, 0); data.writeBundle(state); data.writeTypedList(pendingResults); diff --git a/core/java/android/app/IActivityController.aidl b/core/java/android/app/IActivityController.aidl new file mode 100644 index 0000000..8f6b252 --- /dev/null +++ b/core/java/android/app/IActivityController.aidl @@ -0,0 +1,55 @@ +/* +** +** Copyright 2009, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.app; + +import android.content.Intent; + +/** + * Testing interface to monitor what is happening in the activity manager + * while tests are running. Not for normal application development. + * {@hide} + */ +interface IActivityController +{ + /** + * The system is trying to start an activity. Return true to allow + * it to be started as normal, or false to cancel/reject this activity. + */ + boolean activityStarting(in Intent intent, String pkg); + + /** + * The system is trying to return to an activity. Return true to allow + * it to be resumed as normal, or false to cancel/reject this activity. + */ + boolean activityResuming(String pkg); + + /** + * An application process has crashed (in Java). Return true for the + * normal error recovery (app crash dialog) to occur, false to kill + * it immediately. + */ + boolean appCrashed(String processName, int pid, String shortMsg, + String longMsg, in byte[] crashData); + + /** + * An application process is not responding. Return 0 to show the "app + * not responding" dialog, 1 to continue waiting, or -1 to kill it + * immediately. + */ + int appNotResponding(String processName, int pid, String processStats); +} diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index 3ec7938..ee1b69b 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -216,7 +216,7 @@ public interface IActivityManager extends IInterface { String packageName, boolean waitForDebugger, boolean persistent) throws RemoteException; public void setAlwaysFinish(boolean enabled) throws RemoteException; - public void setActivityWatcher(IActivityWatcher watcher) + public void setActivityController(IActivityController watcher) throws RemoteException; public void enterSafeMode() throws RemoteException; @@ -257,6 +257,11 @@ public interface IActivityManager extends IInterface { public void stopAppSwitches() throws RemoteException; public void resumeAppSwitches() throws RemoteException; + public void registerActivityWatcher(IActivityWatcher watcher) + throws RemoteException; + public void unregisterActivityWatcher(IActivityWatcher watcher) + throws RemoteException; + /* * Private non-Binder interfaces */ @@ -372,7 +377,7 @@ public interface IActivityManager extends IInterface { int CHECK_URI_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+53; int GRANT_URI_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+54; int REVOKE_URI_PERMISSION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+55; - int SET_ACTIVITY_WATCHER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+56; + int SET_ACTIVITY_CONTROLLER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+56; int SHOW_WAITING_FOR_DEBUGGER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+57; int SIGNAL_PERSISTENT_PROCESSES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+58; int GET_RECENT_TASKS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+59; @@ -408,4 +413,6 @@ public interface IActivityManager extends IInterface { int START_BACKUP_AGENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+89; int BACKUP_AGENT_CREATED_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+90; int UNBIND_BACKUP_AGENT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+91; + int REGISTER_ACTIVITY_WATCHER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+92; + int UNREGISTER_ACTIVITY_WATCHER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+93; } diff --git a/core/java/android/app/IActivityWatcher.aidl b/core/java/android/app/IActivityWatcher.aidl index f13a385..5d36e3f 100644 --- a/core/java/android/app/IActivityWatcher.aidl +++ b/core/java/android/app/IActivityWatcher.aidl @@ -1,6 +1,6 @@ -/* //device/java/android/android/app/IInstrumentationWatcher.aidl +/* ** -** Copyright 2007, The Android Open Source Project +** Copyright 2009, The Android Open Source Project ** ** Licensed under the Apache License, Version 2.0 (the "License"); ** you may not use this file except in compliance with the License. @@ -17,39 +17,10 @@ package android.app; -import android.content.Intent; - /** - * Testing interface to monitor what is happening in the activity manager - * while tests are running. Not for normal application development. + * Callback interface to watch the user's traversal through activities. * {@hide} */ -interface IActivityWatcher -{ - /** - * The system is trying to start an activity. Return true to allow - * it to be started as normal, or false to cancel/reject this activity. - */ - boolean activityStarting(in Intent intent, String pkg); - - /** - * The system is trying to return to an activity. Return true to allow - * it to be resumed as normal, or false to cancel/reject this activity. - */ - boolean activityResuming(String pkg); - - /** - * An application process has crashed (in Java). Return true for the - * normal error recovery (app crash dialog) to occur, false to kill - * it immediately. - */ - boolean appCrashed(String processName, int pid, String shortMsg, - String longMsg, in byte[] crashData); - - /** - * An application process is not responding. Return 0 to show the "app - * not responding" dialog, 1 to continue waiting, or -1 to kill it - * immediately. - */ - int appNotResponding(String processName, int pid, String processStats); +oneway interface IActivityWatcher { + void activityResuming(int activityId); } diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index c0bc2a0..c915770 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -49,7 +49,7 @@ public interface IApplicationThread extends IInterface { void scheduleWindowVisibility(IBinder token, boolean showWindow) throws RemoteException; void scheduleResumeActivity(IBinder token, boolean isForward) throws RemoteException; void scheduleSendResult(IBinder token, List results) throws RemoteException; - void scheduleLaunchActivity(Intent intent, IBinder token, + void scheduleLaunchActivity(Intent intent, IBinder token, int ident, ActivityInfo info, Bundle state, List pendingResults, List pendingNewIntents, boolean notResumed, boolean isForward) throws RemoteException; diff --git a/core/java/android/app/ISearchManager.aidl b/core/java/android/app/ISearchManager.aidl index 5b62192..84a6085 100644 --- a/core/java/android/app/ISearchManager.aidl +++ b/core/java/android/app/ISearchManager.aidl @@ -34,6 +34,7 @@ interface ISearchManager { in ComponentName launchActivity, in Bundle appSearchData, boolean globalSearch, - ISearchManagerCallback searchManagerCallback); + ISearchManagerCallback searchManagerCallback, + int ident); void stopSearch(); } diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index 9f44c7e..13eb034 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -752,6 +752,9 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } public void afterTextChanged(Editable s) { + if (mSearchable == null) { + return; + } if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) { // The user changed the query, check if it is a URL and if so change the search // button in the soft keyboard to the 'Go' button. diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index 0291882..b795a54 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -1531,6 +1531,8 @@ public class SearchManager private final Context mContext; + private int mIdent; + // package private since they are used by the inner class SearchManagerCallback /* package */ boolean mIsShowing = false; /* package */ final Handler mHandler; @@ -1546,6 +1548,13 @@ public class SearchManager ServiceManager.getService(Context.SEARCH_SERVICE)); } + /*package*/ void setIdent(int ident) { + if (mIdent != 0) { + throw new IllegalStateException("mIdent already set"); + } + mIdent = ident; + } + /** * Launch search UI. * @@ -1593,11 +1602,13 @@ public class SearchManager boolean globalSearch) { if (DBG) debug("startSearch(), mIsShowing=" + mIsShowing); if (mIsShowing) return; + if (mIdent == 0) throw new IllegalArgumentException( + "Called from outside of an Activity context"); try { mIsShowing = true; // activate the search manager and start it up! mService.startSearch(initialQuery, selectInitialQuery, launchActivity, appSearchData, - globalSearch, mSearchManagerCallback); + globalSearch, mSearchManagerCallback, mIdent); } catch (RemoteException ex) { Log.e(TAG, "startSearch() failed: " + ex); } diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java index f781e0d..756f35c 100644 --- a/core/java/android/content/SyncStorageEngine.java +++ b/core/java/android/content/SyncStorageEngine.java @@ -338,6 +338,7 @@ public class SyncStorageEngine extends Handler { } reports.add(mChangeListeners.getBroadcastItem(i)); } + mChangeListeners.finishBroadcast(); } if (DEBUG) Log.v(TAG, "reportChange " + which + " to: " + reports); diff --git a/core/java/android/os/RemoteCallbackList.java b/core/java/android/os/RemoteCallbackList.java index 23c0a7b..584224f 100644 --- a/core/java/android/os/RemoteCallbackList.java +++ b/core/java/android/os/RemoteCallbackList.java @@ -50,6 +50,7 @@ public class RemoteCallbackList { /*package*/ HashMap mCallbacks = new HashMap(); private Object[] mActiveBroadcast; + private int mBroadcastCount = -1; private boolean mKilled = false; private final class Callback implements IBinder.DeathRecipient { @@ -195,15 +196,16 @@ public class RemoteCallbackList { * This creates a copy of the callback list, which you can retrieve items * from using {@link #getBroadcastItem}. Note that only one broadcast can * be active at a time, so you must be sure to always call this from the - * same thread (usually by scheduling with {@link Handler} or + * same thread (usually by scheduling with {@link Handler}) or * do your own synchronization. You must call {@link #finishBroadcast} * when done. * *

A typical loop delivering a broadcast looks like this: * *

-     * final int N = callbacks.beginBroadcast();
-     * for (int i=0; i 0) {
+     *     i--;
      *     try {
      *         callbacks.getBroadcastItem(i).somethingHappened();
      *     } catch (RemoteException e) {
@@ -222,7 +224,12 @@ public class RemoteCallbackList {
      */
     public int beginBroadcast() {
         synchronized (mCallbacks) {
-            final int N = mCallbacks.size();
+            if (mBroadcastCount > 0) {
+                throw new IllegalStateException(
+                        "beginBroadcast() called while already in a broadcast");
+            }
+            
+            final int N = mBroadcastCount = mCallbacks.size();
             if (N <= 0) {
                 return 0;
             }
@@ -281,12 +288,19 @@ public class RemoteCallbackList {
      * @see #beginBroadcast
      */
     public void finishBroadcast() {
+        if (mBroadcastCount < 0) {
+            throw new IllegalStateException(
+                    "finishBroadcast() called outside of a broadcast");
+        }
+        
         Object[] active = mActiveBroadcast;
         if (active != null) {
-            final int N = active.length;
+            final int N = mBroadcastCount;
             for (int i=0; i