/*
* Copyright (C) 2011 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;
import com.android.nfc.echoserver.EchoServer;
import com.android.nfc.handover.HandoverManager;
import com.android.nfc.ndefpush.NdefPushClient;
import com.android.nfc.ndefpush.NdefPushServer;
import com.android.nfc.snep.SnepClient;
import com.android.nfc.snep.SnepMessage;
import com.android.nfc.snep.SnepServer;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.nfc.INdefPushCallback;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.os.SystemClock;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Profile;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.Charsets;
import java.util.Arrays;
import java.util.List;
/**
* Interface to listen for P2P events.
* All callbacks are made from the UI thread.
*/
interface P2pEventListener {
/**
* Indicates a P2P device is in range.
*
onP2pInRange() and onP2pOutOfRange() will always be called
* alternately.
*
All other callbacks will only occur while a P2P device is in range.
*/
public void onP2pInRange();
/**
* Called when a NDEF payload is prepared to send, and confirmation is
* required. Call Callback.onP2pSendConfirmed() to make the confirmation.
*/
public void onP2pSendConfirmationRequested();
/**
* Called to indicate a send was successful.
*/
public void onP2pSendComplete();
/**
* Called to indicate a receive was successful.
*/
public void onP2pReceiveComplete();
/**
* Indicates the P2P device went out of range.
*/
public void onP2pOutOfRange();
public interface Callback {
public void onP2pSendConfirmed();
}
}
/**
* Manages sending and receiving NDEF message over LLCP link.
* Does simple debouncing of the LLCP link - so that even if the link
* drops and returns the user does not know.
*/
public class P2pLinkManager implements Handler.Callback, P2pEventListener.Callback {
static final String TAG = "NfcP2pLinkManager";
static final boolean DBG = true;
/** Include this constant as a meta-data entry in the manifest
* of an application to disable beaming the market/AAR link, like this:
*
{@code
*
*
*
* }
*/
static final String DISABLE_BEAM_DEFAULT = "android.nfc.disable_beam_default";
/** Enables the LLCP EchoServer, which can be used to test the android
* LLCP stack against nfcpy.
*/
static final boolean ECHOSERVER_ENABLED = false;
// TODO dynamically assign SAP values
static final int NDEFPUSH_SAP = 0x10;
static final int LINK_DEBOUNCE_MS = 750;
static final int MSG_DEBOUNCE_TIMEOUT = 1;
static final int MSG_RECEIVE_COMPLETE = 2;
static final int MSG_SEND_COMPLETE = 3;
static final int MSG_START_ECHOSERVER = 4;
static final int MSG_STOP_ECHOSERVER = 5;
// values for mLinkState
static final int LINK_STATE_DOWN = 1;
static final int LINK_STATE_UP = 2;
static final int LINK_STATE_DEBOUNCE =3;
// values for mSendState
static final int SEND_STATE_NOTHING_TO_SEND = 1;
static final int SEND_STATE_NEED_CONFIRMATION = 2;
static final int SEND_STATE_SENDING = 3;
static final Uri PROFILE_URI = Profile.CONTENT_VCARD_URI.buildUpon().
appendQueryParameter(Contacts.QUERY_PARAMETER_VCARD_NO_PHOTO, "true").
build();
final NdefPushServer mNdefPushServer;
final SnepServer mDefaultSnepServer;
final EchoServer mEchoServer;
final ActivityManager mActivityManager;
final PackageManager mPackageManager;
final Context mContext;
final P2pEventListener mEventListener;
final Handler mHandler;
final HandoverManager mHandoverManager;
// Locked on NdefP2pManager.this
int mLinkState;
int mSendState; // valid during LINK_STATE_UP or LINK_STATE_DEBOUNCE
boolean mIsSendEnabled;
boolean mIsReceiveEnabled;
NdefMessage mMessageToSend; // valid during SEND_STATE_NEED_CONFIRMATION or SEND_STATE_SENDING
Uri mUriToSend; // valid during SEND_STATE_NEED_CONFIRMATION or SEND_STATE_SENDING
String mMimeTypeToSend; // valid during SEND_STATE_NEED_CONFIRMATION or SEND_STATE_SENDING
INdefPushCallback mCallbackNdef;
SendTask mSendTask;
SharedPreferences mPrefs;
boolean mFirstBeam;
public P2pLinkManager(Context context, HandoverManager handoverManager) {
mNdefPushServer = new NdefPushServer(NDEFPUSH_SAP, mNppCallback);
mDefaultSnepServer = new SnepServer(mDefaultSnepCallback);
if (ECHOSERVER_ENABLED) {
mEchoServer = new EchoServer();
} else {
mEchoServer = null;
}
mActivityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
mPackageManager = context.getPackageManager();
mContext = context;
mEventListener = new P2pEventManager(context, this);
mHandler = new Handler(this);
mLinkState = LINK_STATE_DOWN;
mSendState = SEND_STATE_NOTHING_TO_SEND;
mIsSendEnabled = false;
mIsReceiveEnabled = false;
mPrefs = context.getSharedPreferences(NfcService.PREF, Context.MODE_PRIVATE);
mFirstBeam = mPrefs.getBoolean(NfcService.PREF_FIRST_BEAM, true);
mHandoverManager = handoverManager;
}
/**
* May be called from any thread.
* Assumes that NFC is already on if any parameter is true.
*/
public void enableDisable(boolean sendEnable, boolean receiveEnable) {
synchronized (this) {
if (!mIsReceiveEnabled && receiveEnable) {
mDefaultSnepServer.start();
mNdefPushServer.start();
if (mEchoServer != null) {
mHandler.sendEmptyMessage(MSG_START_ECHOSERVER);
}
} else if (mIsReceiveEnabled && !receiveEnable) {
mDefaultSnepServer.stop();
mNdefPushServer.stop();
if (mEchoServer != null) {
mHandler.sendEmptyMessage(MSG_STOP_ECHOSERVER);
}
}
mIsSendEnabled = sendEnable;
mIsReceiveEnabled = receiveEnable;
}
}
/**
* Set NDEF callback for sending.
* May be called from any thread.
* NDEF callbacks may be set at any time (even if NFC is
* currently off or P2P send is currently off). They will become
* active as soon as P2P send is enabled.
*/
public void setNdefCallback(INdefPushCallback callbackNdef) {
synchronized (this) {
mCallbackNdef = callbackNdef;
}
}
/**
* Must be called on UI Thread.
*/
public void onLlcpActivated() {
Log.i(TAG, "LLCP activated");
synchronized (P2pLinkManager.this) {
if (mEchoServer != null) {
mEchoServer.onLlcpActivated();
}
switch (mLinkState) {
case LINK_STATE_DOWN:
mLinkState = LINK_STATE_UP;
mSendState = SEND_STATE_NOTHING_TO_SEND;
if (DBG) Log.d(TAG, "onP2pInRange()");
mEventListener.onP2pInRange();
prepareMessageToSend();
if (mMessageToSend != null || mUriToSend != null) {
mSendState = SEND_STATE_NEED_CONFIRMATION;
if (DBG) Log.d(TAG, "onP2pSendConfirmationRequested()");
mEventListener.onP2pSendConfirmationRequested();
}
break;
case LINK_STATE_UP:
if (DBG) Log.d(TAG, "Duplicate onLlcpActivated()");
return;
case LINK_STATE_DEBOUNCE:
mLinkState = LINK_STATE_UP;
mHandler.removeMessages(MSG_DEBOUNCE_TIMEOUT);
if (mSendState == SEND_STATE_SENDING) {
Log.i(TAG, "Retry send...");
sendNdefMessage();
}
break;
}
}
}
void prepareMessageToSend() {
synchronized (P2pLinkManager.this) {
if (!mIsSendEnabled) {
mMessageToSend = null;
mUriToSend = null;
mMimeTypeToSend = null;
return;
}
// Try application callback first
//TODO: Check that mCallbackNdef refers to the foreground activity
if (mCallbackNdef != null) {
try {
mMessageToSend = mCallbackNdef.createMessage();
mUriToSend = mCallbackNdef.getUri();
mMimeTypeToSend = mCallbackNdef.getMimeType();
return;
} catch (RemoteException e) {
// Ignore
}
}
// fall back to default NDEF for this activity, unless the
// application disabled this explicitly in their manifest.
List tasks = mActivityManager.getRunningTasks(1);
if (tasks.size() > 0) {
String pkg = tasks.get(0).baseActivity.getPackageName();
if (beamDefaultDisabled(pkg)) {
Log.d(TAG, "Disabling default Beam behavior");
mMessageToSend = null;
} else {
mMessageToSend = createDefaultNdef(pkg);
}
} else {
mMessageToSend = null;
}
if (DBG) Log.d(TAG, "mMessageToSend = " + mMessageToSend);
if (DBG) Log.d(TAG, "mUriToSend = " + mUriToSend);
if (DBG) Log.d(TAG, "mMimeTypeToSend = " + mMimeTypeToSend);
}
}
boolean beamDefaultDisabled(String pkgName) {
try {
ApplicationInfo ai = mPackageManager.getApplicationInfo(pkgName,
PackageManager.GET_META_DATA);
if (ai == null || ai.metaData == null) {
return false;
}
return ai.metaData.getBoolean(DISABLE_BEAM_DEFAULT);
} catch (NameNotFoundException e) {
return false;
}
}
NdefMessage createDefaultNdef(String pkgName) {
NdefRecord appUri = NdefRecord.createUri(Uri.parse(
"http://play.google.com/store/apps/details?id=" + pkgName + "&feature=beam"));
NdefRecord appRecord = NdefRecord.createApplicationRecord(pkgName);
return new NdefMessage(new NdefRecord[] { appUri, appRecord });
}
/**
* Must be called on UI Thread.
*/
public void onLlcpDeactivated() {
Log.i(TAG, "LLCP deactivated.");
synchronized (this) {
if (mEchoServer != null) {
mEchoServer.onLlcpDeactivated();
}
switch (mLinkState) {
case LINK_STATE_DOWN:
case LINK_STATE_DEBOUNCE:
Log.i(TAG, "Duplicate onLlcpDectivated()");
break;
case LINK_STATE_UP:
// Debounce
mLinkState = LINK_STATE_DEBOUNCE;
mHandler.sendEmptyMessageDelayed(MSG_DEBOUNCE_TIMEOUT, LINK_DEBOUNCE_MS);
cancelSendNdefMessage();
break;
}
}
}
void onSendComplete(NdefMessage msg, long elapsedRealtime) {
if (mFirstBeam) {
EventLogTags.writeNfcFirstShare();
mPrefs.edit().putBoolean(NfcService.PREF_FIRST_BEAM, false).apply();
mFirstBeam = false;
}
EventLogTags.writeNfcShare(getMessageSize(msg), getMessageTnf(msg), getMessageType(msg),
getMessageAarPresent(msg), (int) elapsedRealtime);
// Make callbacks on UI thread
mHandler.sendEmptyMessage(MSG_SEND_COMPLETE);
}
void sendNdefMessage() {
synchronized (this) {
cancelSendNdefMessage();
mSendTask = new SendTask();
mSendTask.execute();
}
}
void cancelSendNdefMessage() {
synchronized (P2pLinkManager.this) {
if (mSendTask != null) {
mSendTask.cancel(true);
}
}
}
final class SendTask extends AsyncTask {
@Override
public Void doInBackground(Void... args) {
NdefMessage m;
Uri uri;
String mimeType;
boolean result;
synchronized (P2pLinkManager.this) {
if (mLinkState != LINK_STATE_UP || mSendState != SEND_STATE_SENDING) {
return null;
}
m = mMessageToSend;
uri = mUriToSend;
mimeType = mMimeTypeToSend;
}
long time = SystemClock.elapsedRealtime();
try {
if (DBG) Log.d(TAG, "Sending ndef via SNEP");
result = doSnepProtocol(mHandoverManager, m, mimeType, uri);
} catch (IOException e) {
Log.i(TAG, "Failed to connect over SNEP, trying NPP");
if (isCancelled()) {
return null;
}
result = new NdefPushClient().push(m);
}
time = SystemClock.elapsedRealtime() - time;
if (DBG) Log.d(TAG, "SendTask result=" + result + ", time ms=" + time);
if (result) {
onSendComplete(m, time);
}
return null;
}
}
static boolean doSnepProtocol(HandoverManager handoverManager,
NdefMessage msg, String mimeType, Uri uri) throws IOException {
SnepClient snepClient = new SnepClient();
try {
snepClient.connect();
} catch (IOException e) {
// Throw exception to fall back to NPP.
snepClient.close();
throw new IOException("SNEP not available.", e);
}
try {
if (uri != null) {
SnepMessage snepResponse =
snepClient.get(handoverManager.createHandoverRequestMessage());
NdefMessage response = snepResponse.getNdefMessage();
if (response != null) {
handoverManager.doHandoverUri(mimeType, uri, response);
}
} else if (msg != null) {
snepClient.put(msg);
}
return true;
} catch (IOException e) {
// SNEP available but had errors, don't fall back to NPP.
} finally {
snepClient.close();
}
return false;
}
final NdefPushServer.Callback mNppCallback = new NdefPushServer.Callback() {
@Override
public void onMessageReceived(NdefMessage msg) {
onReceiveComplete(msg);
}
};
final SnepServer.Callback mDefaultSnepCallback = new SnepServer.Callback() {
@Override
public SnepMessage doPut(NdefMessage msg) {
onReceiveComplete(msg);
return SnepMessage.getMessage(SnepMessage.RESPONSE_SUCCESS);
}
@Override
public SnepMessage doGet(int acceptableLength, NdefMessage msg) {
NdefMessage response = mHandoverManager.tryHandoverRequest(msg);
if (response != null) {
return SnepMessage.getSuccessResponse(response);
} else {
return SnepMessage.getMessage(SnepMessage.RESPONSE_NOT_FOUND);
}
}
};
void onReceiveComplete(NdefMessage msg) {
EventLogTags.writeNfcNdefReceived(getMessageSize(msg), getMessageTnf(msg),
getMessageType(msg), getMessageAarPresent(msg));
// Make callbacks on UI thread
mHandler.obtainMessage(MSG_RECEIVE_COMPLETE, msg).sendToTarget();
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_START_ECHOSERVER:
synchronized (this) {
mEchoServer.start();
break;
}
case MSG_STOP_ECHOSERVER:
synchronized (this) {
mEchoServer.stop();
break;
}
case MSG_DEBOUNCE_TIMEOUT:
synchronized (this) {
if (mLinkState != LINK_STATE_DEBOUNCE) {
break;
}
if (mSendState == SEND_STATE_SENDING) {
EventLogTags.writeNfcShareFail(getMessageSize(mMessageToSend),
getMessageTnf(mMessageToSend), getMessageType(mMessageToSend),
getMessageAarPresent(mMessageToSend));
}
if (DBG) Log.d(TAG, "Debounce timeout");
mLinkState = LINK_STATE_DOWN;
mSendState = SEND_STATE_NOTHING_TO_SEND;
mMessageToSend = null;
mUriToSend = null;
mMimeTypeToSend = null;
if (DBG) Log.d(TAG, "onP2pOutOfRange()");
mEventListener.onP2pOutOfRange();
}
break;
case MSG_RECEIVE_COMPLETE:
NdefMessage m = (NdefMessage) msg.obj;
synchronized (this) {
if (mLinkState == LINK_STATE_DOWN) {
break;
}
if (mSendState == SEND_STATE_SENDING) {
cancelSendNdefMessage();
}
mSendState = SEND_STATE_NOTHING_TO_SEND;
if (DBG) Log.d(TAG, "onP2pReceiveComplete()");
mEventListener.onP2pReceiveComplete();
NfcService.getInstance().sendMockNdefTag(m);
}
break;
case MSG_SEND_COMPLETE:
synchronized (P2pLinkManager.this) {
mSendTask = null;
if (mLinkState == LINK_STATE_DOWN || mSendState != SEND_STATE_SENDING) {
break;
}
mSendState = SEND_STATE_NOTHING_TO_SEND;
if (DBG) Log.d(TAG, "onP2pSendComplete()");
mEventListener.onP2pSendComplete();
if (mCallbackNdef != null) {
try {
mCallbackNdef.onNdefPushComplete();
} catch (RemoteException e) { }
}
mSendTask = null;
}
break;
}
return true;
}
int getMessageSize(NdefMessage msg) {
if (msg != null) {
return msg.toByteArray().length;
} else {
return 0;
}
}
int getMessageTnf(NdefMessage msg) {
if (msg == null) {
return NdefRecord.TNF_EMPTY;
}
NdefRecord records[] = msg.getRecords();
if (records == null || records.length == 0) {
return NdefRecord.TNF_EMPTY;
}
return records[0].getTnf();
}
String getMessageType(NdefMessage msg) {
if (msg == null) {
return "null";
}
NdefRecord records[] = msg.getRecords();
if (records == null || records.length == 0) {
return "null";
}
NdefRecord record = records[0];
switch (record.getTnf()) {
case NdefRecord.TNF_ABSOLUTE_URI:
// The actual URI is in the type field, don't log it
return "uri";
case NdefRecord.TNF_EXTERNAL_TYPE:
case NdefRecord.TNF_MIME_MEDIA:
case NdefRecord.TNF_WELL_KNOWN:
return new String(record.getType(), Charsets.UTF_8);
default:
return "unknown";
}
}
int getMessageAarPresent(NdefMessage msg) {
if (msg == null) {
return 0;
}
NdefRecord records[] = msg.getRecords();
if (records == null) {
return 0;
}
for (NdefRecord record : records) {
if (record.getTnf() == NdefRecord.TNF_EXTERNAL_TYPE &&
Arrays.equals(NdefRecord.RTD_ANDROID_APP, record.getType())) {
return 1;
}
}
return 0;
}
@Override
public void onP2pSendConfirmed() {
if (DBG) Log.d(TAG, "onP2pSendConfirmed()");
synchronized (this) {
if (mLinkState == LINK_STATE_DOWN || mSendState != SEND_STATE_NEED_CONFIRMATION) {
return;
}
mSendState = SEND_STATE_SENDING;
if (mLinkState == LINK_STATE_UP) {
sendNdefMessage();
}
}
}
static String sendStateToString(int state) {
switch (state) {
case SEND_STATE_NOTHING_TO_SEND:
return "SEND_STATE_NOTHING_TO_SEND";
case SEND_STATE_NEED_CONFIRMATION:
return "SEND_STATE_NEED_CONFIRMATION";
case SEND_STATE_SENDING:
return "SEND_STATE_SENDING";
default:
return "";
}
}
static String linkStateToString(int state) {
switch (state) {
case LINK_STATE_DOWN:
return "LINK_STATE_DOWN";
case LINK_STATE_DEBOUNCE:
return "LINK_STATE_DEBOUNCE";
case LINK_STATE_UP:
return "LINK_STATE_UP";
default:
return "";
}
}
void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
synchronized (this) {
pw.println("mIsSendEnabled=" + mIsSendEnabled);
pw.println("mIsReceiveEnabled=" + mIsReceiveEnabled);
pw.println("mLinkState=" + linkStateToString(mLinkState));
pw.println("mSendState=" + sendStateToString(mSendState));
pw.println("mCallbackNdef=" + mCallbackNdef);
pw.println("mMessageToSend=" + mMessageToSend);
pw.println("mUriToSend=" + mUriToSend);
pw.println("mMimeTypeToSend=" + mMimeTypeToSend);
}
}
}