1/*
2 * Copyright (C) 2013 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 com.android.nfc.cardemulation;
18
19import android.app.ActivityManager;
20import android.app.KeyguardManager;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.ServiceConnection;
25import android.nfc.cardemulation.ApduServiceInfo;
26import android.nfc.cardemulation.CardEmulation;
27import android.nfc.cardemulation.HostApduService;
28import android.os.Bundle;
29import android.os.Handler;
30import android.os.IBinder;
31import android.os.Message;
32import android.os.Messenger;
33import android.os.RemoteException;
34import android.os.UserHandle;
35import android.util.Log;
36
37import com.android.nfc.NfcService;
38import com.android.nfc.cardemulation.RegisteredAidCache.AidResolveInfo;
39
40import java.io.FileDescriptor;
41import java.io.PrintWriter;
42import java.util.ArrayList;
43
44public class HostEmulationManager {
45    static final String TAG = "HostEmulationManager";
46    static final boolean DBG = false;
47
48    static final int STATE_IDLE = 0;
49    static final int STATE_W4_SELECT = 1;
50    static final int STATE_W4_SERVICE = 2;
51    static final int STATE_W4_DEACTIVATE = 3;
52    static final int STATE_XFER = 4;
53
54    /** Minimum AID lenth as per ISO7816 */
55    static final int MINIMUM_AID_LENGTH = 5;
56
57    /** Length of Select APDU header including length byte */
58    static final int SELECT_APDU_HDR_LENGTH = 5;
59
60    static final byte INSTR_SELECT = (byte)0xA4;
61
62    static final String ANDROID_HCE_AID = "A000000476416E64726F6964484345";
63    static final byte[] ANDROID_HCE_RESPONSE = {0x14, (byte)0x81, 0x00, 0x00, (byte)0x90, 0x00};
64
65    static final byte[] AID_NOT_FOUND = {0x6A, (byte)0x82};
66    static final byte[] UNKNOWN_ERROR = {0x6F, 0x00};
67
68    final Context mContext;
69    final RegisteredAidCache mAidCache;
70    final Messenger mMessenger = new Messenger (new MessageHandler());
71    final KeyguardManager mKeyguard;
72    final Object mLock;
73
74    // All variables below protected by mLock
75
76    // Variables below are for a non-payment service,
77    // that is typically only bound in the STATE_XFER state.
78    Messenger mService;
79    boolean mServiceBound;
80    ComponentName mServiceName;
81
82    // Variables below are for a payment service,
83    // which is typically bound persistently to improve on
84    // latency.
85    Messenger mPaymentService;
86    boolean mPaymentServiceBound;
87    ComponentName mPaymentServiceName;
88
89    // mActiveService denotes the service interface
90    // that is the current active one, until a new SELECT AID
91    // comes in that may be resolved to a different service.
92    // On deactivation, mActiveService stops being valid.
93    Messenger mActiveService;
94    ComponentName mActiveServiceName;
95
96    String mLastSelectedAid;
97    int mState;
98    byte[] mSelectApdu;
99
100    public HostEmulationManager(Context context, RegisteredAidCache aidCache) {
101        mContext = context;
102        mLock = new Object();
103        mAidCache = aidCache;
104        mState = STATE_IDLE;
105        mKeyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
106    }
107
108    public void onPreferredPaymentServiceChanged(ComponentName service) {
109        synchronized (mLock) {
110            if (service != null) {
111                bindPaymentServiceLocked(ActivityManager.getCurrentUser(), service);
112            } else {
113                unbindPaymentServiceLocked();
114            }
115        }
116     }
117
118     public void onPreferredForegroundServiceChanged(ComponentName service) {
119         synchronized (mLock) {
120            if (service != null) {
121               bindServiceIfNeededLocked(service);
122            } else {
123               unbindServiceIfNeededLocked();
124            }
125         }
126     }
127
128    public void onHostEmulationActivated() {
129        Log.d(TAG, "notifyHostEmulationActivated");
130        synchronized (mLock) {
131            // Regardless of what happens, if we're having a tap again
132            // activity up, close it
133            Intent intent = new Intent(TapAgainDialog.ACTION_CLOSE);
134            intent.setPackage("com.android.nfc");
135            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
136            if (mState != STATE_IDLE) {
137                Log.e(TAG, "Got activation event in non-idle state");
138            }
139            mState = STATE_W4_SELECT;
140        }
141    }
142
143    public void onHostEmulationData(byte[] data) {
144        Log.d(TAG, "notifyHostEmulationData");
145        String selectAid = findSelectAid(data);
146        ComponentName resolvedService = null;
147        synchronized (mLock) {
148            if (mState == STATE_IDLE) {
149                Log.e(TAG, "Got data in idle state.");
150                return;
151            } else if (mState == STATE_W4_DEACTIVATE) {
152                Log.e(TAG, "Dropping APDU in STATE_W4_DECTIVATE");
153                return;
154            }
155            if (selectAid != null) {
156                if (selectAid.equals(ANDROID_HCE_AID)) {
157                    NfcService.getInstance().sendData(ANDROID_HCE_RESPONSE);
158                    return;
159                }
160                AidResolveInfo resolveInfo = mAidCache.resolveAid(selectAid);
161                if (resolveInfo == null || resolveInfo.services.size() == 0) {
162                    // Tell the remote we don't handle this AID
163                    NfcService.getInstance().sendData(AID_NOT_FOUND);
164                    return;
165                }
166                mLastSelectedAid = selectAid;
167                if (resolveInfo.defaultService != null) {
168                    // Resolve to default
169                    // Check if resolvedService requires unlock
170                    ApduServiceInfo defaultServiceInfo = resolveInfo.defaultService;
171                    if (defaultServiceInfo.requiresUnlock() &&
172                            mKeyguard.isKeyguardLocked() && mKeyguard.isKeyguardSecure()) {
173                        // Just ignore all future APDUs until next tap
174                        mState = STATE_W4_DEACTIVATE;
175                        launchTapAgain(resolveInfo.defaultService, resolveInfo.category);
176                        return;
177                    }
178                    // In no circumstance should this be an OffHostService -
179                    // we should never get this AID on the host in the first place
180                    if (!defaultServiceInfo.isOnHost()) {
181                        Log.e(TAG, "AID that was meant to go off-host was routed to host." +
182                                " Check routing table configuration.");
183                        NfcService.getInstance().sendData(AID_NOT_FOUND);
184                        return;
185                    }
186                    resolvedService = defaultServiceInfo.getComponent();
187                } else if (mActiveServiceName != null) {
188                    for (ApduServiceInfo serviceInfo : resolveInfo.services) {
189                        if (mActiveServiceName.equals(serviceInfo.getComponent())) {
190                            resolvedService = mActiveServiceName;
191                            break;
192                        }
193                    }
194                }
195                if (resolvedService == null) {
196                    // We have no default, and either one or more services.
197                    // Ask the user to confirm.
198                    // Just ignore all future APDUs until we resolve to only one
199                    mState = STATE_W4_DEACTIVATE;
200                    launchResolver((ArrayList<ApduServiceInfo>)resolveInfo.services, null,
201                            resolveInfo.category);
202                    return;
203                }
204            }
205            switch (mState) {
206            case STATE_W4_SELECT:
207                if (selectAid != null) {
208                    Messenger existingService = bindServiceIfNeededLocked(resolvedService);
209                    if (existingService != null) {
210                        Log.d(TAG, "Binding to existing service");
211                        mState = STATE_XFER;
212                        sendDataToServiceLocked(existingService, data);
213                    } else {
214                        // Waiting for service to be bound
215                        Log.d(TAG, "Waiting for new service.");
216                        // Queue SELECT APDU to be used
217                        mSelectApdu = data;
218                        mState = STATE_W4_SERVICE;
219                    }
220                } else {
221                    Log.d(TAG, "Dropping non-select APDU in STATE_W4_SELECT");
222                    NfcService.getInstance().sendData(UNKNOWN_ERROR);
223                }
224                break;
225            case STATE_W4_SERVICE:
226                Log.d(TAG, "Unexpected APDU in STATE_W4_SERVICE");
227                break;
228            case STATE_XFER:
229                if (selectAid != null) {
230                    Messenger existingService = bindServiceIfNeededLocked(resolvedService);
231                    if (existingService != null) {
232                        sendDataToServiceLocked(existingService, data);
233                        mState = STATE_XFER;
234                    } else {
235                        // Waiting for service to be bound
236                        mSelectApdu = data;
237                        mState = STATE_W4_SERVICE;
238                    }
239                } else if (mActiveService != null) {
240                    // Regular APDU data
241                    sendDataToServiceLocked(mActiveService, data);
242                } else {
243                    // No SELECT AID and no active service.
244                    Log.d(TAG, "Service no longer bound, dropping APDU");
245                }
246                break;
247            }
248        }
249    }
250
251    public void onHostEmulationDeactivated() {
252        Log.d(TAG, "notifyHostEmulationDeactivated");
253        synchronized (mLock) {
254            if (mState == STATE_IDLE) {
255                Log.e(TAG, "Got deactivation event while in idle state");
256            }
257            sendDeactivateToActiveServiceLocked(HostApduService.DEACTIVATION_LINK_LOSS);
258            mActiveService = null;
259            mActiveServiceName = null;
260            unbindServiceIfNeededLocked();
261            mState = STATE_IDLE;
262        }
263    }
264
265    public void onOffHostAidSelected() {
266        Log.d(TAG, "notifyOffHostAidSelected");
267        synchronized (mLock) {
268            if (mState != STATE_XFER || mActiveService == null) {
269                // Don't bother telling, we're not bound to any service yet
270            } else {
271                sendDeactivateToActiveServiceLocked(HostApduService.DEACTIVATION_DESELECTED);
272            }
273            mActiveService = null;
274            mActiveServiceName = null;
275            unbindServiceIfNeededLocked();
276            mState = STATE_W4_SELECT;
277
278            //close the TapAgainDialog
279            Intent intent = new Intent(TapAgainDialog.ACTION_CLOSE);
280            intent.setPackage("com.android.nfc");
281            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
282        }
283    }
284
285    Messenger bindServiceIfNeededLocked(ComponentName service) {
286        if (mPaymentServiceBound && mPaymentServiceName.equals(service)) {
287            Log.d(TAG, "Service already bound as payment service.");
288            return mPaymentService;
289        } else if (mServiceBound && mServiceName.equals(service)) {
290            Log.d(TAG, "Service already bound as regular service.");
291            return mService;
292        } else {
293            Log.d(TAG, "Binding to service " + service);
294            unbindServiceIfNeededLocked();
295            Intent aidIntent = new Intent(HostApduService.SERVICE_INTERFACE);
296            aidIntent.setComponent(service);
297            if (mContext.bindServiceAsUser(aidIntent, mConnection,
298                    Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) {
299            } else {
300                Log.e(TAG, "Could not bind service.");
301            }
302            return null;
303        }
304    }
305
306    void sendDataToServiceLocked(Messenger service, byte[] data) {
307        if (service != mActiveService) {
308            sendDeactivateToActiveServiceLocked(HostApduService.DEACTIVATION_DESELECTED);
309            mActiveService = service;
310            if (service.equals(mPaymentService)) {
311                mActiveServiceName = mPaymentServiceName;
312            } else {
313                mActiveServiceName = mServiceName;
314            }
315        }
316        Message msg = Message.obtain(null, HostApduService.MSG_COMMAND_APDU);
317        Bundle dataBundle = new Bundle();
318        dataBundle.putByteArray("data", data);
319        msg.setData(dataBundle);
320        msg.replyTo = mMessenger;
321        try {
322            mActiveService.send(msg);
323        } catch (RemoteException e) {
324            Log.e(TAG, "Remote service has died, dropping APDU");
325        }
326    }
327
328    void sendDeactivateToActiveServiceLocked(int reason) {
329        if (mActiveService == null) return;
330        Message msg = Message.obtain(null, HostApduService.MSG_DEACTIVATED);
331        msg.arg1 = reason;
332        try {
333            mActiveService.send(msg);
334        } catch (RemoteException e) {
335            // Don't care
336        }
337    }
338
339    void unbindPaymentServiceLocked() {
340        if (mPaymentServiceBound) {
341            mContext.unbindService(mPaymentConnection);
342            mPaymentServiceBound = false;
343            mPaymentService = null;
344            mPaymentServiceName = null;
345        }
346    }
347
348    void bindPaymentServiceLocked(int userId, ComponentName service) {
349        unbindPaymentServiceLocked();
350
351        Intent intent = new Intent(HostApduService.SERVICE_INTERFACE);
352        intent.setComponent(service);
353        if (!mContext.bindServiceAsUser(intent, mPaymentConnection,
354                Context.BIND_AUTO_CREATE, new UserHandle(userId))) {
355            Log.e(TAG, "Could not bind (persistent) payment service.");
356        }
357    }
358
359    void unbindServiceIfNeededLocked() {
360        if (mServiceBound) {
361            Log.d(TAG, "Unbinding from service " + mServiceName);
362            mContext.unbindService(mConnection);
363            mServiceBound = false;
364            mService = null;
365            mServiceName = null;
366        }
367    }
368
369    void launchTapAgain(ApduServiceInfo service, String category) {
370        Intent dialogIntent = new Intent(mContext, TapAgainDialog.class);
371        dialogIntent.putExtra(TapAgainDialog.EXTRA_CATEGORY, category);
372        dialogIntent.putExtra(TapAgainDialog.EXTRA_APDU_SERVICE, service);
373        dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
374        mContext.startActivityAsUser(dialogIntent, UserHandle.CURRENT);
375    }
376
377    void launchResolver(ArrayList<ApduServiceInfo> services, ComponentName failedComponent,
378            String category) {
379        Intent intent = new Intent(mContext, AppChooserActivity.class);
380        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
381        intent.putParcelableArrayListExtra(AppChooserActivity.EXTRA_APDU_SERVICES, services);
382        intent.putExtra(AppChooserActivity.EXTRA_CATEGORY, category);
383        if (failedComponent != null) {
384            intent.putExtra(AppChooserActivity.EXTRA_FAILED_COMPONENT, failedComponent);
385        }
386        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
387    }
388
389    String findSelectAid(byte[] data) {
390        if (data == null || data.length < SELECT_APDU_HDR_LENGTH + MINIMUM_AID_LENGTH) {
391            if (DBG) Log.d(TAG, "Data size too small for SELECT APDU");
392            return null;
393        }
394        // To accept a SELECT AID for dispatch, we require the following:
395        // Class byte must be 0x00: logical channel set to zero, no secure messaging, no chaining
396        // Instruction byte must be 0xA4: SELECT instruction
397        // P1: must be 0x04: select by application identifier
398        // P2: File control information is only relevant for higher-level application,
399        //     and we only support "first or only occurrence".
400        if (data[0] == 0x00 && data[1] == INSTR_SELECT && data[2] == 0x04) {
401            if (data[3] != 0x00) {
402                Log.d(TAG, "Selecting next, last or previous AID occurrence is not supported");
403            }
404            int aidLength = data[4];
405            if (data.length < SELECT_APDU_HDR_LENGTH + aidLength) {
406                return null;
407            }
408            return bytesToString(data, SELECT_APDU_HDR_LENGTH, aidLength);
409        }
410        return null;
411    }
412
413    private ServiceConnection mPaymentConnection = new ServiceConnection() {
414        @Override
415        public void onServiceConnected(ComponentName name, IBinder service) {
416            synchronized (mLock) {
417                mPaymentServiceName = name;
418                mPaymentService = new Messenger(service);
419                mPaymentServiceBound = true;
420            }
421        }
422
423        @Override
424        public void onServiceDisconnected(ComponentName name) {
425            synchronized (mLock) {
426                mPaymentService = null;
427                mPaymentServiceBound = false;
428                mPaymentServiceName = null;
429            }
430        }
431    };
432
433    private ServiceConnection mConnection = new ServiceConnection() {
434        @Override
435        public void onServiceConnected(ComponentName name, IBinder service) {
436            synchronized (mLock) {
437                mService = new Messenger(service);
438                mServiceBound = true;
439                mServiceName = name;
440                Log.d(TAG, "Service bound");
441                mState = STATE_XFER;
442                // Send pending select APDU
443                if (mSelectApdu != null) {
444                    sendDataToServiceLocked(mService, mSelectApdu);
445                    mSelectApdu = null;
446                }
447            }
448        }
449
450        @Override
451        public void onServiceDisconnected(ComponentName name) {
452            synchronized (mLock) {
453                Log.d(TAG, "Service unbound");
454                mService = null;
455                mServiceBound = false;
456            }
457        }
458    };
459
460    class MessageHandler extends Handler {
461        @Override
462        public void handleMessage(Message msg) {
463            synchronized(mLock) {
464                if (mActiveService == null) {
465                    Log.d(TAG, "Dropping service response message; service no longer active.");
466                    return;
467                } else if (!msg.replyTo.getBinder().equals(mActiveService.getBinder())) {
468                    Log.d(TAG, "Dropping service response message; service no longer bound.");
469                    return;
470                }
471            }
472            if (msg.what == HostApduService.MSG_RESPONSE_APDU) {
473                Bundle dataBundle = msg.getData();
474                if (dataBundle == null) {
475                    return;
476                }
477                byte[] data = dataBundle.getByteArray("data");
478                if (data == null || data.length == 0) {
479                    Log.e(TAG, "Dropping empty R-APDU");
480                    return;
481                }
482                int state;
483                synchronized(mLock) {
484                    state = mState;
485                }
486                if (state == STATE_XFER) {
487                    Log.d(TAG, "Sending data");
488                    NfcService.getInstance().sendData(data);
489                } else {
490                    Log.d(TAG, "Dropping data, wrong state " + Integer.toString(state));
491                }
492            } else if (msg.what == HostApduService.MSG_UNHANDLED) {
493                synchronized (mLock) {
494                    AidResolveInfo resolveInfo = mAidCache.resolveAid(mLastSelectedAid);
495                    boolean isPayment = false;
496                    if (resolveInfo.services.size() > 0) {
497                        launchResolver((ArrayList<ApduServiceInfo>)resolveInfo.services,
498                                mActiveServiceName, resolveInfo.category);
499                    }
500                }
501            }
502        }
503    }
504
505    static String bytesToString(byte[] bytes, int offset, int length) {
506        final char[] hexChars = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
507        char[] chars = new char[length * 2];
508        int byteValue;
509        for (int j = 0; j < length; j++) {
510            byteValue = bytes[offset + j] & 0xFF;
511            chars[j * 2] = hexChars[byteValue >>> 4];
512            chars[j * 2 + 1] = hexChars[byteValue & 0x0F];
513        }
514        return new String(chars);
515    }
516
517    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
518        pw.println("Bound services: ");
519        if (mPaymentServiceBound) {
520            pw.println("    payment: " + mPaymentServiceName);
521        }
522        if (mServiceBound) {
523            pw.println("    other: " + mServiceName);
524        }
525    }
526}
527