1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.nfc;
18
19import android.app.Activity;
20import android.app.Application;
21import android.net.Uri;
22import android.os.Bundle;
23import android.os.RemoteException;
24import android.util.Log;
25
26import java.util.ArrayList;
27import java.util.LinkedList;
28import java.util.List;
29
30/**
31 * Manages NFC API's that are coupled to the life-cycle of an Activity.
32 *
33 * <p>Uses {@link Application#registerActivityLifecycleCallbacks} to hook
34 * into activity life-cycle events such as onPause() and onResume().
35 *
36 * @hide
37 */
38public final class NfcActivityManager extends INdefPushCallback.Stub
39        implements Application.ActivityLifecycleCallbacks {
40    static final String TAG = NfcAdapter.TAG;
41    static final Boolean DBG = false;
42
43    final NfcAdapter mAdapter;
44    final NfcEvent mDefaultEvent;  // cached NfcEvent (its currently always the same)
45
46    // All objects in the lists are protected by this
47    final List<NfcApplicationState> mApps;  // Application(s) that have NFC state. Usually one
48    final List<NfcActivityState> mActivities;  // Activities that have NFC state
49
50    /**
51     * NFC State associated with an {@link Application}.
52     */
53    class NfcApplicationState {
54        int refCount = 0;
55        final Application app;
56        public NfcApplicationState(Application app) {
57            this.app = app;
58        }
59        public void register() {
60            refCount++;
61            if (refCount == 1) {
62                this.app.registerActivityLifecycleCallbacks(NfcActivityManager.this);
63            }
64        }
65        public void unregister() {
66            refCount--;
67            if (refCount == 0) {
68                this.app.unregisterActivityLifecycleCallbacks(NfcActivityManager.this);
69            } else if (refCount < 0) {
70                Log.e(TAG, "-ve refcount for " + app);
71            }
72        }
73    }
74
75    NfcApplicationState findAppState(Application app) {
76        for (NfcApplicationState appState : mApps) {
77            if (appState.app == app) {
78                return appState;
79            }
80        }
81        return null;
82    }
83
84    void registerApplication(Application app) {
85        NfcApplicationState appState = findAppState(app);
86        if (appState == null) {
87            appState = new NfcApplicationState(app);
88            mApps.add(appState);
89        }
90        appState.register();
91    }
92
93    void unregisterApplication(Application app) {
94        NfcApplicationState appState = findAppState(app);
95        if (appState == null) {
96            Log.e(TAG, "app was not registered " + app);
97            return;
98        }
99        appState.unregister();
100    }
101
102    /**
103     * NFC state associated with an {@link Activity}
104     */
105    class NfcActivityState {
106        boolean resumed = false;
107        Activity activity;
108        NdefMessage ndefMessage = null;  // static NDEF message
109        NfcAdapter.CreateNdefMessageCallback ndefMessageCallback = null;
110        NfcAdapter.OnNdefPushCompleteCallback onNdefPushCompleteCallback = null;
111        NfcAdapter.CreateBeamUrisCallback uriCallback = null;
112        Uri[] uris = null;
113        public NfcActivityState(Activity activity) {
114            if (activity.getWindow().isDestroyed()) {
115                throw new IllegalStateException("activity is already destroyed");
116            }
117            // Check if activity is resumed right now, as we will not
118            // immediately get a callback for that.
119            resumed = activity.isResumed();
120
121            this.activity = activity;
122            registerApplication(activity.getApplication());
123        }
124        public void destroy() {
125            unregisterApplication(activity.getApplication());
126            resumed = false;
127            activity = null;
128            ndefMessage = null;
129            ndefMessageCallback = null;
130            onNdefPushCompleteCallback = null;
131            uriCallback = null;
132            uris = null;
133        }
134        @Override
135        public String toString() {
136            StringBuilder s = new StringBuilder("[").append(" ");
137            s.append(ndefMessage).append(" ").append(ndefMessageCallback).append(" ");
138            s.append(uriCallback).append(" ");
139            if (uris != null) {
140                for (Uri uri : uris) {
141                    s.append(onNdefPushCompleteCallback).append(" ").append(uri).append("]");
142                }
143            }
144            return s.toString();
145        }
146    }
147
148    /** find activity state from mActivities */
149    synchronized NfcActivityState findActivityState(Activity activity) {
150        for (NfcActivityState state : mActivities) {
151            if (state.activity == activity) {
152                return state;
153            }
154        }
155        return null;
156    }
157
158    /** find or create activity state from mActivities */
159    synchronized NfcActivityState getActivityState(Activity activity) {
160        NfcActivityState state = findActivityState(activity);
161        if (state == null) {
162            state = new NfcActivityState(activity);
163            mActivities.add(state);
164        }
165        return state;
166    }
167
168    synchronized NfcActivityState findResumedActivityState() {
169        for (NfcActivityState state : mActivities) {
170            if (state.resumed) {
171                return state;
172            }
173        }
174        return null;
175    }
176
177    synchronized void destroyActivityState(Activity activity) {
178        NfcActivityState activityState = findActivityState(activity);
179        if (activityState != null) {
180            activityState.destroy();
181            mActivities.remove(activityState);
182        }
183    }
184
185    public NfcActivityManager(NfcAdapter adapter) {
186        mAdapter = adapter;
187        mActivities = new LinkedList<NfcActivityState>();
188        mApps = new ArrayList<NfcApplicationState>(1);  // Android VM usually has 1 app
189        mDefaultEvent = new NfcEvent(mAdapter);
190    }
191
192    public void setNdefPushContentUri(Activity activity, Uri[] uris) {
193        boolean isResumed;
194        synchronized (NfcActivityManager.this) {
195            NfcActivityState state = getActivityState(activity);
196            state.uris = uris;
197            isResumed = state.resumed;
198        }
199        if (isResumed) {
200            requestNfcServiceCallback(true);
201        }
202    }
203
204
205    public void setNdefPushContentUriCallback(Activity activity,
206            NfcAdapter.CreateBeamUrisCallback callback) {
207        boolean isResumed;
208        synchronized (NfcActivityManager.this) {
209            NfcActivityState state = getActivityState(activity);
210            state.uriCallback = callback;
211            isResumed = state.resumed;
212        }
213        if (isResumed) {
214            requestNfcServiceCallback(true);
215        }
216    }
217
218    public void setNdefPushMessage(Activity activity, NdefMessage message) {
219        boolean isResumed;
220        synchronized (NfcActivityManager.this) {
221            NfcActivityState state = getActivityState(activity);
222            state.ndefMessage = message;
223            isResumed = state.resumed;
224        }
225        if (isResumed) {
226            requestNfcServiceCallback(true);
227        }
228    }
229
230    public void setNdefPushMessageCallback(Activity activity,
231            NfcAdapter.CreateNdefMessageCallback callback) {
232        boolean isResumed;
233        synchronized (NfcActivityManager.this) {
234            NfcActivityState state = getActivityState(activity);
235            state.ndefMessageCallback = callback;
236            isResumed = state.resumed;
237        }
238        if (isResumed) {
239            requestNfcServiceCallback(true);
240        }
241    }
242
243    public void setOnNdefPushCompleteCallback(Activity activity,
244            NfcAdapter.OnNdefPushCompleteCallback callback) {
245        boolean isResumed;
246        synchronized (NfcActivityManager.this) {
247            NfcActivityState state = getActivityState(activity);
248            state.onNdefPushCompleteCallback = callback;
249            isResumed = state.resumed;
250        }
251        if (isResumed) {
252            requestNfcServiceCallback(true);
253        }
254    }
255
256    /**
257     * Request or unrequest NFC service callbacks for NDEF push.
258     * Makes IPC call - do not hold lock.
259     * TODO: Do not do IPC on every onPause/onResume
260     */
261    void requestNfcServiceCallback(boolean request) {
262        try {
263            NfcAdapter.sService.setNdefPushCallback(request ? this : null);
264        } catch (RemoteException e) {
265            mAdapter.attemptDeadServiceRecovery(e);
266        }
267    }
268
269    /** Callback from NFC service, usually on binder thread */
270    @Override
271    public NdefMessage createMessage() {
272        NfcAdapter.CreateNdefMessageCallback callback;
273        NdefMessage message;
274        synchronized (NfcActivityManager.this) {
275            NfcActivityState state = findResumedActivityState();
276            if (state == null) return null;
277
278            callback = state.ndefMessageCallback;
279            message = state.ndefMessage;
280        }
281
282        // Make callback without lock
283        if (callback != null) {
284            return callback.createNdefMessage(mDefaultEvent);
285        } else {
286            return message;
287        }
288    }
289
290    /** Callback from NFC service, usually on binder thread */
291    @Override
292    public Uri[] getUris() {
293        Uri[] uris;
294        NfcAdapter.CreateBeamUrisCallback callback;
295        synchronized (NfcActivityManager.this) {
296            NfcActivityState state = findResumedActivityState();
297            if (state == null) return null;
298            uris = state.uris;
299            callback = state.uriCallback;
300        }
301        if (callback != null) {
302            uris = callback.createBeamUris(mDefaultEvent);
303            if (uris != null) {
304                for (Uri uri : uris) {
305                    if (uri == null) {
306                        Log.e(TAG, "Uri not allowed to be null.");
307                        return null;
308                    }
309                    String scheme = uri.getScheme();
310                    if (scheme == null || (!scheme.equalsIgnoreCase("file") &&
311                            !scheme.equalsIgnoreCase("content"))) {
312                        Log.e(TAG, "Uri needs to have " +
313                                "either scheme file or scheme content");
314                        return null;
315                    }
316                }
317            }
318            return uris;
319        } else {
320            return uris;
321        }
322    }
323
324    /** Callback from NFC service, usually on binder thread */
325    @Override
326    public void onNdefPushComplete() {
327        NfcAdapter.OnNdefPushCompleteCallback callback;
328        synchronized (NfcActivityManager.this) {
329            NfcActivityState state = findResumedActivityState();
330            if (state == null) return;
331
332            callback = state.onNdefPushCompleteCallback;
333        }
334
335        // Make callback without lock
336        if (callback != null) {
337            callback.onNdefPushComplete(mDefaultEvent);
338        }
339    }
340
341    /** Callback from Activity life-cycle, on main thread */
342    @Override
343    public void onActivityCreated(Activity activity, Bundle savedInstanceState) { /* NO-OP */ }
344
345    /** Callback from Activity life-cycle, on main thread */
346    @Override
347    public void onActivityStarted(Activity activity) { /* NO-OP */ }
348
349    /** Callback from Activity life-cycle, on main thread */
350    @Override
351    public void onActivityResumed(Activity activity) {
352        synchronized (NfcActivityManager.this) {
353            NfcActivityState state = findActivityState(activity);
354            if (DBG) Log.d(TAG, "onResume() for " + activity + " " + state);
355            if (state == null) return;
356            state.resumed = true;
357        }
358        requestNfcServiceCallback(true);
359    }
360
361    /** Callback from Activity life-cycle, on main thread */
362    @Override
363    public void onActivityPaused(Activity activity) {
364        synchronized (NfcActivityManager.this) {
365            NfcActivityState state = findActivityState(activity);
366            if (DBG) Log.d(TAG, "onPause() for " + activity + " " + state);
367            if (state == null) return;
368            state.resumed = false;
369        }
370        requestNfcServiceCallback(false);
371    }
372
373    /** Callback from Activity life-cycle, on main thread */
374    @Override
375    public void onActivityStopped(Activity activity) { /* NO-OP */ }
376
377    /** Callback from Activity life-cycle, on main thread */
378    @Override
379    public void onActivitySaveInstanceState(Activity activity, Bundle outState) { /* NO-OP */ }
380
381    /** Callback from Activity life-cycle, on main thread */
382    @Override
383    public void onActivityDestroyed(Activity activity) {
384        synchronized (NfcActivityManager.this) {
385            NfcActivityState state = findActivityState(activity);
386            if (DBG) Log.d(TAG, "onDestroy() for " + activity + " " + state);
387            if (state != null) {
388                // release all associated references
389                destroyActivityState(activity);
390            }
391        }
392    }
393
394}
395