blob: 0a5ce363f169db91ef5f3ce67746e1ec7fd8aa01 [file] [log] [blame]
/*
* Copyright (C) 2013 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 com.android.nfc.cardemulation;
import android.app.ActivityManager;
import android.app.KeyguardManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.nfc.cardemulation.ApduServiceInfo;
import android.nfc.cardemulation.CardEmulation;
import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import com.android.nfc.NfcService;
import com.android.nfc.cardemulation.RegisteredAidCache.AidResolveInfo;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
public class HostEmulationManager {
static final String TAG = "HostEmulationManager";
static final boolean DBG = false;
static final int STATE_IDLE = 0;
static final int STATE_W4_SELECT = 1;
static final int STATE_W4_SERVICE = 2;
static final int STATE_W4_DEACTIVATE = 3;
static final int STATE_XFER = 4;
/** Minimum AID lenth as per ISO7816 */
static final int MINIMUM_AID_LENGTH = 5;
/** Length of Select APDU header including length byte */
static final int SELECT_APDU_HDR_LENGTH = 5;
static final byte INSTR_SELECT = (byte)0xA4;
static final String ANDROID_HCE_AID = "A000000476416E64726F6964484345";
static final byte[] ANDROID_HCE_RESPONSE = {0x14, (byte)0x81, 0x00, 0x00, (byte)0x90, 0x00};
static final byte[] AID_NOT_FOUND = {0x6A, (byte)0x82};
static final byte[] UNKNOWN_ERROR = {0x6F, 0x00};
final Context mContext;
final RegisteredAidCache mAidCache;
final Messenger mMessenger = new Messenger (new MessageHandler());
final KeyguardManager mKeyguard;
final Object mLock;
// All variables below protected by mLock
// Variables below are for a non-payment service,
// that is typically only bound in the STATE_XFER state.
Messenger mService;
boolean mServiceBound = false;
ComponentName mServiceName = null;
// Variables below are for a payment service,
// which is typically bound persistently to improve on
// latency.
Messenger mPaymentService;
boolean mPaymentServiceBound = false;
ComponentName mPaymentServiceName = null;
ComponentName mLastBoundPaymentServiceName;
// mActiveService denotes the service interface
// that is the current active one, until a new SELECT AID
// comes in that may be resolved to a different service.
// On deactivation, mActiveService stops being valid.
Messenger mActiveService;
ComponentName mActiveServiceName;
String mLastSelectedAid;
int mState;
byte[] mSelectApdu;
public HostEmulationManager(Context context, RegisteredAidCache aidCache) {
mContext = context;
mLock = new Object();
mAidCache = aidCache;
mState = STATE_IDLE;
mKeyguard = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
}
public void onPreferredPaymentServiceChanged(ComponentName service) {
synchronized (mLock) {
if (service != null) {
bindPaymentServiceLocked(ActivityManager.getCurrentUser(), service);
} else {
unbindPaymentServiceLocked();
}
}
}
public void onPreferredForegroundServiceChanged(ComponentName service) {
synchronized (mLock) {
if (service != null) {
bindServiceIfNeededLocked(service);
} else {
unbindServiceIfNeededLocked();
}
}
}
public void onHostEmulationActivated() {
Log.d(TAG, "notifyHostEmulationActivated");
synchronized (mLock) {
// Regardless of what happens, if we're having a tap again
// activity up, close it
Intent intent = new Intent(TapAgainDialog.ACTION_CLOSE);
intent.setPackage("com.android.nfc");
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
if (mState != STATE_IDLE) {
Log.e(TAG, "Got activation event in non-idle state");
}
mState = STATE_W4_SELECT;
}
}
public void onHostEmulationData(byte[] data) {
Log.d(TAG, "notifyHostEmulationData");
String selectAid = findSelectAid(data);
ComponentName resolvedService = null;
synchronized (mLock) {
if (mState == STATE_IDLE) {
Log.e(TAG, "Got data in idle state.");
return;
} else if (mState == STATE_W4_DEACTIVATE) {
Log.e(TAG, "Dropping APDU in STATE_W4_DECTIVATE");
return;
}
if (selectAid != null) {
if (selectAid.equals(ANDROID_HCE_AID)) {
NfcService.getInstance().sendData(ANDROID_HCE_RESPONSE);
return;
}
AidResolveInfo resolveInfo = mAidCache.resolveAid(selectAid);
if (resolveInfo == null || resolveInfo.services.size() == 0) {
// Tell the remote we don't handle this AID
NfcService.getInstance().sendData(AID_NOT_FOUND);
return;
}
mLastSelectedAid = selectAid;
if (resolveInfo.defaultService != null) {
// Resolve to default
// Check if resolvedService requires unlock
ApduServiceInfo defaultServiceInfo = resolveInfo.defaultService;
if (defaultServiceInfo.requiresUnlock() &&
mKeyguard.isKeyguardLocked() && mKeyguard.isKeyguardSecure()) {
// Just ignore all future APDUs until next tap
mState = STATE_W4_DEACTIVATE;
launchTapAgain(resolveInfo.defaultService, resolveInfo.category);
return;
}
// In no circumstance should this be an OffHostService -
// we should never get this AID on the host in the first place
if (!defaultServiceInfo.isOnHost()) {
Log.e(TAG, "AID that was meant to go off-host was routed to host." +
" Check routing table configuration.");
NfcService.getInstance().sendData(AID_NOT_FOUND);
return;
}
resolvedService = defaultServiceInfo.getComponent();
} else if (mActiveServiceName != null) {
for (ApduServiceInfo serviceInfo : resolveInfo.services) {
if (mActiveServiceName.equals(serviceInfo.getComponent())) {
resolvedService = mActiveServiceName;
break;
}
}
}
if (resolvedService == null) {
// We have no default, and either one or more services.
// Ask the user to confirm.
// Just ignore all future APDUs until we resolve to only one
mState = STATE_W4_DEACTIVATE;
launchResolver((ArrayList<ApduServiceInfo>)resolveInfo.services, null,
resolveInfo.category);
return;
}
}
switch (mState) {
case STATE_W4_SELECT:
if (selectAid != null) {
Messenger existingService = bindServiceIfNeededLocked(resolvedService);
if (existingService != null) {
Log.d(TAG, "Binding to existing service");
mState = STATE_XFER;
sendDataToServiceLocked(existingService, data);
} else {
// Waiting for service to be bound
Log.d(TAG, "Waiting for new service.");
// Queue SELECT APDU to be used
mSelectApdu = data;
mState = STATE_W4_SERVICE;
}
} else {
Log.d(TAG, "Dropping non-select APDU in STATE_W4_SELECT");
NfcService.getInstance().sendData(UNKNOWN_ERROR);
}
break;
case STATE_W4_SERVICE:
Log.d(TAG, "Unexpected APDU in STATE_W4_SERVICE");
break;
case STATE_XFER:
if (selectAid != null) {
Messenger existingService = bindServiceIfNeededLocked(resolvedService);
if (existingService != null) {
sendDataToServiceLocked(existingService, data);
mState = STATE_XFER;
} else {
// Waiting for service to be bound
mSelectApdu = data;
mState = STATE_W4_SERVICE;
}
} else if (mActiveService != null) {
// Regular APDU data
sendDataToServiceLocked(mActiveService, data);
} else {
// No SELECT AID and no active service.
Log.d(TAG, "Service no longer bound, dropping APDU");
}
break;
}
}
}
public void onHostEmulationDeactivated() {
Log.d(TAG, "notifyHostEmulationDeactivated");
synchronized (mLock) {
if (mState == STATE_IDLE) {
Log.e(TAG, "Got deactivation event while in idle state");
}
sendDeactivateToActiveServiceLocked(HostApduService.DEACTIVATION_LINK_LOSS);
mActiveService = null;
mActiveServiceName = null;
unbindServiceIfNeededLocked();
mState = STATE_IDLE;
}
}
public void onOffHostAidSelected() {
Log.d(TAG, "notifyOffHostAidSelected");
synchronized (mLock) {
if (mState != STATE_XFER || mActiveService == null) {
// Don't bother telling, we're not bound to any service yet
} else {
sendDeactivateToActiveServiceLocked(HostApduService.DEACTIVATION_DESELECTED);
}
mActiveService = null;
mActiveServiceName = null;
unbindServiceIfNeededLocked();
mState = STATE_W4_SELECT;
//close the TapAgainDialog
Intent intent = new Intent(TapAgainDialog.ACTION_CLOSE);
intent.setPackage("com.android.nfc");
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
}
}
Messenger bindServiceIfNeededLocked(ComponentName service) {
if (mPaymentServiceName != null && mPaymentServiceName.equals(service)) {
Log.d(TAG, "Service already bound as payment service.");
return mPaymentService;
} else if (mServiceName != null && mServiceName.equals(service)) {
Log.d(TAG, "Service already bound as regular service.");
return mService;
} else {
Log.d(TAG, "Binding to service " + service);
unbindServiceIfNeededLocked();
Intent aidIntent = new Intent(HostApduService.SERVICE_INTERFACE);
aidIntent.setComponent(service);
if (mContext.bindServiceAsUser(aidIntent, mConnection,
Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) {
mServiceBound = true;
} else {
Log.e(TAG, "Could not bind service.");
}
return null;
}
}
void sendDataToServiceLocked(Messenger service, byte[] data) {
if (service != mActiveService) {
sendDeactivateToActiveServiceLocked(HostApduService.DEACTIVATION_DESELECTED);
mActiveService = service;
if (service.equals(mPaymentService)) {
mActiveServiceName = mPaymentServiceName;
} else {
mActiveServiceName = mServiceName;
}
}
Message msg = Message.obtain(null, HostApduService.MSG_COMMAND_APDU);
Bundle dataBundle = new Bundle();
dataBundle.putByteArray("data", data);
msg.setData(dataBundle);
msg.replyTo = mMessenger;
try {
mActiveService.send(msg);
} catch (RemoteException e) {
Log.e(TAG, "Remote service has died, dropping APDU");
}
}
void sendDeactivateToActiveServiceLocked(int reason) {
if (mActiveService == null) return;
Message msg = Message.obtain(null, HostApduService.MSG_DEACTIVATED);
msg.arg1 = reason;
try {
mActiveService.send(msg);
} catch (RemoteException e) {
// Don't care
}
}
void unbindPaymentServiceLocked() {
if (mPaymentServiceBound) {
mContext.unbindService(mPaymentConnection);
mPaymentServiceBound = false;
mPaymentService = null;
mPaymentServiceName = null;
}
}
void bindPaymentServiceLocked(int userId, ComponentName service) {
unbindPaymentServiceLocked();
Intent intent = new Intent(HostApduService.SERVICE_INTERFACE);
intent.setComponent(service);
mLastBoundPaymentServiceName = service;
if (mContext.bindServiceAsUser(intent, mPaymentConnection,
Context.BIND_AUTO_CREATE, new UserHandle(userId))) {
mPaymentServiceBound = true;
} else {
Log.e(TAG, "Could not bind (persistent) payment service.");
}
}
void unbindServiceIfNeededLocked() {
if (mServiceBound) {
Log.d(TAG, "Unbinding from service " + mServiceName);
mContext.unbindService(mConnection);
mServiceBound = false;
mService = null;
mServiceName = null;
}
}
void launchTapAgain(ApduServiceInfo service, String category) {
Intent dialogIntent = new Intent(mContext, TapAgainDialog.class);
dialogIntent.putExtra(TapAgainDialog.EXTRA_CATEGORY, category);
dialogIntent.putExtra(TapAgainDialog.EXTRA_APDU_SERVICE, service);
dialogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
mContext.startActivityAsUser(dialogIntent, UserHandle.CURRENT);
}
void launchResolver(ArrayList<ApduServiceInfo> services, ComponentName failedComponent,
String category) {
Intent intent = new Intent(mContext, AppChooserActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.putParcelableArrayListExtra(AppChooserActivity.EXTRA_APDU_SERVICES, services);
intent.putExtra(AppChooserActivity.EXTRA_CATEGORY, category);
if (failedComponent != null) {
intent.putExtra(AppChooserActivity.EXTRA_FAILED_COMPONENT, failedComponent);
}
mContext.startActivityAsUser(intent, UserHandle.CURRENT);
}
String findSelectAid(byte[] data) {
if (data == null || data.length < SELECT_APDU_HDR_LENGTH + MINIMUM_AID_LENGTH) {
if (DBG) Log.d(TAG, "Data size too small for SELECT APDU");
return null;
}
// To accept a SELECT AID for dispatch, we require the following:
// Class byte must be 0x00: logical channel set to zero, no secure messaging, no chaining
// Instruction byte must be 0xA4: SELECT instruction
// P1: must be 0x04: select by application identifier
// P2: File control information is only relevant for higher-level application,
// and we only support "first or only occurrence".
if (data[0] == 0x00 && data[1] == INSTR_SELECT && data[2] == 0x04) {
if (data[3] != 0x00) {
Log.d(TAG, "Selecting next, last or previous AID occurrence is not supported");
}
int aidLength = data[4];
if (data.length < SELECT_APDU_HDR_LENGTH + aidLength) {
return null;
}
return bytesToString(data, SELECT_APDU_HDR_LENGTH, aidLength);
}
return null;
}
private ServiceConnection mPaymentConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mLock) {
/* Preferred Payment Service has been changed. */
if (!mLastBoundPaymentServiceName.equals(name)) {
return;
}
mPaymentServiceName = name;
mPaymentService = new Messenger(service);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (mLock) {
mPaymentService = null;
mPaymentServiceBound = false;
mPaymentServiceName = null;
}
}
};
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mLock) {
/* Service is already deactivated, don't bind */
if (mState == STATE_IDLE) {
return;
}
mService = new Messenger(service);
mServiceName = name;
Log.d(TAG, "Service bound");
mState = STATE_XFER;
// Send pending select APDU
if (mSelectApdu != null) {
sendDataToServiceLocked(mService, mSelectApdu);
mSelectApdu = null;
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (mLock) {
Log.d(TAG, "Service unbound");
mService = null;
mServiceBound = false;
}
}
};
class MessageHandler extends Handler {
@Override
public void handleMessage(Message msg) {
synchronized(mLock) {
if (mActiveService == null) {
Log.d(TAG, "Dropping service response message; service no longer active.");
return;
} else if (!msg.replyTo.getBinder().equals(mActiveService.getBinder())) {
Log.d(TAG, "Dropping service response message; service no longer bound.");
return;
}
}
if (msg.what == HostApduService.MSG_RESPONSE_APDU) {
Bundle dataBundle = msg.getData();
if (dataBundle == null) {
return;
}
byte[] data = dataBundle.getByteArray("data");
if (data == null || data.length == 0) {
Log.e(TAG, "Dropping empty R-APDU");
return;
}
int state;
synchronized(mLock) {
state = mState;
}
if (state == STATE_XFER) {
Log.d(TAG, "Sending data");
NfcService.getInstance().sendData(data);
} else {
Log.d(TAG, "Dropping data, wrong state " + Integer.toString(state));
}
} else if (msg.what == HostApduService.MSG_UNHANDLED) {
synchronized (mLock) {
AidResolveInfo resolveInfo = mAidCache.resolveAid(mLastSelectedAid);
boolean isPayment = false;
if (resolveInfo.services.size() > 0) {
launchResolver((ArrayList<ApduServiceInfo>)resolveInfo.services,
mActiveServiceName, resolveInfo.category);
}
}
}
}
}
static String bytesToString(byte[] bytes, int offset, int length) {
final char[] hexChars = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
char[] chars = new char[length * 2];
int byteValue;
for (int j = 0; j < length; j++) {
byteValue = bytes[offset + j] & 0xFF;
chars[j * 2] = hexChars[byteValue >>> 4];
chars[j * 2 + 1] = hexChars[byteValue & 0x0F];
}
return new String(chars);
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("Bound HCE-A/HCE-B services: ");
if (mPaymentServiceBound) {
pw.println(" payment: " + mPaymentServiceName);
}
if (mServiceBound) {
pw.println(" other: " + mServiceName);
}
}
}