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

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

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