Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2475)

Unified Diff: device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java

Issue 1938393003: [webnfc] WIP: Implement push method for Android nfc mojo service. Base URL: https://chromium.googlesource.com/chromium/src.git@implement_nfc_push_in_blink
Patch Set: Created 4 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « device/nfc/android/BUILD.gn ('k') | device/nfc/nfc.gyp » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java
diff --git a/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java b/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..8ca718d50b2849e968f69f146ca71648c9069060
--- /dev/null
+++ b/device/nfc/android/java/src/org/chromium/device/nfc/NfcImpl.java
@@ -0,0 +1,653 @@
+// 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.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentFilter.MalformedMimeTypeException;
+import android.content.pm.PackageManager;
+import android.nfc.FormatException;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcManager;
+import android.nfc.Tag;
+import android.nfc.TagLostException;
+import android.nfc.tech.Ndef;
+import android.nfc.tech.NdefFormatable;
+import android.nfc.tech.TagTechnology;
+import android.os.Build;
+import android.os.Process;
+import android.support.annotation.Nullable;
+import android.support.v4.content.LocalBroadcastManager;
+
+import org.chromium.base.Log;
+import org.chromium.mojo.bindings.Callbacks;
+import org.chromium.mojo.system.MojoException;
+import org.chromium.mojom.device.Nfc;
+import org.chromium.mojom.device.NfcError;
+import org.chromium.mojom.device.NfcErrorType;
+import org.chromium.mojom.device.NfcMessage;
+import org.chromium.mojom.device.NfcPushOptions;
+import org.chromium.mojom.device.NfcPushTarget;
+import org.chromium.mojom.device.NfcRecord;
+import org.chromium.mojom.device.NfcRecordType;
+import org.chromium.mojom.device.NfcWatchOptions;
+
+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 extends BroadcastReceiver implements Nfc {
+ private static final String TAG = "NfcImpl";
+ private static final String DOMAIN = "w3.org";
+ private static final String TYPE = "webnfc";
+ private static final String TEXT_MIME = "text/plain";
+ private static final String JSON_MIME = "application/json";
+ private static final String OPAQUE_MIME = "application/octet-stream";
+ private static final String CHARSET_UTF8 = ";charset=UTF-8";
+ private static final String CHARSET_UTF16 = ";charset=UTF-16";
+
+ /**
+ * 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 object that is requred to enable / disable NFC reader mode operations.
+ */
+ private final Activity mActivity;
+
+ /**
+ * Flag that indicates whether NFC permission is granted.
+ */
+ private final boolean mHasPermission;
+
+ /**
+ * Object that contains data that was passed to method
+ * #push(NfcMessage message, NfcPushOptions 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 NfcTagWriter
+ */
+ private NfcTagWriter mTagWriter;
+
+ /**
+ * Pending intent that is used for foreground dispatch.
+ * @see android.nfc.NfcAdapter.enableForegroundDispatch
+ */
+ private PendingIntent mPendingIntent;
+
+ public NfcImpl(Activity activity) {
+ mActivity = activity;
+
+ if (mActivity != null) {
+ int permission = mActivity.checkPermission(
+ Manifest.permission.NFC, Process.myPid(), Process.myUid());
+ mHasPermission = permission == PackageManager.PERMISSION_GRANTED;
+ } else {
+ mHasPermission = false;
+ }
+
+ if (mHasPermission) {
+ mNfcManager = (NfcManager) mActivity.getSystemService(Context.NFC_SERVICE);
+ if (mNfcManager != null) {
+ mNfcAdapter = mNfcManager.getDefaultAdapter();
+ } else {
+ Log.w(TAG, "NFC is not supported.");
+ mNfcAdapter = null;
+ }
+ } else {
+ Log.w(TAG, "NFC operations are not permitted.");
+ mNfcAdapter = null;
+ mNfcManager = null;
+ }
+ }
+
+ /**
+ * Pushes NfcMessage to Tag or Peer, whenever NFC device is in proximity.
+ * At the moment, only passive NFC devices are supported (NfcPushTarget.TAG).
+ *
+ * @param message that should be pushed to NFC device.
+ * @param options that contain information about timeout and target device type.
+ * @param callback that is used to notify when push operation is completed.
+ */
+ @Override
+ public void push(NfcMessage message, NfcPushOptions options, PushResponse callback) {
+ if (!checkIfReady(callback)) return;
+
+ if (options.target == NfcPushTarget.PEER) {
+ callback.call(createError(NfcErrorType.NOT_SUPPORTED));
+ return;
+ }
+
+ // If previous pending push operation is not completed, subsequent call
+ // should cancel pending operation.
+ if (mPendingPushOperation != null) {
+ mPendingPushOperation.complete(createError(NfcErrorType.OPERATION_CANCELLED));
+ }
+
+ mPendingPushOperation = new PendingPushOperation(message, options, callback);
+ enableForegroundDispatch();
+ processPendingPushOperation();
+ }
+
+ /**
+ * Cancels pending push operation.
+ * At the moment, only passive NFC devices are supported (NfcPushTarget.TAG).
+ *
+ * @param target @see NfcPushTarget
+ * @param callback that is used to notify caller when cancelPush() is completed.
+ */
+ @Override
+ public void cancelPush(int target, CancelPushResponse callback) {
+ if (!checkIfReady(callback)) return;
+
+ if (target == NfcPushTarget.PEER) {
+ callback.call(createError(NfcErrorType.NOT_SUPPORTED));
+ return;
+ }
+
+ if (mPendingPushOperation != null) {
+ mPendingPushOperation.complete(createError(NfcErrorType.OPERATION_CANCELLED));
+ mPendingPushOperation = null;
+ callback.call(null);
+ disableForegroundDispatch();
+ } else {
+ callback.call(createError(NfcErrorType.NOT_FOUND));
+ }
+ }
+
+ /**
+ * Watch method allows to set filtering criteria for NfcMessages that are
+ * found when NFC device is within proximity. On success, watch ID is
+ * returned to caller through WatchResponse callback. When NfcMessage that
+ * matches NfcWatchOptions is found, it is returned through
+ * SubscribeToWatchEventsResponse callback interface.
+ * @see SubscribeToWatchEventsResponse
+ *
+ * @param options used to filter NfcMessages, @see NfcWatchOptions.
+ * @param callback that is used to notify caller when watch() is completed and return watch ID.
+ */
+ @Override
+ public void watch(NfcWatchOptions options, WatchResponse callback) {
+ if (!checkIfReady(callback)) return;
+ // TODO(shalamov): Not implemented.
+ callback.call(0, createError(NfcErrorType.NOT_SUPPORTED));
+ }
+
+ /**
+ * Cancels NFC watch operation.
+ *
+ * @param id of watch operation.
+ * @param callback that is used to notify caller when cancelWatch() is completed.
+ */
+ @Override
+ public void cancelWatch(int id, CancelWatchResponse callback) {
+ if (!checkIfReady(callback)) return;
+ // TODO(shalamov): Not implemented.
+ callback.call(createError(NfcErrorType.NOT_SUPPORTED));
+ }
+
+ /**
+ * Cancels all NFC watch operations.
+ *
+ * @param callback that is used to notify caller when cancelAllWatches() is completed.
+ */
+ @Override
+ public void cancelAllWatches(CancelAllWatchesResponse callback) {
+ if (!checkIfReady(callback)) return;
+ // TODO(shalamov): Not implemented.
+ callback.call(createError(NfcErrorType.NOT_SUPPORTED));
+ }
+
+ @Override
+ public void subscribeToWatchEvents(SubscribeToWatchEventsResponse callback) {
+ // TODO(shalamov): Not implemented.
+ }
+
+ /**
+ * Suspends all pending watch / push operations. Should be called when web
+ * page visibility is lost.
+ */
+ @Override
+ public void suspendNfcOperations() {
+ disableForegroundDispatch();
+ }
+
+ /**
+ * Resumes all pending watch / push operations. Should be called when web
+ * page becomes visible.
+ */
+ @Override
+ public void resumeNfcOperations() {
+ if (mPendingPushOperation != null) enableForegroundDispatch();
+ }
+
+ @Override
+ public void close() {
+ disableForegroundDispatch();
+ }
+
+ @Override
+ public void onConnectionError(MojoException e) {
+ close();
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+ if (tag != null) onTagDiscovered(tag);
+ }
+
+ /**
+ * Holds information about pending push operation.
+ */
+ private static class PendingPushOperation {
+ private final NfcMessage mNfcMessage;
+ private final NfcPushOptions mNfcPushOptions;
+ private final PushResponse mPushResponseCallback;
+
+ public PendingPushOperation(
+ NfcMessage message, NfcPushOptions options, PushResponse callback) {
+ mNfcMessage = message;
+ mNfcPushOptions = options;
+ mPushResponseCallback = callback;
+ }
+
+ /**
+ * Completes pending push operation.
+ *
+ * @param error should be null when operation is completed successfully,
+ * otherwise, error object with corresponding NfcErrorType must be provided.
+ */
+ public void complete(NfcError error) {
+ if (mPushResponseCallback != null) mPushResponseCallback.call(error);
+ }
+
+ public NfcMessage message() {
+ return mNfcMessage;
+ }
+ public NfcPushOptions pushOptions() {
+ return mNfcPushOptions;
+ }
+ }
+
+ /**
+ * Helper method that creates NfcError object from NfcErrorType.
+ *
+ * @param errorType @see NfcErrorType.
+ * @return NfcError
+ * @see NfcError
+ */
+ private NfcError createError(int errorType) {
+ NfcError error = new NfcError();
+ error.errorType = errorType;
+ return error;
+ }
+
+ /**
+ * Checks if NFC funcionality can be used by the mojo service.
+ * If permission to use NFC is granted and hardware is enabled, returns null.
+ *
+ * @return NfcError
+ */
+ @Nullable
+ private NfcError checkIfReady() {
+ if (!mHasPermission) {
+ return createError(NfcErrorType.SECURITY);
+ } else if (mNfcManager == null || mNfcAdapter == null
+ || Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD_MR1) {
+ return createError(NfcErrorType.NOT_SUPPORTED);
+ } else if (!mNfcAdapter.isEnabled()) {
+ return createError(NfcErrorType.DEVICE_DISABLED);
+ }
+ return null;
+ }
+
+ /**
+ * Uses checkIfReady() method and if NFC functionality cannot be used,
+ * calls mojo callback with NfcError.
+ *
+ * @param WatchResponse Callback that is provided to watch() method.
+ * @return boolean true if NFC functionality can be used, false otherwise.
+ */
+ private boolean checkIfReady(WatchResponse callback) {
+ NfcError error = checkIfReady();
+ if (error == null) return true;
+ callback.call(0, error);
+ return false;
+ }
+
+ /**
+ * Uses checkIfReady() method and if NFC functionality cannot be used,
+ * calls mojo callback NfcError.
+ *
+ * @param callback Generic callback that is provided to push(), cancelPush(),
+ * cancelWatch() and cancelAllWatches() methods.
+ * @return boolean true if NFC functionality can be used, false otherwise.
+ */
+ private boolean checkIfReady(Callbacks.Callback1<NfcError> callback) {
+ NfcError error = checkIfReady();
+ if (error == null) return true;
+ callback.call(error);
+ return false;
+ }
+
+ /**
+ * Enables foreground dispatch.
+ */
+ private void enableForegroundDispatch() {
+ if (mPendingIntent == null) {
+ mPendingIntent = PendingIntent.getActivity(mActivity, 0,
+ new Intent(mActivity, mActivity.getClass())
+ .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
+ 0);
+
+ List<IntentFilter> filters = new ArrayList<IntentFilter>();
+ IntentFilter tag_filter = new IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED);
+ IntentFilter ndef_filter = new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
+ filters.add(tag_filter);
+
+ try {
+ ndef_filter.addDataType(TEXT_MIME);
+ ndef_filter.addDataType(JSON_MIME);
+ ndef_filter.addDataType(OPAQUE_MIME);
+ filters.add(ndef_filter);
+ } catch (MalformedMimeTypeException e) {
+ Log.w(TAG, "Invalid MIME type was provided to Intent filter.");
+ ndef_filter = null;
+ }
+
+ IntentFilter[] filterArray = new IntentFilter[filters.size()];
+ filters.toArray(filterArray);
+
+ String[][] techLists = new String[][] {
+ new String[] {Ndef.class.getName(), NdefFormatable.class.getName()}};
+
+ LocalBroadcastManager manager = LocalBroadcastManager.getInstance(mActivity);
+ manager.registerReceiver(this, tag_filter);
+ if (ndef_filter != null) manager.registerReceiver(this, ndef_filter);
+ mNfcAdapter.enableForegroundDispatch(mActivity, mPendingIntent, filterArray, techLists);
+ }
+ }
+
+ /**
+ * Disables foreground dispatch.
+ */
+ private void disableForegroundDispatch() {
+ if (mPendingIntent != null) {
+ mNfcAdapter.disableForegroundDispatch(mActivity);
+ LocalBroadcastManager manager = LocalBroadcastManager.getInstance(mActivity);
+ manager.unregisterReceiver(this);
+ mPendingIntent = null;
+ }
+ }
+
+ /**
+ * NdefFormatable and Ndef interfaces have different signatures for writing
+ * NdefMessage to a tag. This interface provides generic write method.
+ */
+ private interface TagTechnologyWriter {
+ public void write(NdefMessage message)
+ throws IOException, TagLostException, FormatException;
+ }
+
+ /**
+ * Implementation of TagTechnologyWriter that can write NdefMessage to NFC tag.
+ */
+ private static class NdefWriter implements TagTechnologyWriter {
+ private final Ndef mNdef;
+
+ NdefWriter(Ndef ndef) {
+ mNdef = ndef;
+ }
+
+ public void write(NdefMessage message)
+ throws IOException, TagLostException, FormatException {
+ mNdef.writeNdefMessage(message);
+ }
+ }
+
+ /**
+ * Implementation of TagTechnologyWriter that can format empty NFC tag
+ * with provided NFCMessage.
+ */
+ private static class NdefFormattableWriter implements TagTechnologyWriter {
+ private final NdefFormatable mNdefFormattable;
+
+ NdefFormattableWriter(NdefFormatable ndefFormattable) {
+ mNdefFormattable = ndefFormattable;
+ }
+
+ public void write(NdefMessage message)
+ throws IOException, TagLostException, FormatException {
+ mNdefFormattable.format(message);
+ }
+ }
+
+ /**
+ * Utility class that holds TagTechnology and TagTechnologyWriter objects.
+ * Provides connectivity and I/O related operations for NFC tag.
+ */
+ private static class NfcTagWriter {
+ private final TagTechnology mTech;
+ private final TagTechnologyWriter mTechWriter;
+ private boolean mWasConnected = false;
+
+ /**
+ * Factory method that creates NfcTagWriter with TagTechnologyWriter
+ * appropriate for a given NFC Tag.
+ *
+ * @param tag @see android.nfc.Tag
+ * @return NfcTagWriter or null when unsupported Tag is provided.
+ */
+ public static NfcTagWriter create(Tag tag) {
+ if (tag == null) return null;
+
+ Ndef ndef = Ndef.get(tag);
+ if (ndef != null) return new NfcTagWriter(ndef, new NdefWriter(ndef));
+
+ NdefFormatable formattable = NdefFormatable.get(tag);
+ if (formattable != null) {
+ return new NfcTagWriter(formattable, new NdefFormattableWriter(formattable));
+ }
+
+ return null;
+ }
+
+ private NfcTagWriter(TagTechnology tech, TagTechnologyWriter writer) {
+ mTech = tech;
+ mTechWriter = writer;
+ }
+
+ /**
+ * Connects to NFC tag.
+ */
+ public void connect() throws IOException, TagLostException {
+ if (!mTech.isConnected()) {
+ mTech.connect();
+ mWasConnected = true;
+ }
+ }
+
+ /**
+ * Closes connection.
+ */
+ public void close() throws IOException {
+ mTech.close();
+ }
+
+ /**
+ * Writes NdefMessage to NFC tag.
+ *
+ * @param message @see android.nfc.NdefMessage
+ */
+ public void write(NdefMessage message)
+ throws IOException, TagLostException, FormatException {
+ mTechWriter.write(message);
+ }
+
+ /**
+ * If tag was previously connected and subsequent connection to the same
+ * tag fails, consider tag to be out of ragne.
+ */
+ public boolean isTagOutOfRange() {
+ try {
+ connect();
+ } catch (IOException e) {
+ return mWasConnected;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Exception that is raised when mojo NfcMessage cannot be coverted to NdefMessage.
+ */
+ private static class InvalidMessageException extends Exception {}
+
+ /**
+ * Converts mojo NfcMessage to android.nfc.NdefMessage.
+ *
+ * @param message mojo NfcMessage
+ * @return NdefMessage
+ * @see android.nfc.NdefMessage
+ */
+ private NdefMessage toNdefMessage(NfcMessage message) throws InvalidMessageException {
+ if (message == null || message.data.length == 0) throw new InvalidMessageException();
+
+ try {
+ List<NdefRecord> records = new ArrayList<NdefRecord>();
+ for (NfcRecord record : message.data) {
+ records.add(toNdefRecord(record));
+ }
+ records.add(NdefRecord.createExternal(DOMAIN, TYPE, message.url.getBytes()));
+ NdefRecord[] ndefRecords = new NdefRecord[records.size()];
+ records.toArray(ndefRecords);
+ return new NdefMessage(ndefRecords);
+ } catch (UnsupportedEncodingException | InvalidMessageException
+ | IllegalArgumentException e) {
+ throw new InvalidMessageException();
+ }
+ }
+
+ /**
+ * Returns charset of mojo NfcRecord. Only applicable for URL and TEXT records.
+ * If charset cannot be determined, UTF-8 charset is used by default.
+ *
+ * @param record
+ * @return String
+ */
+ private String getCharset(NfcRecord record) {
+ if (record.mediaType.endsWith(CHARSET_UTF8)) return "UTF-8";
+
+ if (record.mediaType.endsWith(CHARSET_UTF16)) return "UTF-16LE";
+
+ Log.w(TAG, "Unknown charset, defaulting to UTF-8.");
+ return "UTF-8";
+ }
+
+ /**
+ * Converts mojo NfcRecord to android.nfc.NdefRecord.
+ *
+ * @param record mojo NfcRecord
+ * @return NdefRecord
+ * @see android.nfc.NdefRecord
+ */
+ private NdefRecord toNdefRecord(NfcRecord record)
+ throws InvalidMessageException, IllegalArgumentException, UnsupportedEncodingException {
+ switch (record.recordType) {
+ case NfcRecordType.URL:
+ return NdefRecord.createUri(new String(record.data, getCharset(record)));
+ case NfcRecordType.TEXT:
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return NdefRecord.createTextRecord(
+ "en-US", new String(record.data, getCharset(record)));
+ } else {
+ return NdefRecord.createMime(TEXT_MIME, record.data);
+ }
+ case NfcRecordType.JSON:
+ case NfcRecordType.OPAQUE_RECORD:
+ return NdefRecord.createMime(record.mediaType, record.data);
+ default:
+ throw new InvalidMessageException();
+ }
+ }
+
+ /**
+ * Completes pending push operation. On error, invalidates #mTagWriter.
+ *
+ * @param error
+ */
+ private void pendingPushOperationCompleted(NfcError error) {
+ if (mPendingPushOperation != null) {
+ mPendingPushOperation.complete(error);
+ mPendingPushOperation = null;
+ }
+
+ if (error != null) mTagWriter = null;
+ }
+
+ /**
+ * 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 (mTagWriter == null || mPendingPushOperation == null) return;
+
+ if (mTagWriter.isTagOutOfRange()) {
+ mTagWriter = null;
+ return;
+ }
+
+ try {
+ mTagWriter.connect();
+ mTagWriter.write(toNdefMessage(mPendingPushOperation.message()));
+ pendingPushOperationCompleted(null);
+ mTagWriter.close();
+ } catch (InvalidMessageException e) {
+ Log.w(TAG, "Cannot write data to NFC tag. Invalid NfcMessage.");
+ pendingPushOperationCompleted(createError(NfcErrorType.INVALID_MESSAGE));
+ } catch (TagLostException e) {
+ Log.w(TAG, "Cannot write data to NFC tag. Tag is lost.");
+ pendingPushOperationCompleted(createError(NfcErrorType.IO_ERROR));
+ } catch (FormatException | IOException e) {
+ Log.w(TAG, "Cannot write data to NFC tag. IO_ERROR.");
+ pendingPushOperationCompleted(createError(NfcErrorType.IO_ERROR));
+ }
+ }
+
+ /**
+ * Called by ReaderCallbackHandler when NFC tag is in proximity.
+ * calls processPendingPushOperation() that will write data to a tag.
+ *
+ * @param tag
+ */
+ private void onTagDiscovered(Tag tag) {
+ mTagWriter = NfcTagWriter.create(tag);
+ processPendingPushOperation();
+ }
+}
« no previous file with comments | « device/nfc/android/BUILD.gn ('k') | device/nfc/nfc.gyp » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698