HostEmulationManager.java revision 75f63db568f953e935e62cb3046d167b881979c8
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.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.ServiceConnection;
24import android.nfc.cardemulation.ApduServiceInfo;
25import android.nfc.cardemulation.HostApduService;
26import android.os.Bundle;
27import android.os.Handler;
28import android.os.IBinder;
29import android.os.Message;
30import android.os.Messenger;
31import android.os.RemoteException;
32import android.os.UserHandle;
33import android.util.Log;
34import com.android.nfc.NfcService;
35
36import java.util.ArrayList;
37
38public class HostEmulationManager {
39    static final String TAG = "HostEmulationManager";
40
41    static final int STATE_IDLE = 0;
42    static final int STATE_W4_SELECT = 1;
43    static final int STATE_W4_SERVICE = 2;
44    static final int STATE_XFER = 3;
45
46    final Context mContext;
47    final RegisteredServicesCache mAidCache;
48    final Messenger mMessenger = new Messenger (new MessageHandler());
49
50    final Object mLock;
51
52    // Variables below protected by mLock
53    boolean mBound;
54    boolean mClearNextTapDefault;
55    Messenger mService;
56    int mState;
57    byte[] mSelectApdu;
58
59    public HostEmulationManager(Context context, RegisteredServicesCache aidCache) {
60        mContext = context;
61        mLock = new Object();
62        mAidCache = aidCache;
63        mState = STATE_IDLE;
64    }
65
66    public void notifyHostEmulationActivated() {
67        Log.d(TAG, "notifyHostEmulationActivated");
68        synchronized (mLock) {
69            mClearNextTapDefault = mAidCache.isNextTapOverriden();
70            if (mState != STATE_IDLE) {
71                Log.e(TAG, "Got activation event in non-idle state");
72                if (mState >= STATE_W4_SERVICE) {
73                    unbindServiceLocked();
74                }
75            }
76            mState = STATE_W4_SELECT;
77        }
78    }
79
80    public void notifyHostEmulationData(byte[] data) {
81        Log.d(TAG, "notifyHostEmulationData");
82        String selectAid = findSelectAid(data);
83        synchronized (mLock) {
84            switch (mState) {
85            case STATE_IDLE:
86                Log.e(TAG, "Got data in idle state");
87                break;
88            case STATE_W4_SELECT:
89                if (selectAid != null) {
90                    mSelectApdu = data;
91                    if (dispatchAidLocked(selectAid))
92                        mState = STATE_W4_SERVICE;
93                } else {
94                    Log.d(TAG, "Dropping non-select APDU in STATE_W4_SELECT");
95                }
96                break;
97            case STATE_W4_SERVICE:
98                if (selectAid != null) {
99                    // This should normally not happen, but deal with it gracefully
100                    unbindServiceLocked();
101                    mSelectApdu = data;
102                    if (dispatchAidLocked(selectAid))
103                        mState = STATE_W4_SERVICE;
104                } else {
105                    Log.d(TAG, "Unexpected APDU in STATE_W4_SERVICE");
106                }
107                break;
108            case STATE_XFER:
109                // TODO if we get another select we may need to resolve
110                // it to a different service
111                if (mService != null) {
112                    Message msg = Message.obtain(null, HostApduService.MSG_COMMAND_APDU);
113                    Bundle dataBundle = new Bundle();
114                    dataBundle.putByteArray("data", data);
115                    msg.setData(dataBundle);
116                    msg.replyTo = mMessenger;
117                    try {
118                        Log.d(TAG, "Sending data to service");
119                        mService.send(msg);
120                    } catch (RemoteException e) {
121                        Log.e(TAG, "Remote service has died, dropping APDU");
122                    }
123                } else {
124                    Log.d(TAG, "Service no longer bound, dropping APDU");
125                }
126                break;
127            }
128        }
129    }
130
131    public void notifyNostEmulationDeactivated() {
132        Log.d(TAG, "notifyHostEmulationDeactivated");
133        synchronized (mLock) {
134            if (mClearNextTapDefault) {
135                mAidCache.setDefaultForNextTap(ActivityManager.getCurrentUser(), null);
136            }
137            if (mState == STATE_IDLE) {
138                Log.e(TAG, "Got deactivation event while in idle state");
139                return;
140            }
141            if (mService != null) {
142                // Send a MSG_DEACTIVATED
143                Message msg = Message.obtain(null, HostApduService.MSG_DEACTIVATED);
144                try {
145                    mService.send(msg);
146                } catch (RemoteException e) {
147                    // Don't care
148                }
149            }
150            unbindServiceLocked();
151            mState = STATE_IDLE;
152        }
153    }
154
155    void unbindServiceLocked() {
156        mContext.unbindService(mConnection);
157        synchronized (mLock) {
158            mBound = false;
159            mService = null;
160        }
161    }
162
163    void launchResolver(ArrayList<ComponentName> components, String category) {
164        Intent intent = new Intent(mContext, AppChooserActivity.class);
165        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
166        intent.putParcelableArrayListExtra(AppChooserActivity.EXTRA_COMPONENTS, components);
167        intent.putExtra(AppChooserActivity.EXTRA_CATEGORY, category);
168        mContext.startActivityAsUser(intent, UserHandle.CURRENT);
169    }
170
171    String findSelectAid(byte[] data) {
172        if (data == null || data.length < 6) {
173            Log.d(TAG, "Data size too small for SELECT APDU");
174        }
175        // TODO we'll support only logical channel 0 in CLA?
176        // TODO support chaining bits in CLA?
177        // TODO what about selection using DF/EF identifiers and/or path?
178        // TODO what about P2?
179        // TODO what about the default selection status?
180        if (data[0] == 0x00 && data[1] == (byte)0xA4 && data[2] == 0x04) {
181            int aidLength = data[4];
182            if (data.length < 5 + aidLength) {
183                return null;
184            }
185            return bytesToString(data, 5, aidLength);
186        }
187        return null;
188    }
189
190    boolean dispatchAidLocked(String aid) {
191        Log.d(TAG, "dispatchAidLocked");
192        // Regardless of what happens, if we're having a tap again
193        // activity up, close it
194        Intent intent = new Intent(TapAgainDialog.ACTION_CLOSE);
195        intent.setPackage("com.android.nfc");
196        mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
197        int userId = ActivityManager.getCurrentUser();
198        ArrayList<ApduServiceInfo> matchingServices = mAidCache.resolveAidPrefix(userId, aid);
199        if (matchingServices == null || matchingServices.size() == 0) {
200            // TODO return 6A82?
201            Log.e(TAG, "Could not find matching services for AID " + aid);
202        } else if (matchingServices.size() == 1) {
203            ComponentName service = matchingServices.get(0).getComponent();
204            Intent aidIntent = new Intent(HostApduService.SERVICE_INTERFACE);
205            aidIntent.setComponent(service);
206            Log.e(TAG, "Resolved to service " + service);
207            if (mContext.bindServiceAsUser(aidIntent, mConnection, Context.BIND_AUTO_CREATE,
208                    new UserHandle(userId))) {
209                return true;
210            } else {
211                Log.e(TAG, "Could not bind service");
212            }
213        } else {
214            final ArrayList<ComponentName> components = new ArrayList<ComponentName>();
215            for (ApduServiceInfo service : matchingServices) {
216               components.add(service.getComponent());
217            }
218            // Get corresponding category
219            String category = mAidCache.getCategoryForAid(userId, aid);
220            launchResolver(components, category);
221        }
222        return false;
223    }
224
225    private ServiceConnection mConnection = new ServiceConnection() {
226        @Override
227        public void onServiceConnected(ComponentName name, IBinder service) {
228            synchronized (mLock) {
229                mService = new Messenger(service);
230                mBound = true;
231                Log.d(TAG, "Service bound");
232                mState = STATE_XFER;
233                // Send pending select APDU
234                if (mSelectApdu != null) {
235                    Message msg = Message.obtain(null, HostApduService.MSG_COMMAND_APDU);
236                    Bundle dataBundle = new Bundle();
237                    dataBundle.putByteArray("data", mSelectApdu);
238                    msg.setData(dataBundle);
239                    msg.replyTo = mMessenger;
240                    try {
241                        mService.send(msg);
242                    } catch (RemoteException e) {
243                        Log.d(TAG, "Service gone!");
244                    }
245                    mSelectApdu = null;
246                }
247            }
248        }
249
250        @Override
251        public void onServiceDisconnected(ComponentName name) {
252            synchronized (mLock) {
253                Log.d(TAG, "Service unbound");
254                mService = null;
255                mBound = false;
256            }
257        }
258    };
259
260    class MessageHandler extends Handler {
261        @Override
262        public void handleMessage(Message msg) {
263            if (msg.what == HostApduService.MSG_RESPONSE_APDU) {
264                Bundle dataBundle = msg.getData();
265                if (dataBundle == null) {
266                    return;
267                }
268                byte[] data = dataBundle.getByteArray("data");
269                if (data == null || data.length == 0) {
270                    Log.e(TAG, "Dropping empty R-APDU");
271                    return;
272                }
273                int state;
274                synchronized(mLock) {
275                    state = mState;
276                }
277                if (state == STATE_XFER) {
278                    Log.d(TAG, "Sending data");
279                    NfcService.getInstance().sendData(data);
280                } else {
281                    Log.d(TAG, "Dropping data, wrong state " + Integer.toString(state));
282                }
283            }
284        }
285    }
286
287    static String bytesToString(byte[] bytes, int offset, int length) {
288        final char[] hexChars = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
289        char[] chars = new char[length * 2];
290        int byteValue;
291        for (int j = 0; j < length; j++) {
292            byteValue = bytes[offset + j] & 0xFF;
293            chars[j * 2] = hexChars[byteValue >>> 4];
294            chars[j * 2 + 1] = hexChars[byteValue & 0x0F];
295        }
296        return new String(chars);
297    }
298}
299