|  | /* | 
|  | * Copyright (C) 2011 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.nfc; | 
|  |  | 
|  | import android.app.Activity; | 
|  | import android.app.Application; | 
|  | import android.compat.annotation.UnsupportedAppUsage; | 
|  | import android.nfc.NfcAdapter.ReaderCallback; | 
|  | import android.os.Binder; | 
|  | import android.os.Bundle; | 
|  | import android.os.RemoteException; | 
|  | import android.util.Log; | 
|  |  | 
|  | 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 {@link Application#registerActivityLifecycleCallbacks} to hook | 
|  | * into activity life-cycle events such as onPause() and onResume(). | 
|  | * | 
|  | * @hide | 
|  | */ | 
|  | public final class NfcActivityManager extends IAppCallback.Stub | 
|  | implements Application.ActivityLifecycleCallbacks { | 
|  | static final String TAG = NfcAdapter.TAG; | 
|  | static final Boolean DBG = false; | 
|  |  | 
|  | @UnsupportedAppUsage | 
|  | final NfcAdapter mAdapter; | 
|  |  | 
|  | // 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; | 
|  | Activity activity; | 
|  | NfcAdapter.ReaderCallback readerCallback = null; | 
|  | int readerModeFlags = 0; | 
|  | Bundle readerModeExtras = null; | 
|  | Binder token; | 
|  |  | 
|  | int mPollTech = NfcAdapter.FLAG_USE_ALL_TECH; | 
|  | int mListenTech = NfcAdapter.FLAG_USE_ALL_TECH; | 
|  |  | 
|  | public NfcActivityState(Activity activity) { | 
|  | if (activity.isDestroyed()) { | 
|  | throw new IllegalStateException("activity is already destroyed"); | 
|  | } | 
|  | // Check if activity is resumed right now, as we will not | 
|  | // immediately get a callback for that. | 
|  | resumed = activity.isResumed(); | 
|  |  | 
|  | this.activity = activity; | 
|  | this.token = new Binder(); | 
|  | registerApplication(activity.getApplication()); | 
|  | } | 
|  | public void destroy() { | 
|  | unregisterApplication(activity.getApplication()); | 
|  | resumed = false; | 
|  | activity = null; | 
|  | readerCallback = null; | 
|  | readerModeFlags = 0; | 
|  | readerModeExtras = null; | 
|  | token = null; | 
|  |  | 
|  | mPollTech = NfcAdapter.FLAG_USE_ALL_TECH; | 
|  | mListenTech = NfcAdapter.FLAG_USE_ALL_TECH; | 
|  | } | 
|  | @Override | 
|  | public String toString() { | 
|  | StringBuilder s = new StringBuilder("["); | 
|  | s.append(readerCallback); | 
|  | s.append("]"); | 
|  | return s.toString(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** find activity state from mActivities */ | 
|  | synchronized NfcActivityState findActivityState(Activity activity) { | 
|  | for (NfcActivityState state : mActivities) { | 
|  | if (state.activity == activity) { | 
|  | return state; | 
|  | } | 
|  | } | 
|  | return null; | 
|  | } | 
|  |  | 
|  | /** 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; | 
|  | } | 
|  |  | 
|  | 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 | 
|  | } | 
|  |  | 
|  | public void enableReaderMode(Activity activity, ReaderCallback callback, int flags, | 
|  | Bundle extras) { | 
|  | boolean isResumed; | 
|  | Binder token; | 
|  | int pollTech, listenTech; | 
|  | synchronized (NfcActivityManager.this) { | 
|  | NfcActivityState state = getActivityState(activity); | 
|  | state.readerCallback = callback; | 
|  | state.readerModeFlags = flags; | 
|  | state.readerModeExtras = extras; | 
|  | pollTech = state.mPollTech; | 
|  | listenTech = state.mListenTech; | 
|  | token = state.token; | 
|  | isResumed = state.resumed; | 
|  | } | 
|  | if (isResumed) { | 
|  | if (listenTech != NfcAdapter.FLAG_USE_ALL_TECH | 
|  | || pollTech != NfcAdapter.FLAG_USE_ALL_TECH) { | 
|  | throw new IllegalStateException( | 
|  | "Cannot be used when alternative DiscoveryTechnology is set"); | 
|  | } else { | 
|  | setReaderMode(token, flags, extras); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | public void disableReaderMode(Activity activity) { | 
|  | boolean isResumed; | 
|  | Binder token; | 
|  | synchronized (NfcActivityManager.this) { | 
|  | NfcActivityState state = getActivityState(activity); | 
|  | state.readerCallback = null; | 
|  | state.readerModeFlags = 0; | 
|  | state.readerModeExtras = null; | 
|  | token = state.token; | 
|  | isResumed = state.resumed; | 
|  | } | 
|  | if (isResumed) { | 
|  | setReaderMode(token, 0, null); | 
|  | } | 
|  |  | 
|  | } | 
|  |  | 
|  | public void setReaderMode(Binder token, int flags, Bundle extras) { | 
|  | if (DBG) Log.d(TAG, "Setting reader mode"); | 
|  | NfcAdapter.callService(() -> NfcAdapter.sService.setReaderMode(token, this, flags, extras)); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Request or unrequest NFC service callbacks. | 
|  | * Makes IPC call - do not hold lock. | 
|  | */ | 
|  | void requestNfcServiceCallback() { | 
|  | NfcAdapter.callService(() -> NfcAdapter.sService.setAppCallback(this)); | 
|  | } | 
|  |  | 
|  | void verifyNfcPermission() { | 
|  | NfcAdapter.callService(() -> NfcAdapter.sService.verifyNfcPermission()); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onTagDiscovered(Tag tag) throws RemoteException { | 
|  | NfcAdapter.ReaderCallback callback; | 
|  | synchronized (NfcActivityManager.this) { | 
|  | NfcActivityState state = findResumedActivityState(); | 
|  | if (state == null) return; | 
|  |  | 
|  | callback = state.readerCallback; | 
|  | } | 
|  |  | 
|  | // Make callback without lock | 
|  | if (callback != null) { | 
|  | callback.onTagDiscovered(tag); | 
|  | } | 
|  |  | 
|  | } | 
|  | /** 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) { | 
|  | int readerModeFlags = 0; | 
|  | Bundle readerModeExtras = null; | 
|  | Binder token; | 
|  | int pollTech; | 
|  | int listenTech; | 
|  |  | 
|  | synchronized (NfcActivityManager.this) { | 
|  | NfcActivityState state = findActivityState(activity); | 
|  | if (DBG) Log.d(TAG, "onResume() for " + activity + " " + state); | 
|  | if (state == null) return; | 
|  | state.resumed = true; | 
|  | token = state.token; | 
|  | readerModeFlags = state.readerModeFlags; | 
|  | readerModeExtras = state.readerModeExtras; | 
|  |  | 
|  | pollTech = state.mPollTech; | 
|  | listenTech = state.mListenTech; | 
|  | } | 
|  | if (readerModeFlags != 0) { | 
|  | setReaderMode(token, readerModeFlags, readerModeExtras); | 
|  | } else if (listenTech != NfcAdapter.FLAG_USE_ALL_TECH | 
|  | || pollTech != NfcAdapter.FLAG_USE_ALL_TECH) { | 
|  | changeDiscoveryTech(token, pollTech, listenTech); | 
|  | } | 
|  | requestNfcServiceCallback(); | 
|  | } | 
|  |  | 
|  | /** Callback from Activity life-cycle, on main thread */ | 
|  | @Override | 
|  | public void onActivityPaused(Activity activity) { | 
|  | boolean readerModeFlagsSet; | 
|  | Binder token; | 
|  | int pollTech; | 
|  | int listenTech; | 
|  |  | 
|  | synchronized (NfcActivityManager.this) { | 
|  | NfcActivityState state = findActivityState(activity); | 
|  | if (DBG) Log.d(TAG, "onPause() for " + activity + " " + state); | 
|  | if (state == null) return; | 
|  | state.resumed = false; | 
|  | token = state.token; | 
|  | readerModeFlagsSet = state.readerModeFlags != 0; | 
|  |  | 
|  | pollTech = state.mPollTech; | 
|  | listenTech = state.mListenTech; | 
|  | } | 
|  | if (readerModeFlagsSet) { | 
|  | // Restore default p2p modes | 
|  | setReaderMode(token, 0, null); | 
|  | } else if (listenTech != NfcAdapter.FLAG_USE_ALL_TECH | 
|  | || pollTech != NfcAdapter.FLAG_USE_ALL_TECH) { | 
|  | changeDiscoveryTech(token, | 
|  | NfcAdapter.FLAG_USE_ALL_TECH, NfcAdapter.FLAG_USE_ALL_TECH); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** 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); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** setDiscoveryTechnology() implementation */ | 
|  | public void setDiscoveryTech(Activity activity, int pollTech, int listenTech) { | 
|  | boolean isResumed; | 
|  | Binder token; | 
|  | boolean readerModeFlagsSet; | 
|  | synchronized (NfcActivityManager.this) { | 
|  | NfcActivityState state = getActivityState(activity); | 
|  | readerModeFlagsSet = state.readerModeFlags != 0; | 
|  | state.mListenTech = listenTech; | 
|  | state.mPollTech = pollTech; | 
|  | token = state.token; | 
|  | isResumed = state.resumed; | 
|  | } | 
|  | if (!readerModeFlagsSet && isResumed) { | 
|  | changeDiscoveryTech(token, pollTech, listenTech); | 
|  | } else if (readerModeFlagsSet) { | 
|  | throw new IllegalStateException("Cannot be used when the Reader Mode is enabled"); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** resetDiscoveryTechnology() implementation */ | 
|  | public void resetDiscoveryTech(Activity activity) { | 
|  | boolean isResumed; | 
|  | Binder token; | 
|  | boolean readerModeFlagsSet; | 
|  | synchronized (NfcActivityManager.this) { | 
|  | NfcActivityState state = getActivityState(activity); | 
|  | state.mListenTech = NfcAdapter.FLAG_USE_ALL_TECH; | 
|  | state.mPollTech = NfcAdapter.FLAG_USE_ALL_TECH; | 
|  | token = state.token; | 
|  | isResumed = state.resumed; | 
|  | } | 
|  | if (isResumed) { | 
|  | changeDiscoveryTech(token, NfcAdapter.FLAG_USE_ALL_TECH, NfcAdapter.FLAG_USE_ALL_TECH); | 
|  | } | 
|  |  | 
|  | } | 
|  |  | 
|  | private void changeDiscoveryTech(Binder token, int pollTech, int listenTech) { | 
|  | NfcAdapter.callService( | 
|  | () -> NfcAdapter.sService.updateDiscoveryTechnology(token, pollTech, listenTech)); | 
|  | } | 
|  |  | 
|  | } |