Use Application#registerLifecycleEvents() instead of a Fragment for Beam.

Fragments don't work as desired if called after life-cycle events
such as onDestory() or onSaveInstanceState().

The new approach doesn't work after onDestroy() either, but we can more
easily detect this now. For pre-JB apps, we will log an error, and for
JB and onwards we will throw.

Update documentation to make these rules clear, and to encourage
the use of a single Activity per API call, and to make the call
in onCreate().

Bug: 5199662
Bug: 5994691
Bug: 6034901
Bug: 6125297

Change-Id: Ib0dde6abfa44cd56c7ddc13ba0ad0e83bbe30058
diff --git a/core/java/android/nfc/NfcActivityManager.java b/core/java/android/nfc/NfcActivityManager.java
index 5fe58e9..2c73056 100644
--- a/core/java/android/nfc/NfcActivityManager.java
+++ b/core/java/android/nfc/NfcActivityManager.java
@@ -17,210 +17,303 @@
 package android.nfc;
 
 import android.app.Activity;
+import android.app.Application;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.util.Log;
 
-import java.util.WeakHashMap;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
 
 /**
  * Manages NFC API's that are coupled to the life-cycle of an Activity.
  *
- * <p>Uses a fragment to hook into onPause() and onResume() of the host
- * activities.
- *
- * <p>Ideally all of this management would be done in the NFC Service,
- * but right now it is much easier to do it in the application process.
+ * <p>Uses {@link Application#registerActivityLifecycleCallbacks} to hook
+ * into activity life-cycle events such as onPause() and onResume().
  *
  * @hide
  */
-public final class NfcActivityManager extends INdefPushCallback.Stub {
+public final class NfcActivityManager extends INdefPushCallback.Stub
+        implements Application.ActivityLifecycleCallbacks {
     static final String TAG = NfcAdapter.TAG;
     static final Boolean DBG = false;
 
     final NfcAdapter mAdapter;
-    final WeakHashMap<Activity, NfcActivityState> mNfcState;  // contents protected by this
-    final NfcEvent mDefaultEvent;  // can re-use one NfcEvent because it just contains adapter
+    final NfcEvent mDefaultEvent;  // cached NfcEvent (its currently always the same)
+
+    // All objects in the lists are protected by this
+    final List<NfcApplicationState> mApps;  // Application(s) that have NFC state. Usually one
+    final List<NfcActivityState> mActivities;  // Activities that have NFC state
+
+    /**
+     * NFC State associated with an {@link Application}.
+     */
+    class NfcApplicationState {
+        int refCount = 0;
+        final Application app;
+        public NfcApplicationState(Application app) {
+            this.app = app;
+        }
+        public void register() {
+            refCount++;
+            if (refCount == 1) {
+                this.app.registerActivityLifecycleCallbacks(NfcActivityManager.this);
+            }
+        }
+        public void unregister() {
+            refCount--;
+            if (refCount == 0) {
+                this.app.unregisterActivityLifecycleCallbacks(NfcActivityManager.this);
+            } else if (refCount < 0) {
+                Log.e(TAG, "-ve refcount for " + app);
+            }
+        }
+    }
+
+    NfcApplicationState findAppState(Application app) {
+        for (NfcApplicationState appState : mApps) {
+            if (appState.app == app) {
+                return appState;
+            }
+        }
+        return null;
+    }
+
+    void registerApplication(Application app) {
+        NfcApplicationState appState = findAppState(app);
+        if (appState == null) {
+            appState = new NfcApplicationState(app);
+            mApps.add(appState);
+        }
+        appState.register();
+    }
+
+    void unregisterApplication(Application app) {
+        NfcApplicationState appState = findAppState(app);
+        if (appState == null) {
+            Log.e(TAG, "app was not registered " + app);
+            return;
+        }
+        appState.unregister();
+    }
 
     /**
      * NFC state associated with an {@link Activity}
      */
     class NfcActivityState {
-        boolean resumed = false;  // is the activity resumed
-        NdefMessage ndefMessage;
-        NfcAdapter.CreateNdefMessageCallback ndefMessageCallback;
-        NfcAdapter.OnNdefPushCompleteCallback onNdefPushCompleteCallback;
+        boolean resumed = false;
+        Activity activity;
+        NdefMessage ndefMessage = null;  // static NDEF message
+        NfcAdapter.CreateNdefMessageCallback ndefMessageCallback = null;
+        NfcAdapter.OnNdefPushCompleteCallback onNdefPushCompleteCallback = null;
+        public NfcActivityState(Activity activity) {
+            if (activity.getWindow().isDestroyed()) {
+                throw new IllegalStateException("activity is already destroyed");
+            }
+            this.activity = activity;
+            registerApplication(activity.getApplication());
+        }
+        public void destroy() {
+            unregisterApplication(activity.getApplication());
+            resumed = false;
+            activity = null;
+            ndefMessage = null;
+            ndefMessageCallback = null;
+            onNdefPushCompleteCallback = null;
+        }
         @Override
         public String toString() {
-            StringBuilder s = new StringBuilder("[").append(resumed).append(" ");
+            StringBuilder s = new StringBuilder("[").append(" ");
             s.append(ndefMessage).append(" ").append(ndefMessageCallback).append(" ");
             s.append(onNdefPushCompleteCallback).append("]");
             return s.toString();
         }
     }
 
-    public NfcActivityManager(NfcAdapter adapter) {
-        mAdapter = adapter;
-        mNfcState = new WeakHashMap<Activity, NfcActivityState>();
-        mDefaultEvent = new NfcEvent(mAdapter);
+    /** find activity state from mActivities */
+    synchronized NfcActivityState findActivityState(Activity activity) {
+        for (NfcActivityState state : mActivities) {
+            if (state.activity == activity) {
+                return state;
+            }
+        }
+        return null;
     }
 
-    /**
-     * onResume hook from fragment attached to activity
-     */
-    public synchronized void onResume(Activity activity) {
-        NfcActivityState state = mNfcState.get(activity);
-        if (DBG) Log.d(TAG, "onResume() for " + activity + " " + state);
-        if (state != null) {
-            state.resumed = true;
-            updateNfcService(state);
-        }
-    }
-
-    /**
-     * onPause hook from fragment attached to activity
-     */
-    public synchronized void onPause(Activity activity) {
-        NfcActivityState state = mNfcState.get(activity);
-        if (DBG) Log.d(TAG, "onPause() for " + activity + " " + state);
-        if (state != null) {
-            state.resumed = false;
-            updateNfcService(state);
-        }
-    }
-
-    /**
-     * onDestroy hook from fragment attached to activity
-     */
-    public void onDestroy(Activity activity) {
-        mNfcState.remove(activity);
-    }
-
-    public synchronized void setNdefPushMessage(Activity activity, NdefMessage message) {
-        NfcActivityState state = getOrCreateState(activity, message != null);
-        if (state == null || state.ndefMessage == message) {
-            return;  // nothing more to do;
-        }
-        state.ndefMessage = message;
-        if (message == null) {
-            maybeRemoveState(activity, state);
-        }
-        if (state.resumed) {
-            updateNfcService(state);
-        }
-    }
-
-    public synchronized void setNdefPushMessageCallback(Activity activity,
-            NfcAdapter.CreateNdefMessageCallback callback) {
-        NfcActivityState state = getOrCreateState(activity, callback != null);
-        if (state == null || state.ndefMessageCallback == callback) {
-            return;  // nothing more to do;
-        }
-        state.ndefMessageCallback = callback;
-        if (callback == null) {
-            maybeRemoveState(activity, state);
-        }
-        if (state.resumed) {
-            updateNfcService(state);
-        }
-    }
-
-    public synchronized void setOnNdefPushCompleteCallback(Activity activity,
-            NfcAdapter.OnNdefPushCompleteCallback callback) {
-        NfcActivityState state = getOrCreateState(activity, callback != null);
-        if (state == null || state.onNdefPushCompleteCallback == callback) {
-            return;  // nothing more to do;
-        }
-        state.onNdefPushCompleteCallback = callback;
-        if (callback == null) {
-            maybeRemoveState(activity, state);
-        }
-        if (state.resumed) {
-            updateNfcService(state);
-        }
-    }
-
-    /**
-     * Get the NfcActivityState for the specified Activity.
-     * If create is true, then create it if it doesn't already exist,
-     * and ensure the NFC fragment is attached to the activity.
-     */
-    synchronized NfcActivityState getOrCreateState(Activity activity, boolean create) {
-        if (DBG) Log.d(TAG, "getOrCreateState " + activity + " " + create);
-        NfcActivityState state = mNfcState.get(activity);
-        if (state == null && create) {
-            state = new NfcActivityState();
-            mNfcState.put(activity, state);
-            NfcFragment.attach(activity);
+    /** find or create activity state from mActivities */
+    synchronized NfcActivityState getActivityState(Activity activity) {
+        NfcActivityState state = findActivityState(activity);
+        if (state == null) {
+            state = new NfcActivityState(activity);
+            mActivities.add(state);
         }
         return state;
     }
 
-    /**
-     * If the NfcActivityState is empty then remove it, and
-     * detach it from the Activity.
-     */
-    synchronized void maybeRemoveState(Activity activity, NfcActivityState state) {
-        if (state.ndefMessage == null && state.ndefMessageCallback == null &&
-                state.onNdefPushCompleteCallback == null) {
-            NfcFragment.remove(activity);
-            mNfcState.remove(activity);
+    synchronized NfcActivityState findResumedActivityState() {
+        for (NfcActivityState state : mActivities) {
+            if (state.resumed) {
+                return state;
+            }
+        }
+        return null;
+    }
+
+    synchronized void destroyActivityState(Activity activity) {
+        NfcActivityState activityState = findActivityState(activity);
+        if (activityState != null) {
+            activityState.destroy();
+            mActivities.remove(activityState);
+        }
+    }
+
+    public NfcActivityManager(NfcAdapter adapter) {
+        mAdapter = adapter;
+        mActivities = new LinkedList<NfcActivityState>();
+        mApps = new ArrayList<NfcApplicationState>(1);  // Android VM usually has 1 app
+        mDefaultEvent = new NfcEvent(mAdapter);
+    }
+
+    public void setNdefPushMessage(Activity activity, NdefMessage message) {
+        boolean isResumed;
+        synchronized (NfcActivityManager.this) {
+            NfcActivityState state = getActivityState(activity);
+            state.ndefMessage = message;
+            isResumed = state.resumed;
+        }
+        if (isResumed) {
+            requestNfcServiceCallback(true);
+        }
+    }
+
+    public void setNdefPushMessageCallback(Activity activity,
+            NfcAdapter.CreateNdefMessageCallback callback) {
+        boolean isResumed;
+        synchronized (NfcActivityManager.this) {
+            NfcActivityState state = getActivityState(activity);
+            state.ndefMessageCallback = callback;
+            isResumed = state.resumed;
+        }
+        if (isResumed) {
+            requestNfcServiceCallback(true);
+        }
+    }
+
+    public void setOnNdefPushCompleteCallback(Activity activity,
+            NfcAdapter.OnNdefPushCompleteCallback callback) {
+        boolean isResumed;
+        synchronized (NfcActivityManager.this) {
+            NfcActivityState state = getActivityState(activity);
+            state.onNdefPushCompleteCallback = callback;
+            isResumed = state.resumed;
+        }
+        if (isResumed) {
+            requestNfcServiceCallback(true);
         }
     }
 
     /**
-     * Register NfcActivityState with the NFC service.
+     * Request or unrequest NFC service callbacks for NDEF push.
+     * Makes IPC call - do not hold lock.
+     * TODO: Do not do IPC on every onPause/onResume
      */
-    synchronized void updateNfcService(NfcActivityState state) {
-        boolean serviceCallbackNeeded = state.ndefMessageCallback != null ||
-                state.onNdefPushCompleteCallback != null;
-
+    void requestNfcServiceCallback(boolean request) {
         try {
-            NfcAdapter.sService.setForegroundNdefPush(state.resumed ? state.ndefMessage : null,
-                    state.resumed && serviceCallbackNeeded ? this : null);
+            NfcAdapter.sService.setNdefPushCallback(request ? this : null);
         } catch (RemoteException e) {
             mAdapter.attemptDeadServiceRecovery(e);
         }
     }
 
-    /**
-     * Callback from NFC service
-     */
+    /** Callback from NFC service, usually on binder thread */
     @Override
     public NdefMessage createMessage() {
-        NfcAdapter.CreateNdefMessageCallback callback = null;
+        NfcAdapter.CreateNdefMessageCallback callback;
+        NdefMessage message;
         synchronized (NfcActivityManager.this) {
-            for (NfcActivityState state : mNfcState.values()) {
-                if (state.resumed) {
-                    callback = state.ndefMessageCallback;
-                }
-            }
+            NfcActivityState state = findResumedActivityState();
+            if (state == null) return null;
+
+            callback = state.ndefMessageCallback;
+            message = state.ndefMessage;
         }
 
-        // drop lock before making callback
+        // Make callback without lock
         if (callback != null) {
             return callback.createNdefMessage(mDefaultEvent);
+        } else {
+            return message;
         }
-        return null;
     }
 
-    /**
-     * Callback from NFC service
-     */
+    /** Callback from NFC service, usually on binder thread */
     @Override
     public void onNdefPushComplete() {
-        NfcAdapter.OnNdefPushCompleteCallback callback = null;
+        NfcAdapter.OnNdefPushCompleteCallback callback;
         synchronized (NfcActivityManager.this) {
-            for (NfcActivityState state : mNfcState.values()) {
-                if (state.resumed) {
-                    callback = state.onNdefPushCompleteCallback;
-                }
-            }
+            NfcActivityState state = findResumedActivityState();
+            if (state == null) return;
+
+            callback = state.onNdefPushCompleteCallback;
         }
 
-        // drop lock before making callback
+        // Make callback without lock
         if (callback != null) {
             callback.onNdefPushComplete(mDefaultEvent);
         }
     }
 
+    /** Callback from Activity life-cycle, on main thread */
+    @Override
+    public void onActivityCreated(Activity activity, Bundle savedInstanceState) { /* NO-OP */ }
+
+    /** Callback from Activity life-cycle, on main thread */
+    @Override
+    public void onActivityStarted(Activity activity) { /* NO-OP */ }
+
+    /** Callback from Activity life-cycle, on main thread */
+    @Override
+    public void onActivityResumed(Activity activity) {
+        synchronized (NfcActivityManager.this) {
+            NfcActivityState state = findActivityState(activity);
+            if (DBG) Log.d(TAG, "onResume() for " + activity + " " + state);
+            if (state == null) return;
+            state.resumed = true;
+        }
+        requestNfcServiceCallback(true);
+    }
+
+    /** Callback from Activity life-cycle, on main thread */
+    @Override
+    public void onActivityPaused(Activity activity) {
+        synchronized (NfcActivityManager.this) {
+            NfcActivityState state = findActivityState(activity);
+            if (DBG) Log.d(TAG, "onPause() for " + activity + " " + state);
+            if (state == null) return;
+            state.resumed = false;
+        }
+        requestNfcServiceCallback(false);
+    }
+
+    /** Callback from Activity life-cycle, on main thread */
+    @Override
+    public void onActivityStopped(Activity activity) { /* NO-OP */ }
+
+    /** Callback from Activity life-cycle, on main thread */
+    @Override
+    public void onActivitySaveInstanceState(Activity activity, Bundle outState) { /* NO-OP */ }
+
+    /** Callback from Activity life-cycle, on main thread */
+    @Override
+    public void onActivityDestroyed(Activity activity) {
+        synchronized (NfcActivityManager.this) {
+            NfcActivityState state = findActivityState(activity);
+            if (DBG) Log.d(TAG, "onDestroy() for " + activity + " " + state);
+            if (state != null) {
+                // release all associated references
+                destroyActivityState(activity);
+            }
+        }
+    }
 }