blob: 8b9e7deea95f211b1d6a0799551a5f1715e1a4d4 [file] [log] [blame]
/*
* Copyright (C) 2015 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.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.nfc.cardemulation.HostNfcFService;
import android.nfc.cardemulation.NfcFServiceInfo;
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 android.util.proto.ProtoOutputStream;
import com.android.nfc.NfcService;
import com.android.nfc.NfcStatsLog;
import java.io.FileDescriptor;
import java.io.PrintWriter;
public class HostNfcFEmulationManager {
static final String TAG = "HostNfcFEmulationManager";
static final boolean DBG = false;
static final int STATE_IDLE = 0;
static final int STATE_W4_SERVICE = 1;
static final int STATE_XFER = 2;
/** NFCID2 length */
static final int NFCID2_LENGTH = 8;
/** Minimum NFC-F packets including length, command code and NFCID2 */
static final int MINIMUM_NFCF_PACKET_LENGTH = 10;
final Context mContext;
final RegisteredT3tIdentifiersCache mT3tIdentifiersCache;
final Messenger mMessenger = new Messenger (new MessageHandler());
final Object mLock;
// All variables below protected by mLock
ComponentName mEnabledFgServiceName;
Messenger mService;
boolean mServiceBound;
ComponentName mServiceName;
// mActiveService denotes the service interface
// that is the current active one, until a new packet
// comes in that may be resolved to a different service.
// On deactivation, mActiveService stops being valid.
Messenger mActiveService;
ComponentName mActiveServiceName;
int mState;
byte[] mPendingPacket;
public HostNfcFEmulationManager(Context context,
RegisteredT3tIdentifiersCache t3tIdentifiersCache) {
mContext = context;
mLock = new Object();
mEnabledFgServiceName = null;
mT3tIdentifiersCache = t3tIdentifiersCache;
mState = STATE_IDLE;
}
public void onEnabledForegroundNfcFServiceChanged(ComponentName service) {
synchronized (mLock) {
mEnabledFgServiceName = service;
if (service == null) {
sendDeactivateToActiveServiceLocked(HostNfcFService.DEACTIVATION_LINK_LOSS);
unbindServiceIfNeededLocked();
}
}
}
public void onHostEmulationActivated() {
if (DBG) Log.d(TAG, "notifyHostEmulationActivated");
}
public void onHostEmulationData(byte[] data) {
if (DBG) Log.d(TAG, "notifyHostEmulationData");
String nfcid2 = findNfcid2(data);
ComponentName resolvedServiceName = null;
synchronized (mLock) {
if (nfcid2 != null) {
NfcFServiceInfo resolvedService = mT3tIdentifiersCache.resolveNfcid2(nfcid2);
if (resolvedService != null) {
resolvedServiceName = resolvedService.getComponent();
}
}
if (resolvedServiceName == null) {
if (mActiveServiceName == null) {
return;
}
resolvedServiceName = mActiveServiceName;
}
// Check if resolvedService is actually currently enabled
if (mEnabledFgServiceName == null ||
!mEnabledFgServiceName.equals(resolvedServiceName)) {
return;
}
if (DBG) Log.d(TAG, "resolvedServiceName: " + resolvedServiceName.toString() +
"mState: " + String.valueOf(mState));
switch (mState) {
case STATE_IDLE:
Messenger existingService = bindServiceIfNeededLocked(resolvedServiceName);
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 packet to be used
mPendingPacket = data;
mState = STATE_W4_SERVICE;
}
NfcStatsLog.write(NfcStatsLog.NFC_CARDEMULATION_OCCURRED,
NfcStatsLog.NFC_CARDEMULATION_OCCURRED__CATEGORY__HCE_PAYMENT,
"HCEF");
break;
case STATE_W4_SERVICE:
Log.d(TAG, "Unexpected packet in STATE_W4_SERVICE");
break;
case STATE_XFER:
// Regular packet data
sendDataToServiceLocked(mActiveService, data);
break;
}
}
}
public void onHostEmulationDeactivated() {
if (DBG) Log.d(TAG, "notifyHostEmulationDeactivated");
synchronized (mLock) {
sendDeactivateToActiveServiceLocked(HostNfcFService.DEACTIVATION_LINK_LOSS);
mActiveService = null;
mActiveServiceName = null;
unbindServiceIfNeededLocked();
mState = STATE_IDLE;
}
}
public void onNfcDisabled() {
synchronized (mLock) {
sendDeactivateToActiveServiceLocked(HostNfcFService.DEACTIVATION_LINK_LOSS);
mEnabledFgServiceName = null;
mActiveService = null;
mActiveServiceName = null;
unbindServiceIfNeededLocked();
mState = STATE_IDLE;
}
}
public void onUserSwitched() {
synchronized (mLock) {
sendDeactivateToActiveServiceLocked(HostNfcFService.DEACTIVATION_LINK_LOSS);
mEnabledFgServiceName = null;
mActiveService = null;
mActiveServiceName = null;
unbindServiceIfNeededLocked();
mState = STATE_IDLE;
}
}
void sendDataToServiceLocked(Messenger service, byte[] data) {
if (DBG) Log.d(TAG, "sendDataToServiceLocked");
if (DBG) {
Log.d(TAG, "service: " +
(service != null ? service.toString() : "null"));
Log.d(TAG, "mActiveService: " +
(mActiveService != null ? mActiveService.toString() : "null"));
}
if (service != mActiveService) {
sendDeactivateToActiveServiceLocked(HostNfcFService.DEACTIVATION_LINK_LOSS);
mActiveService = service;
mActiveServiceName = mServiceName;
}
Message msg = Message.obtain(null, HostNfcFService.MSG_COMMAND_PACKET);
Bundle dataBundle = new Bundle();
dataBundle.putByteArray("data", data);
msg.setData(dataBundle);
msg.replyTo = mMessenger;
try {
Log.d(TAG, "Sending data to service");
if (DBG) Log.d(TAG, "data: " + getByteDump(data));
mActiveService.send(msg);
} catch (RemoteException e) {
Log.e(TAG, "Remote service has died, dropping packet");
}
}
void sendDeactivateToActiveServiceLocked(int reason) {
if (DBG) Log.d(TAG, "sendDeactivateToActiveServiceLocked");
if (mActiveService == null) return;
Message msg = Message.obtain(null, HostNfcFService.MSG_DEACTIVATED);
msg.arg1 = reason;
try {
mActiveService.send(msg);
} catch (RemoteException e) {
// Don't care
}
}
Messenger bindServiceIfNeededLocked(ComponentName service) {
if (DBG) Log.d(TAG, "bindServiceIfNeededLocked");
if (mServiceBound && mServiceName.equals(service)) {
Log.d(TAG, "Service already bound.");
return mService;
} else {
Log.d(TAG, "Binding to service " + service);
unbindServiceIfNeededLocked();
Intent bindIntent = new Intent(HostNfcFService.SERVICE_INTERFACE);
bindIntent.setComponent(service);
if (mContext.bindServiceAsUser(bindIntent, mConnection,
Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) {
} else {
Log.e(TAG, "Could not bind service.");
}
return null;
}
}
void unbindServiceIfNeededLocked() {
if (DBG) Log.d(TAG, "unbindServiceIfNeededLocked");
if (mServiceBound) {
Log.d(TAG, "Unbinding from service " + mServiceName);
mContext.unbindService(mConnection);
mServiceBound = false;
mService = null;
mServiceName = null;
}
}
String findNfcid2(byte[] data) {
if (DBG) Log.d(TAG, "findNfcid2");
if (data == null || data.length < MINIMUM_NFCF_PACKET_LENGTH) {
if (DBG) Log.d(TAG, "Data size too small");
return null;
}
int nfcid2Offset = 2;
return bytesToString(data, nfcid2Offset, NFCID2_LENGTH);
}
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mLock) {
mService = new Messenger(service);
mServiceBound = true;
mServiceName = name;
Log.d(TAG, "Service bound");
mState = STATE_XFER;
// Send pending packet
if (mPendingPacket != null) {
sendDataToServiceLocked(mService, mPendingPacket);
mPendingPacket = null;
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
synchronized (mLock) {
Log.d(TAG, "Service unbound");
mService = null;
mServiceBound = false;
mServiceName = null;
}
}
};
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 == HostNfcFService.MSG_RESPONSE_PACKET) {
Bundle dataBundle = msg.getData();
if (dataBundle == null) {
return;
}
byte[] data = dataBundle.getByteArray("data");
if (data == null) {
return;
}
if (data.length == 0) {
Log.e(TAG, "Invalid response packet");
return;
}
if (data.length != (data[0] & 0xff)) {
Log.e(TAG, "Invalid response packet");
return;
}
int state;
synchronized(mLock) {
state = mState;
}
if (state == STATE_XFER) {
Log.d(TAG, "Sending data");
if (DBG) Log.d(TAG, "data:" + getByteDump(data));
NfcService.getInstance().sendData(data);
} else {
Log.d(TAG, "Dropping data, wrong state " + Integer.toString(state));
}
}
}
}
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);
}
private String getByteDump(final byte[] cmd) {
StringBuffer str = new StringBuffer("");
int letters = 8;
int i = 0;
if (cmd == null) {
str.append(" null\n");
return str.toString();
}
for (; i < cmd.length; i++) {
str.append(String.format(" %02X", cmd[i]));
if ((i % letters == letters - 1) || (i + 1 == cmd.length)) {
str.append("\n");
}
}
return str.toString();
}
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("Bound HCE-F services: ");
if (mServiceBound) {
pw.println(" service: " + mServiceName);
}
}
/**
* Dump debugging information as a HostNfcFEmulationManagerProto
*
* Note:
* See proto definition in frameworks/base/core/proto/android/nfc/card_emulation.proto
* When writing a nested message, must call {@link ProtoOutputStream#start(long)} before and
* {@link ProtoOutputStream#end(long)} after.
* Never reuse a proto field number. When removing a field, mark it as reserved.
*/
void dumpDebug(ProtoOutputStream proto) {
if (mServiceBound) {
mServiceName.dumpDebug(proto, HostNfcFEmulationManagerProto.SERVICE_NAME);
}
}
}