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);
+ }
+ }
+ }
}