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

Side by Side 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 unified diff | Download patch
« no previous file with comments | « device/nfc/android/BUILD.gn ('k') | device/nfc/nfc.gyp » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package org.chromium.device.nfc;
6
7 import android.Manifest;
8 import android.app.Activity;
9 import android.app.PendingIntent;
10 import android.content.BroadcastReceiver;
11 import android.content.Context;
12 import android.content.Intent;
13 import android.content.IntentFilter;
14 import android.content.IntentFilter.MalformedMimeTypeException;
15 import android.content.pm.PackageManager;
16 import android.nfc.FormatException;
17 import android.nfc.NdefMessage;
18 import android.nfc.NdefRecord;
19 import android.nfc.NfcAdapter;
20 import android.nfc.NfcManager;
21 import android.nfc.Tag;
22 import android.nfc.TagLostException;
23 import android.nfc.tech.Ndef;
24 import android.nfc.tech.NdefFormatable;
25 import android.nfc.tech.TagTechnology;
26 import android.os.Build;
27 import android.os.Process;
28 import android.support.annotation.Nullable;
29 import android.support.v4.content.LocalBroadcastManager;
30
31 import org.chromium.base.Log;
32 import org.chromium.mojo.bindings.Callbacks;
33 import org.chromium.mojo.system.MojoException;
34 import org.chromium.mojom.device.Nfc;
35 import org.chromium.mojom.device.NfcError;
36 import org.chromium.mojom.device.NfcErrorType;
37 import org.chromium.mojom.device.NfcMessage;
38 import org.chromium.mojom.device.NfcPushOptions;
39 import org.chromium.mojom.device.NfcPushTarget;
40 import org.chromium.mojom.device.NfcRecord;
41 import org.chromium.mojom.device.NfcRecordType;
42 import org.chromium.mojom.device.NfcWatchOptions;
43
44 import java.io.IOException;
45 import java.io.UnsupportedEncodingException;
46 import java.util.ArrayList;
47 import java.util.List;
48
49 /**
50 * Android implementation of the NFC mojo service defined in
51 * device/nfc/nfc.mojom.
52 */
53 public class NfcImpl extends BroadcastReceiver implements Nfc {
54 private static final String TAG = "NfcImpl";
55 private static final String DOMAIN = "w3.org";
56 private static final String TYPE = "webnfc";
57 private static final String TEXT_MIME = "text/plain";
58 private static final String JSON_MIME = "application/json";
59 private static final String OPAQUE_MIME = "application/octet-stream";
60 private static final String CHARSET_UTF8 = ";charset=UTF-8";
61 private static final String CHARSET_UTF16 = ";charset=UTF-16";
62
63 /**
64 * Used to get instance of NFC adapter, @see android.nfc.NfcManager
65 */
66 private final NfcManager mNfcManager;
67
68 /**
69 * NFC adapter. @see android.nfc.NfcAdapter
70 */
71 private final NfcAdapter mNfcAdapter;
72
73 /**
74 * Activity object that is requred to enable / disable NFC reader mode opera tions.
75 */
76 private final Activity mActivity;
77
78 /**
79 * Flag that indicates whether NFC permission is granted.
80 */
81 private final boolean mHasPermission;
82
83 /**
84 * Object that contains data that was passed to method
85 * #push(NfcMessage message, NfcPushOptions options, PushResponse callback)
86 * @see PendingPushOperation
87 */
88 private PendingPushOperation mPendingPushOperation;
89
90 /**
91 * Utility that provides I/O operations for a Tag. Created on demand when
92 * Tag is found. @see NfcTagWriter
93 */
94 private NfcTagWriter mTagWriter;
95
96 /**
97 * Pending intent that is used for foreground dispatch.
98 * @see android.nfc.NfcAdapter.enableForegroundDispatch
99 */
100 private PendingIntent mPendingIntent;
101
102 public NfcImpl(Activity activity) {
103 mActivity = activity;
104
105 if (mActivity != null) {
106 int permission = mActivity.checkPermission(
107 Manifest.permission.NFC, Process.myPid(), Process.myUid());
108 mHasPermission = permission == PackageManager.PERMISSION_GRANTED;
109 } else {
110 mHasPermission = false;
111 }
112
113 if (mHasPermission) {
114 mNfcManager = (NfcManager) mActivity.getSystemService(Context.NFC_SE RVICE);
115 if (mNfcManager != null) {
116 mNfcAdapter = mNfcManager.getDefaultAdapter();
117 } else {
118 Log.w(TAG, "NFC is not supported.");
119 mNfcAdapter = null;
120 }
121 } else {
122 Log.w(TAG, "NFC operations are not permitted.");
123 mNfcAdapter = null;
124 mNfcManager = null;
125 }
126 }
127
128 /**
129 * Pushes NfcMessage to Tag or Peer, whenever NFC device is in proximity.
130 * At the moment, only passive NFC devices are supported (NfcPushTarget.TAG) .
131 *
132 * @param message that should be pushed to NFC device.
133 * @param options that contain information about timeout and target device t ype.
134 * @param callback that is used to notify when push operation is completed.
135 */
136 @Override
137 public void push(NfcMessage message, NfcPushOptions options, PushResponse ca llback) {
138 if (!checkIfReady(callback)) return;
139
140 if (options.target == NfcPushTarget.PEER) {
141 callback.call(createError(NfcErrorType.NOT_SUPPORTED));
142 return;
143 }
144
145 // If previous pending push operation is not completed, subsequent call
146 // should cancel pending operation.
147 if (mPendingPushOperation != null) {
148 mPendingPushOperation.complete(createError(NfcErrorType.OPERATION_CA NCELLED));
149 }
150
151 mPendingPushOperation = new PendingPushOperation(message, options, callb ack);
152 enableForegroundDispatch();
153 processPendingPushOperation();
154 }
155
156 /**
157 * Cancels pending push operation.
158 * At the moment, only passive NFC devices are supported (NfcPushTarget.TAG) .
159 *
160 * @param target @see NfcPushTarget
161 * @param callback that is used to notify caller when cancelPush() is comple ted.
162 */
163 @Override
164 public void cancelPush(int target, CancelPushResponse callback) {
165 if (!checkIfReady(callback)) return;
166
167 if (target == NfcPushTarget.PEER) {
168 callback.call(createError(NfcErrorType.NOT_SUPPORTED));
169 return;
170 }
171
172 if (mPendingPushOperation != null) {
173 mPendingPushOperation.complete(createError(NfcErrorType.OPERATION_CA NCELLED));
174 mPendingPushOperation = null;
175 callback.call(null);
176 disableForegroundDispatch();
177 } else {
178 callback.call(createError(NfcErrorType.NOT_FOUND));
179 }
180 }
181
182 /**
183 * Watch method allows to set filtering criteria for NfcMessages that are
184 * found when NFC device is within proximity. On success, watch ID is
185 * returned to caller through WatchResponse callback. When NfcMessage that
186 * matches NfcWatchOptions is found, it is returned through
187 * SubscribeToWatchEventsResponse callback interface.
188 * @see SubscribeToWatchEventsResponse
189 *
190 * @param options used to filter NfcMessages, @see NfcWatchOptions.
191 * @param callback that is used to notify caller when watch() is completed a nd return watch ID.
192 */
193 @Override
194 public void watch(NfcWatchOptions options, WatchResponse callback) {
195 if (!checkIfReady(callback)) return;
196 // TODO(shalamov): Not implemented.
197 callback.call(0, createError(NfcErrorType.NOT_SUPPORTED));
198 }
199
200 /**
201 * Cancels NFC watch operation.
202 *
203 * @param id of watch operation.
204 * @param callback that is used to notify caller when cancelWatch() is compl eted.
205 */
206 @Override
207 public void cancelWatch(int id, CancelWatchResponse callback) {
208 if (!checkIfReady(callback)) return;
209 // TODO(shalamov): Not implemented.
210 callback.call(createError(NfcErrorType.NOT_SUPPORTED));
211 }
212
213 /**
214 * Cancels all NFC watch operations.
215 *
216 * @param callback that is used to notify caller when cancelAllWatches() is completed.
217 */
218 @Override
219 public void cancelAllWatches(CancelAllWatchesResponse callback) {
220 if (!checkIfReady(callback)) return;
221 // TODO(shalamov): Not implemented.
222 callback.call(createError(NfcErrorType.NOT_SUPPORTED));
223 }
224
225 @Override
226 public void subscribeToWatchEvents(SubscribeToWatchEventsResponse callback) {
227 // TODO(shalamov): Not implemented.
228 }
229
230 /**
231 * Suspends all pending watch / push operations. Should be called when web
232 * page visibility is lost.
233 */
234 @Override
235 public void suspendNfcOperations() {
236 disableForegroundDispatch();
237 }
238
239 /**
240 * Resumes all pending watch / push operations. Should be called when web
241 * page becomes visible.
242 */
243 @Override
244 public void resumeNfcOperations() {
245 if (mPendingPushOperation != null) enableForegroundDispatch();
246 }
247
248 @Override
249 public void close() {
250 disableForegroundDispatch();
251 }
252
253 @Override
254 public void onConnectionError(MojoException e) {
255 close();
256 }
257
258 @Override
259 public void onReceive(Context context, Intent intent) {
260 Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
261 if (tag != null) onTagDiscovered(tag);
262 }
263
264 /**
265 * Holds information about pending push operation.
266 */
267 private static class PendingPushOperation {
268 private final NfcMessage mNfcMessage;
269 private final NfcPushOptions mNfcPushOptions;
270 private final PushResponse mPushResponseCallback;
271
272 public PendingPushOperation(
273 NfcMessage message, NfcPushOptions options, PushResponse callbac k) {
274 mNfcMessage = message;
275 mNfcPushOptions = options;
276 mPushResponseCallback = callback;
277 }
278
279 /**
280 * Completes pending push operation.
281 *
282 * @param error should be null when operation is completed successfully,
283 * otherwise, error object with corresponding NfcErrorType must be provi ded.
284 */
285 public void complete(NfcError error) {
286 if (mPushResponseCallback != null) mPushResponseCallback.call(error) ;
287 }
288
289 public NfcMessage message() {
290 return mNfcMessage;
291 }
292 public NfcPushOptions pushOptions() {
293 return mNfcPushOptions;
294 }
295 }
296
297 /**
298 * Helper method that creates NfcError object from NfcErrorType.
299 *
300 * @param errorType @see NfcErrorType.
301 * @return NfcError
302 * @see NfcError
303 */
304 private NfcError createError(int errorType) {
305 NfcError error = new NfcError();
306 error.errorType = errorType;
307 return error;
308 }
309
310 /**
311 * Checks if NFC funcionality can be used by the mojo service.
312 * If permission to use NFC is granted and hardware is enabled, returns null .
313 *
314 * @return NfcError
315 */
316 @Nullable
317 private NfcError checkIfReady() {
318 if (!mHasPermission) {
319 return createError(NfcErrorType.SECURITY);
320 } else if (mNfcManager == null || mNfcAdapter == null
321 || Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD_MR1) {
322 return createError(NfcErrorType.NOT_SUPPORTED);
323 } else if (!mNfcAdapter.isEnabled()) {
324 return createError(NfcErrorType.DEVICE_DISABLED);
325 }
326 return null;
327 }
328
329 /**
330 * Uses checkIfReady() method and if NFC functionality cannot be used,
331 * calls mojo callback with NfcError.
332 *
333 * @param WatchResponse Callback that is provided to watch() method.
334 * @return boolean true if NFC functionality can be used, false otherwise.
335 */
336 private boolean checkIfReady(WatchResponse callback) {
337 NfcError error = checkIfReady();
338 if (error == null) return true;
339 callback.call(0, error);
340 return false;
341 }
342
343 /**
344 * Uses checkIfReady() method and if NFC functionality cannot be used,
345 * calls mojo callback NfcError.
346 *
347 * @param callback Generic callback that is provided to push(), cancelPush() ,
348 * cancelWatch() and cancelAllWatches() methods.
349 * @return boolean true if NFC functionality can be used, false otherwise.
350 */
351 private boolean checkIfReady(Callbacks.Callback1<NfcError> callback) {
352 NfcError error = checkIfReady();
353 if (error == null) return true;
354 callback.call(error);
355 return false;
356 }
357
358 /**
359 * Enables foreground dispatch.
360 */
361 private void enableForegroundDispatch() {
362 if (mPendingIntent == null) {
363 mPendingIntent = PendingIntent.getActivity(mActivity, 0,
364 new Intent(mActivity, mActivity.getClass())
365 .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
366 0);
367
368 List<IntentFilter> filters = new ArrayList<IntentFilter>();
369 IntentFilter tag_filter = new IntentFilter(NfcAdapter.ACTION_TAG_DIS COVERED);
370 IntentFilter ndef_filter = new IntentFilter(NfcAdapter.ACTION_NDEF_D ISCOVERED);
371 filters.add(tag_filter);
372
373 try {
374 ndef_filter.addDataType(TEXT_MIME);
375 ndef_filter.addDataType(JSON_MIME);
376 ndef_filter.addDataType(OPAQUE_MIME);
377 filters.add(ndef_filter);
378 } catch (MalformedMimeTypeException e) {
379 Log.w(TAG, "Invalid MIME type was provided to Intent filter.");
380 ndef_filter = null;
381 }
382
383 IntentFilter[] filterArray = new IntentFilter[filters.size()];
384 filters.toArray(filterArray);
385
386 String[][] techLists = new String[][] {
387 new String[] {Ndef.class.getName(), NdefFormatable.class.get Name()}};
388
389 LocalBroadcastManager manager = LocalBroadcastManager.getInstance(mA ctivity);
390 manager.registerReceiver(this, tag_filter);
391 if (ndef_filter != null) manager.registerReceiver(this, ndef_filter) ;
392 mNfcAdapter.enableForegroundDispatch(mActivity, mPendingIntent, filt erArray, techLists);
393 }
394 }
395
396 /**
397 * Disables foreground dispatch.
398 */
399 private void disableForegroundDispatch() {
400 if (mPendingIntent != null) {
401 mNfcAdapter.disableForegroundDispatch(mActivity);
402 LocalBroadcastManager manager = LocalBroadcastManager.getInstance(mA ctivity);
403 manager.unregisterReceiver(this);
404 mPendingIntent = null;
405 }
406 }
407
408 /**
409 * NdefFormatable and Ndef interfaces have different signatures for writing
410 * NdefMessage to a tag. This interface provides generic write method.
411 */
412 private interface TagTechnologyWriter {
413 public void write(NdefMessage message)
414 throws IOException, TagLostException, FormatException;
415 }
416
417 /**
418 * Implementation of TagTechnologyWriter that can write NdefMessage to NFC t ag.
419 */
420 private static class NdefWriter implements TagTechnologyWriter {
421 private final Ndef mNdef;
422
423 NdefWriter(Ndef ndef) {
424 mNdef = ndef;
425 }
426
427 public void write(NdefMessage message)
428 throws IOException, TagLostException, FormatException {
429 mNdef.writeNdefMessage(message);
430 }
431 }
432
433 /**
434 * Implementation of TagTechnologyWriter that can format empty NFC tag
435 * with provided NFCMessage.
436 */
437 private static class NdefFormattableWriter implements TagTechnologyWriter {
438 private final NdefFormatable mNdefFormattable;
439
440 NdefFormattableWriter(NdefFormatable ndefFormattable) {
441 mNdefFormattable = ndefFormattable;
442 }
443
444 public void write(NdefMessage message)
445 throws IOException, TagLostException, FormatException {
446 mNdefFormattable.format(message);
447 }
448 }
449
450 /**
451 * Utility class that holds TagTechnology and TagTechnologyWriter objects.
452 * Provides connectivity and I/O related operations for NFC tag.
453 */
454 private static class NfcTagWriter {
455 private final TagTechnology mTech;
456 private final TagTechnologyWriter mTechWriter;
457 private boolean mWasConnected = false;
458
459 /**
460 * Factory method that creates NfcTagWriter with TagTechnologyWriter
461 * appropriate for a given NFC Tag.
462 *
463 * @param tag @see android.nfc.Tag
464 * @return NfcTagWriter or null when unsupported Tag is provided.
465 */
466 public static NfcTagWriter create(Tag tag) {
467 if (tag == null) return null;
468
469 Ndef ndef = Ndef.get(tag);
470 if (ndef != null) return new NfcTagWriter(ndef, new NdefWriter(ndef) );
471
472 NdefFormatable formattable = NdefFormatable.get(tag);
473 if (formattable != null) {
474 return new NfcTagWriter(formattable, new NdefFormattableWriter(f ormattable));
475 }
476
477 return null;
478 }
479
480 private NfcTagWriter(TagTechnology tech, TagTechnologyWriter writer) {
481 mTech = tech;
482 mTechWriter = writer;
483 }
484
485 /**
486 * Connects to NFC tag.
487 */
488 public void connect() throws IOException, TagLostException {
489 if (!mTech.isConnected()) {
490 mTech.connect();
491 mWasConnected = true;
492 }
493 }
494
495 /**
496 * Closes connection.
497 */
498 public void close() throws IOException {
499 mTech.close();
500 }
501
502 /**
503 * Writes NdefMessage to NFC tag.
504 *
505 * @param message @see android.nfc.NdefMessage
506 */
507 public void write(NdefMessage message)
508 throws IOException, TagLostException, FormatException {
509 mTechWriter.write(message);
510 }
511
512 /**
513 * If tag was previously connected and subsequent connection to the same
514 * tag fails, consider tag to be out of ragne.
515 */
516 public boolean isTagOutOfRange() {
517 try {
518 connect();
519 } catch (IOException e) {
520 return mWasConnected;
521 }
522 return false;
523 }
524 }
525
526 /**
527 * Exception that is raised when mojo NfcMessage cannot be coverted to NdefM essage.
528 */
529 private static class InvalidMessageException extends Exception {}
530
531 /**
532 * Converts mojo NfcMessage to android.nfc.NdefMessage.
533 *
534 * @param message mojo NfcMessage
535 * @return NdefMessage
536 * @see android.nfc.NdefMessage
537 */
538 private NdefMessage toNdefMessage(NfcMessage message) throws InvalidMessageE xception {
539 if (message == null || message.data.length == 0) throw new InvalidMessag eException();
540
541 try {
542 List<NdefRecord> records = new ArrayList<NdefRecord>();
543 for (NfcRecord record : message.data) {
544 records.add(toNdefRecord(record));
545 }
546 records.add(NdefRecord.createExternal(DOMAIN, TYPE, message.url.getB ytes()));
547 NdefRecord[] ndefRecords = new NdefRecord[records.size()];
548 records.toArray(ndefRecords);
549 return new NdefMessage(ndefRecords);
550 } catch (UnsupportedEncodingException | InvalidMessageException
551 | IllegalArgumentException e) {
552 throw new InvalidMessageException();
553 }
554 }
555
556 /**
557 * Returns charset of mojo NfcRecord. Only applicable for URL and TEXT recor ds.
558 * If charset cannot be determined, UTF-8 charset is used by default.
559 *
560 * @param record
561 * @return String
562 */
563 private String getCharset(NfcRecord record) {
564 if (record.mediaType.endsWith(CHARSET_UTF8)) return "UTF-8";
565
566 if (record.mediaType.endsWith(CHARSET_UTF16)) return "UTF-16LE";
567
568 Log.w(TAG, "Unknown charset, defaulting to UTF-8.");
569 return "UTF-8";
570 }
571
572 /**
573 * Converts mojo NfcRecord to android.nfc.NdefRecord.
574 *
575 * @param record mojo NfcRecord
576 * @return NdefRecord
577 * @see android.nfc.NdefRecord
578 */
579 private NdefRecord toNdefRecord(NfcRecord record)
580 throws InvalidMessageException, IllegalArgumentException, Unsupporte dEncodingException {
581 switch (record.recordType) {
582 case NfcRecordType.URL:
583 return NdefRecord.createUri(new String(record.data, getCharset(r ecord)));
584 case NfcRecordType.TEXT:
585 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
586 return NdefRecord.createTextRecord(
587 "en-US", new String(record.data, getCharset(record)) );
588 } else {
589 return NdefRecord.createMime(TEXT_MIME, record.data);
590 }
591 case NfcRecordType.JSON:
592 case NfcRecordType.OPAQUE_RECORD:
593 return NdefRecord.createMime(record.mediaType, record.data);
594 default:
595 throw new InvalidMessageException();
596 }
597 }
598
599 /**
600 * Completes pending push operation. On error, invalidates #mTagWriter.
601 *
602 * @param error
603 */
604 private void pendingPushOperationCompleted(NfcError error) {
605 if (mPendingPushOperation != null) {
606 mPendingPushOperation.complete(error);
607 mPendingPushOperation = null;
608 }
609
610 if (error != null) mTagWriter = null;
611 }
612
613 /**
614 * Checks whether there is a #mPendingPushOperation and writes data to NFC t ag.
615 * In case of exception calls pendingPushOperationCompleted() with appropria te
616 * error object.
617 */
618 private void processPendingPushOperation() {
619 if (mTagWriter == null || mPendingPushOperation == null) return;
620
621 if (mTagWriter.isTagOutOfRange()) {
622 mTagWriter = null;
623 return;
624 }
625
626 try {
627 mTagWriter.connect();
628 mTagWriter.write(toNdefMessage(mPendingPushOperation.message()));
629 pendingPushOperationCompleted(null);
630 mTagWriter.close();
631 } catch (InvalidMessageException e) {
632 Log.w(TAG, "Cannot write data to NFC tag. Invalid NfcMessage.");
633 pendingPushOperationCompleted(createError(NfcErrorType.INVALID_MESSA GE));
634 } catch (TagLostException e) {
635 Log.w(TAG, "Cannot write data to NFC tag. Tag is lost.");
636 pendingPushOperationCompleted(createError(NfcErrorType.IO_ERROR));
637 } catch (FormatException | IOException e) {
638 Log.w(TAG, "Cannot write data to NFC tag. IO_ERROR.");
639 pendingPushOperationCompleted(createError(NfcErrorType.IO_ERROR));
640 }
641 }
642
643 /**
644 * Called by ReaderCallbackHandler when NFC tag is in proximity.
645 * calls processPendingPushOperation() that will write data to a tag.
646 *
647 * @param tag
648 */
649 private void onTagDiscovered(Tag tag) {
650 mTagWriter = NfcTagWriter.create(tag);
651 processPendingPushOperation();
652 }
653 }
OLDNEW
« 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