blob: 0d442d17712ad66b0d9dad258efea7f619ca9dd6 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.device.nfc;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.nfc.FormatException;
import android.nfc.NfcAdapter;
import android.nfc.NfcAdapter.ReaderCallback;
import android.nfc.NfcManager;
import android.nfc.Tag;
import android.nfc.TagLostException;
import android.os.Process;
import android.os.Vibrator;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.device.mojom.NdefError;
import org.chromium.device.mojom.NdefErrorType;
import org.chromium.device.mojom.NdefMessage;
import org.chromium.device.mojom.NdefRecord;
import org.chromium.device.mojom.NdefWriteOptions;
import org.chromium.device.mojom.Nfc;
import org.chromium.device.mojom.NfcClient;
import org.chromium.mojo.bindings.Callbacks;
import org.chromium.mojo.bindings.InterfaceRequest;
import org.chromium.mojo.bindings.Router;
import org.chromium.mojo.system.MojoException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
/** Android implementation of the NFC mojo service defined in device/nfc/nfc.mojom.
*/
public class NfcImpl implements Nfc {
private static final String TAG = "NfcImpl";
private static final String ANY_PATH = "/*";
private final int mHostId;
private final NfcDelegate mDelegate;
private Router mRouter;
/**
* Used to get instance of NFC adapter, @see android.nfc.NfcManager
*/
private final NfcManager mNfcManager;
/**
* NFC adapter. @see android.nfc.NfcAdapter
*/
private final NfcAdapter mNfcAdapter;
/**
* Activity that is in foreground and is used to enable / disable NFC reader mode operations.
* Can be updated when activity associated with web page is changed. @see #setActivity
*/
private Activity mActivity;
/**
* Flag that indicates whether NFC permission is granted.
*/
private final boolean mHasPermission;
/**
* Flag that indicates whether NFC operations are suspended. Is updated when
* suspendNfcOperations() and resumeNfcOperations() are called.
*/
private boolean mOperationsSuspended;
/**
* Implementation of android.nfc.NfcAdapter.ReaderCallback. @see ReaderCallbackHandler
*/
private ReaderCallbackHandler mReaderCallbackHandler;
/**
* Object that contains data that was passed to method
* #push(NdefMessage message, NdefWriteOptions options, PushResponse callback)
* @see PendingPushOperation
*/
private PendingPushOperation mPendingPushOperation;
/**
* Utility that provides I/O operations for a Tag. Created on demand when Tag is found.
* @see NfcTagHandler
*/
private NfcTagHandler mTagHandler;
/**
* Client interface used to deliver NdefMessages for registered watch operations.
* @see #watch
*/
private NfcClient mClient;
private final List<Integer> mWatchIds = new ArrayList<Integer>();
/**
* Vibrator. @see android.os.Vibrator
*/
private Vibrator mVibrator;
public NfcImpl(int hostId, NfcDelegate delegate, InterfaceRequest<Nfc> request) {
mHostId = hostId;
mDelegate = delegate;
mOperationsSuspended = false;
// |request| may be null in tests.
if (request != null) {
mRouter = Nfc.MANAGER.bind(this, request);
}
int permission = ContextUtils.getApplicationContext().checkPermission(
Manifest.permission.NFC, Process.myPid(), Process.myUid());
mHasPermission = permission == PackageManager.PERMISSION_GRANTED;
Callback<Activity> onActivityUpdatedCallback = new Callback<Activity>() {
@Override
public void onResult(Activity activity) {
setActivity(activity);
}
};
mDelegate.trackActivityForHost(mHostId, onActivityUpdatedCallback);
if (!mHasPermission) {
Log.w(TAG, "NFC operations are not permitted.");
mNfcAdapter = null;
mNfcManager = null;
} else {
mNfcManager = (NfcManager) ContextUtils.getApplicationContext().getSystemService(
Context.NFC_SERVICE);
if (mNfcManager == null) {
Log.w(TAG, "NFC is not supported.");
mNfcAdapter = null;
} else {
mNfcAdapter = mNfcManager.getDefaultAdapter();
}
}
mVibrator = (Vibrator) ContextUtils.getApplicationContext().getSystemService(
Context.VIBRATOR_SERVICE);
}
/**
* Sets Activity that is used to enable / disable NFC reader mode. When Activity is set,
* reader mode is disabled for old Activity and enabled for the new Activity.
*/
protected void setActivity(Activity activity) {
disableReaderMode();
mActivity = activity;
enableReaderModeIfNeeded();
}
/**
* Forces the Mojo connection to this object to be closed. This will trigger a call to close()
* so that pending NFC operations are canceled.
*/
public void closeMojoConnection() {
if (mRouter != null) {
mRouter.close();
mRouter = null;
}
}
/**
* Sets NfcClient. NfcClient interface is used to notify mojo NFC service client when NFC
* device is in proximity and has NdefMessage.
* @see Nfc#watch(int id, WatchResponse callback)
*
* @param client @see NfcClient
*/
@Override
public void setClient(NfcClient client) {
mClient = client;
}
/**
* Pushes NdefMessage to NFC Tag whenever it is in proximity.
*
* @param message that should be pushed to NFC device.
* @param options that contain options for the pending push operation.
* @param callback that is used to notify when push operation is completed.
*/
@Override
public void push(NdefMessage message, NdefWriteOptions options, PushResponse callback) {
if (!checkIfReady(callback)) return;
if (mOperationsSuspended) {
callback.call(createError(NdefErrorType.OPERATION_CANCELLED,
"Cannot push the message because NFC operations are suspended."));
}
if (!NdefMessageValidator.isValid(message)) {
callback.call(createError(NdefErrorType.INVALID_MESSAGE,
"Cannot push the message because it's invalid."));
return;
}
// If previous pending push operation is not completed, cancel it.
if (mPendingPushOperation != null) {
mPendingPushOperation.complete(createError(NdefErrorType.OPERATION_CANCELLED,
"Push is cancelled due to a new push request."));
}
mPendingPushOperation = new PendingPushOperation(message, options, callback);
enableReaderModeIfNeeded();
processPendingPushOperation();
}
/**
* Cancels pending push operation.
*/
@Override
public void cancelPush() {
completePendingPushOperation(createError(
NdefErrorType.OPERATION_CANCELLED, "The push operation is already cancelled."));
}
/**
* When NdefMessages that are found when NFC device is within proximity, it
* is passed to NfcClient interface together with corresponding watch ID.
* @see NfcClient#onWatch(int[] id, String serial_number, NdefMessage message)
*
* @param id request ID from Blink which will be the watch ID if succeeded.
* @param callback that is used to notify caller when watch() is completed.
*/
@Override
public void watch(int id, WatchResponse callback) {
if (!checkIfReady(callback)) return;
// We received a duplicate |id| here that should never happen, in such a case we should
// report a bad message to Mojo but unfortunately Mojo bindings for Java does not support
// this feature yet. So, we just passes back a generic error instead.
if (mWatchIds.contains(id)) {
callback.call(createError(NdefErrorType.NOT_READABLE,
"Cannot start because the received scan request is duplicate."));
return;
}
mWatchIds.add(id);
callback.call(null);
enableReaderModeIfNeeded();
processPendingWatchOperations();
}
/**
* Cancels NFC watch operation.
*
* @param id of watch operation.
*/
@Override
public void cancelWatch(int id) {
if (mWatchIds.contains(id)) {
mWatchIds.remove(mWatchIds.indexOf(id));
disableReaderModeIfNeeded();
}
}
@Override
public void close() {
mDelegate.stopTrackingActivityForHost(mHostId);
disableReaderMode();
}
@Override
public void onConnectionError(MojoException e) {
// We do nothing here since close() is always called no matter the connection gets closed
// normally or abnormally.
}
/**
* Suspends all pending watch operations and cancel push operations.
*/
public void suspendNfcOperations() {
mOperationsSuspended = true;
disableReaderMode();
cancelPush();
}
/**
* Resumes all pending watch / push operations.
*/
public void resumeNfcOperations() {
mOperationsSuspended = false;
enableReaderModeIfNeeded();
}
/**
* Holds information about pending push operation.
*/
private static class PendingPushOperation {
public final NdefMessage ndefMessage;
public final NdefWriteOptions ndefWriteOptions;
private final PushResponse mPushResponseCallback;
public PendingPushOperation(
NdefMessage message, NdefWriteOptions options, PushResponse callback) {
ndefMessage = message;
ndefWriteOptions = options;
mPushResponseCallback = callback;
}
/**
* Completes pending push operation.
*
* @param error should be null when operation is completed successfully, otherwise,
* error object with corresponding NdefErrorType should be provided.
*/
public void complete(NdefError error) {
if (mPushResponseCallback != null) mPushResponseCallback.call(error);
}
}
/**
* Helper method that creates NdefError object from NdefErrorType.
*/
private NdefError createError(int errorType, String errorMessage) {
// Guaranteed by callers.
assert errorMessage != null;
NdefError error = new NdefError();
error.errorType = errorType;
error.errorMessage = errorMessage;
return error;
}
/**
* Checks if NFC functionality can be used by the mojo service. If permission to use NFC is
* granted and hardware is enabled, returns null.
*/
private NdefError checkIfReady() {
if (!mHasPermission || mActivity == null) {
return createError(NdefErrorType.NOT_ALLOWED, "The operation is not allowed.");
} else if (mNfcManager == null || mNfcAdapter == null) {
return createError(NdefErrorType.NOT_SUPPORTED, "NFC is not supported.");
} else if (!mNfcAdapter.isEnabled()) {
return createError(NdefErrorType.NOT_READABLE, "NFC setting is disabled.");
}
return null;
}
/**
* Uses checkIfReady() method and if NFC cannot be used, calls mojo callback with NdefError.
*
* @param callback Generic callback that is provided to watch() and push() methods.
* @return boolean true if NFC functionality can be used, false otherwise.
*/
private boolean checkIfReady(Callbacks.Callback1<NdefError> callback) {
NdefError error = checkIfReady();
if (error == null) return true;
callback.call(error);
return false;
}
/**
* Implementation of android.nfc.NfcAdapter.ReaderCallback. Callback is called when NFC tag is
* discovered, Tag object is delegated to mojo service implementation method
* NfcImpl.onTagDiscovered().
*/
private static class ReaderCallbackHandler implements ReaderCallback {
private final NfcImpl mNfcImpl;
public ReaderCallbackHandler(NfcImpl impl) {
mNfcImpl = impl;
}
@Override
public void onTagDiscovered(Tag tag) {
mNfcImpl.onTagDiscovered(tag);
}
}
/**
* Enables reader mode, allowing NFC device to read / write NFC tags.
* @see android.nfc.NfcAdapter#enableReaderMode
*/
private void enableReaderModeIfNeeded() {
if (mReaderCallbackHandler != null || mActivity == null || mNfcAdapter == null) return;
// Do not enable reader mode, if there are no active push / watch operations.
if (mPendingPushOperation == null && mWatchIds.size() == 0) return;
mReaderCallbackHandler = new ReaderCallbackHandler(this);
mNfcAdapter.enableReaderMode(mActivity, mReaderCallbackHandler,
NfcAdapter.FLAG_READER_NFC_A | NfcAdapter.FLAG_READER_NFC_B
| NfcAdapter.FLAG_READER_NFC_F | NfcAdapter.FLAG_READER_NFC_V
| NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS,
null);
}
/**
* Disables reader mode.
* @see android.nfc.NfcAdapter#disableReaderMode
*/
private void disableReaderMode() {
// There is no API that could query whether reader mode is enabled for adapter.
// If mReaderCallbackHandler is null, reader mode is not enabled.
if (mReaderCallbackHandler == null) return;
mReaderCallbackHandler = null;
if (mActivity != null && mNfcAdapter != null && !mActivity.isDestroyed()) {
mNfcAdapter.disableReaderMode(mActivity);
}
}
/**
* Checks if there are pending push / watch operations and disables readre mode
* whenever necessary.
*/
private void disableReaderModeIfNeeded() {
if (mPendingPushOperation == null && mWatchIds.size() == 0) {
disableReaderMode();
}
}
/**
* Handles completion of pending push operation, completes push operation.
* On error, invalidates #mTagHandler.
*/
private void pendingPushOperationCompleted(NdefError error) {
completePendingPushOperation(error);
if (error != null) mTagHandler = null;
}
/**
* Completes pending push operation and disables reader mode if needed.
*/
private void completePendingPushOperation(NdefError error) {
if (mPendingPushOperation == null) return;
mPendingPushOperation.complete(error);
mPendingPushOperation = null;
disableReaderModeIfNeeded();
}
/**
* Checks whether there is a #mPendingPushOperation and writes data to NFC tag. In case of
* exception calls pendingPushOperationCompleted() with appropriate error object.
*/
private void processPendingPushOperation() {
if (mTagHandler == null || mPendingPushOperation == null) return;
if (mTagHandler.isTagOutOfRange()) {
mTagHandler = null;
return;
}
try {
mTagHandler.connect();
if (!mPendingPushOperation.ndefWriteOptions.overwrite
&& !mTagHandler.canAlwaysOverwrite()) {
Log.w(TAG, "Cannot overwrite the NFC tag due to existing data on it.");
pendingPushOperationCompleted(createError(NdefErrorType.NOT_ALLOWED,
"NDEFWriteOptions#overwrite does not allow overwrite."));
return;
}
mTagHandler.write(NdefMessageUtils.toNdefMessage(mPendingPushOperation.ndefMessage));
pendingPushOperationCompleted(null);
} catch (InvalidNdefMessageException e) {
Log.w(TAG, "Cannot write data to NFC tag. Invalid NdefMessage.");
pendingPushOperationCompleted(createError(NdefErrorType.INVALID_MESSAGE,
"Cannot push the message because it's invalid."));
} catch (TagLostException e) {
Log.w(TAG, "Cannot write data to NFC tag. Tag is lost: " + e.getMessage());
pendingPushOperationCompleted(createError(NdefErrorType.IO_ERROR,
"Failed to write because the tag is lost: " + e.getMessage()));
} catch (FormatException | IllegalStateException | IOException e) {
Log.w(TAG, "Cannot write data to NFC tag: " + e.getMessage());
pendingPushOperationCompleted(createError(NdefErrorType.IO_ERROR,
"Failed to write due to an IO error: " + e.getMessage()));
}
}
/**
* Reads NdefMessage from a tag and forwards message to matching method.
*/
private void processPendingWatchOperations() {
if (mTagHandler == null || mClient == null || mWatchIds.size() == 0
|| mOperationsSuspended) {
return;
}
if (mTagHandler.isTagOutOfRange()) {
mTagHandler = null;
return;
}
try {
mTagHandler.connect();
android.nfc.NdefMessage message = mTagHandler.read();
if (message == null) {
// Tag is formatted to support NDEF but does not contain a message yet.
// Let's create one with no records so that watchers can be notified.
NdefMessage webNdefMessage = new NdefMessage();
webNdefMessage.data = new NdefRecord[0];
notifyWatchers(webNdefMessage);
return;
}
NdefMessage webNdefMessage = NdefMessageUtils.toNdefMessage(message);
notifyWatchers(webNdefMessage);
} catch (UnsupportedEncodingException e) {
Log.w(TAG,
"Cannot read data from NFC tag. Cannot convert to NdefMessage:"
+ e.getMessage());
notifyErrorToAllWatchers(createError(NdefErrorType.INVALID_MESSAGE,
"Failed to decode the NdefMessage read from the tag: " + e.getMessage()));
} catch (TagLostException e) {
Log.w(TAG, "Cannot read data from NFC tag. Tag is lost: " + e.getMessage());
notifyErrorToAllWatchers(createError(NdefErrorType.IO_ERROR,
"Failed to read because the tag is lost: " + e.getMessage()));
} catch (FormatException | IllegalStateException | IOException e) {
Log.w(TAG, "Cannot read data from NFC tag. IO_ERROR: " + e.getMessage());
notifyErrorToAllWatchers(createError(NdefErrorType.IO_ERROR,
"Failed to read due to an IO error: " + e.getMessage()));
}
}
/**
* Notify all active watchers that an error happened when trying to read the tag coming nearby.
*/
private void notifyErrorToAllWatchers(NdefError error) {
if (mWatchIds.size() != 0) mClient.onError(error);
}
/**
* Iterates through active watchers and delivers NdefMessage to the client.
*/
private void notifyWatchers(NdefMessage message) {
if (mWatchIds.size() != 0) {
int[] ids = new int[mWatchIds.size()];
for (int i = 0; i < mWatchIds.size(); ++i) {
ids[i] = mWatchIds.get(i).intValue();
}
mClient.onWatch(ids, mTagHandler.serialNumber(), message);
}
}
/**
* Called by ReaderCallbackHandler when NFC tag is in proximity.
*/
public void onTagDiscovered(Tag tag) {
mVibrator.vibrate(200);
processPendingOperations(NfcTagHandler.create(tag));
}
/**
* Processes pending operation when NFC tag is in proximity.
*/
protected void processPendingOperations(NfcTagHandler tagHandler) {
mTagHandler = tagHandler;
// This tag is not supported.
if (mTagHandler == null) {
Log.w(TAG, "This tag is not supported.");
notifyErrorToAllWatchers(
createError(NdefErrorType.NOT_SUPPORTED, "This tag is not supported."));
pendingPushOperationCompleted(
createError(NdefErrorType.NOT_SUPPORTED, "This tag is not supported."));
return;
}
processPendingWatchOperations();
processPendingPushOperation();
if (mTagHandler != null && mTagHandler.isConnected()) {
try {
mTagHandler.close();
} catch (IOException e) {
Log.w(TAG, "Cannot close NFC tag connection.");
}
}
}
}