diff --git a/samples/NFCDemo/Android.mk b/samples/NFCDemo/Android.mk new file mode 100644 index 000000000..60815a207 --- /dev/null +++ b/samples/NFCDemo/Android.mk @@ -0,0 +1,18 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := samples tests + +LOCAL_STATIC_JAVA_LIBRARIES := guava + +# Only compile source java files in this apk. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_PACKAGE_NAME := NFCDemo + +LOCAL_SDK_VERSION := current + +include $(BUILD_PACKAGE) + +# Use the following include to make our test apk. +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/samples/NFCDemo/AndroidManifest.xml b/samples/NFCDemo/AndroidManifest.xml new file mode 100644 index 000000000..044690b75 --- /dev/null +++ b/samples/NFCDemo/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/NFCDemo/_index.html b/samples/NFCDemo/_index.html new file mode 100644 index 000000000..d1c53f17d --- /dev/null +++ b/samples/NFCDemo/_index.html @@ -0,0 +1,129 @@ +

+ Near-field Communication or NFC is a standard defined by the + NFC Forum + . + NFC Data Exchange Format (NDEF) defines a common data format between NFC-compliant devices and tags. + This demo application shows how to read a NDEF Tags using using Android 2.3 SDK APIs. + The NFC Tags consist of data encoded in NDEF Message format specified by NFC Forum Type 2 Specification. + Each NDEF message consists of one or more NDEF Records. + + You need a NFC compliant device and a NFC compliant Tag to use this sample app. Or else, you could use + the FakeTagsActivity displayed at launch of this sample app, to generate fake Tag broadcasts from the emulator. +

+ +

The application includes: +

+ +

If you are developing an application that uses the NFC API, remember that the feature + is supported only on Android 2.3 (API level 9) and higher versions of the platform. Also, + among devices running Android 2.3 (API level 9) or higher, not all devices will offer NFC + support. To ensure that your application can only be installed on devices that are capable + of supporting NFC, remember to add the following to the application's manifest before + publishing to Android Market: +

+ +

To control how Android Market filters your application + from devices that do not support NFC, remember to add the following to the application's manifest +

+

For more information about using the NFC API, see the + + android.nfc + + documentation. +

+ diff --git a/samples/NFCDemo/res/drawable/icon.png b/samples/NFCDemo/res/drawable/icon.png new file mode 100644 index 000000000..a07c69fa5 Binary files /dev/null and b/samples/NFCDemo/res/drawable/icon.png differ diff --git a/samples/NFCDemo/res/layout/tag_divider.xml b/samples/NFCDemo/res/layout/tag_divider.xml new file mode 100644 index 000000000..b6b1b7cd4 --- /dev/null +++ b/samples/NFCDemo/res/layout/tag_divider.xml @@ -0,0 +1,22 @@ + + + + \ No newline at end of file diff --git a/samples/NFCDemo/res/layout/tag_text.xml b/samples/NFCDemo/res/layout/tag_text.xml new file mode 100644 index 000000000..59cbbfbc9 --- /dev/null +++ b/samples/NFCDemo/res/layout/tag_text.xml @@ -0,0 +1,28 @@ + + + + \ No newline at end of file diff --git a/samples/NFCDemo/res/layout/tag_viewer.xml b/samples/NFCDemo/res/layout/tag_viewer.xml new file mode 100644 index 000000000..a429ef95d --- /dev/null +++ b/samples/NFCDemo/res/layout/tag_viewer.xml @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file diff --git a/samples/NFCDemo/res/raw/discovered_tag_notification.ogg b/samples/NFCDemo/res/raw/discovered_tag_notification.ogg new file mode 100755 index 000000000..7bf5df7ef Binary files /dev/null and b/samples/NFCDemo/res/raw/discovered_tag_notification.ogg differ diff --git a/samples/NFCDemo/res/values/strings.xml b/samples/NFCDemo/res/values/strings.xml new file mode 100644 index 000000000..251d2f66c --- /dev/null +++ b/samples/NFCDemo/res/values/strings.xml @@ -0,0 +1,28 @@ + + + + + + + NFCDemo + + + New tag collected + + + Text + + diff --git a/samples/NFCDemo/src/com/example/android/nfc/NdefMessageParser.java b/samples/NFCDemo/src/com/example/android/nfc/NdefMessageParser.java new file mode 100644 index 000000000..7372810b3 --- /dev/null +++ b/samples/NFCDemo/src/com/example/android/nfc/NdefMessageParser.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 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.example.android.nfc; + +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; + +import com.example.android.nfc.record.ParsedNdefRecord; +import com.example.android.nfc.record.SmartPoster; +import com.example.android.nfc.record.TextRecord; +import com.example.android.nfc.record.UriRecord; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utility class for creating {@link ParsedNdefMessage}s. + */ +public class NdefMessageParser { + + // Utility class + private NdefMessageParser() { + + } + + /** Parse an NdefMessage */ + public static List parse(NdefMessage message) { + return getRecords(message.getRecords()); + } + + public static List getRecords(NdefRecord[] records) { + List elements = new ArrayList(); + for (NdefRecord record : records) { + if (UriRecord.isUri(record)) { + elements.add(UriRecord.parse(record)); + } else if (TextRecord.isText(record)) { + elements.add(TextRecord.parse(record)); + } else if (SmartPoster.isPoster(record)) { + elements.add(SmartPoster.parse(record)); + } + } + return elements; + } +} diff --git a/samples/NFCDemo/src/com/example/android/nfc/TagViewer.java b/samples/NFCDemo/src/com/example/android/nfc/TagViewer.java new file mode 100644 index 000000000..01dc0bd80 --- /dev/null +++ b/samples/NFCDemo/src/com/example/android/nfc/TagViewer.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2010 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.example.android.nfc; + +import android.app.Activity; +import android.content.Intent; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.WindowManager; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.example.android.nfc.record.ParsedNdefRecord; + +import java.util.List; + +/** + * An {@link Activity} which handles a broadcast of a new tag that the device + * just discovered. + */ +public class TagViewer extends Activity { + + static final String TAG = "ViewTag"; + + /** + * This activity will finish itself in this amount of time if the user + * doesn't do anything. + */ + static final int ACTIVITY_TIMEOUT_MS = 1 * 1000; + + TextView mTitle; + + LinearLayout mTagContent; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.tag_viewer); + mTagContent = (LinearLayout) findViewById(R.id.list); + mTitle = (TextView) findViewById(R.id.title); + resolveIntent(getIntent()); + } + + void resolveIntent(Intent intent) { + // Parse the intent + String action = intent.getAction(); + if (NfcAdapter.ACTION_TAG_DISCOVERED.equals(action)) { + // When a tag is discovered we send it to the service to be save. We + // include a PendingIntent for the service to call back onto. This + // will cause this activity to be restarted with onNewIntent(). At + // that time we read it from the database and view it. + Parcelable[] rawMsgs = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES); + NdefMessage[] msgs; + if (rawMsgs != null) { + msgs = new NdefMessage[rawMsgs.length]; + for (int i = 0; i < rawMsgs.length; i++) { + msgs[i] = (NdefMessage) rawMsgs[i]; + } + } else { + // Unknown tag type + byte[] empty = new byte[] {}; + NdefRecord record = new NdefRecord(NdefRecord.TNF_UNKNOWN, empty, empty, empty); + NdefMessage msg = new NdefMessage(new NdefRecord[] {record}); + msgs = new NdefMessage[] {msg}; + } + // Setup the views + setTitle(R.string.title_scanned_tag); + buildTagViews(msgs); + } else { + Log.e(TAG, "Unknown intent " + intent); + finish(); + return; + } + } + + void buildTagViews(NdefMessage[] msgs) { + if (msgs == null || msgs.length == 0) { + return; + } + LayoutInflater inflater = LayoutInflater.from(this); + LinearLayout content = mTagContent; + // Clear out any old views in the content area, for example if you scan + // two tags in a row. + content.removeAllViews(); + // Parse the first message in the list + // Build views for all of the sub records + List records = NdefMessageParser.parse(msgs[0]); + final int size = records.size(); + for (int i = 0; i < size; i++) { + ParsedNdefRecord record = records.get(i); + content.addView(record.getView(this, inflater, content, i)); + inflater.inflate(R.layout.tag_divider, content, true); + } + } + + @Override + public void onNewIntent(Intent intent) { + setIntent(intent); + resolveIntent(intent); + } + + @Override + public void setTitle(CharSequence title) { + mTitle.setText(title); + } +} diff --git a/samples/NFCDemo/src/com/example/android/nfc/record/ParsedNdefRecord.java b/samples/NFCDemo/src/com/example/android/nfc/record/ParsedNdefRecord.java new file mode 100644 index 000000000..b706ae8cb --- /dev/null +++ b/samples/NFCDemo/src/com/example/android/nfc/record/ParsedNdefRecord.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 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.example.android.nfc.record; + +import android.app.Activity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + + +public interface ParsedNdefRecord { + + /** + * Returns a view to display this record. + */ + public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent, + int offset); + +} diff --git a/samples/NFCDemo/src/com/example/android/nfc/record/SmartPoster.java b/samples/NFCDemo/src/com/example/android/nfc/record/SmartPoster.java new file mode 100644 index 000000000..f86d3ef63 --- /dev/null +++ b/samples/NFCDemo/src/com/example/android/nfc/record/SmartPoster.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2010 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.example.android.nfc.record; + +import android.app.Activity; +import android.nfc.FormatException; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.LinearLayout; + +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; + +import com.example.android.nfc.NdefMessageParser; +import com.example.android.nfc.R; + +import java.util.Arrays; +import java.util.NoSuchElementException; + +/** + * A representation of an NFC Forum "Smart Poster". + */ +public class SmartPoster implements ParsedNdefRecord { + + /** + * NFC Forum Smart Poster Record Type Definition section 3.2.1. + * + * "The Title record for the service (there can be many of these in + * different languages, but a language MUST NOT be repeated). This record is + * optional." + */ + private final TextRecord mTitleRecord; + + /** + * NFC Forum Smart Poster Record Type Definition section 3.2.1. + * + * "The URI record. This is the core of the Smart Poster, and all other + * records are just metadata about this record. There MUST be one URI record + * and there MUST NOT be more than one." + */ + private final UriRecord mUriRecord; + + /** + * NFC Forum Smart Poster Record Type Definition section 3.2.1. + * + * "The Action record. This record describes how the service should be + * treated. For example, the action may indicate that the device should save + * the URI as a bookmark or open a browser. The Action record is optional. + * If it does not exist, the device may decide what to do with the service. + * If the action record exists, it should be treated as a strong suggestion; + * the UI designer may ignore it, but doing so will induce a different user + * experience from device to device." + */ + private final RecommendedAction mAction; + + /** + * NFC Forum Smart Poster Record Type Definition section 3.2.1. + * + * "The Type record. If the URI references an external entity (e.g., via a + * URL), the Type record may be used to declare the MIME type of the entity. + * This can be used to tell the mobile device what kind of an object it can + * expect before it opens the connection. The Type record is optional." + */ + private final String mType; + + private SmartPoster(UriRecord uri, TextRecord title, RecommendedAction action, String type) { + mUriRecord = Preconditions.checkNotNull(uri); + mTitleRecord = title; + mAction = Preconditions.checkNotNull(action); + mType = type; + } + + public UriRecord getUriRecord() { + return mUriRecord; + } + + /** + * Returns the title of the smart poster. This may be {@code null}. + */ + public TextRecord getTitle() { + return mTitleRecord; + } + + public static SmartPoster parse(NdefRecord record) { + Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN); + Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_SMART_POSTER)); + try { + NdefMessage subRecords = new NdefMessage(record.getPayload()); + return parse(subRecords.getRecords()); + } catch (FormatException e) { + throw new IllegalArgumentException(e); + } + } + + public static SmartPoster parse(NdefRecord[] recordsRaw) { + try { + Iterable records = NdefMessageParser.getRecords(recordsRaw); + UriRecord uri = Iterables.getOnlyElement(Iterables.filter(records, UriRecord.class)); + TextRecord title = getFirstIfExists(records, TextRecord.class); + RecommendedAction action = parseRecommendedAction(recordsRaw); + String type = parseType(recordsRaw); + return new SmartPoster(uri, title, action, type); + } catch (NoSuchElementException e) { + throw new IllegalArgumentException(e); + } + } + + public static boolean isPoster(NdefRecord record) { + try { + parse(record); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent, int offset) { + if (mTitleRecord != null) { + // Build a container to hold the title and the URI + LinearLayout container = new LinearLayout(activity); + container.setOrientation(LinearLayout.VERTICAL); + container.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT)); + container.addView(mTitleRecord.getView(activity, inflater, container, offset)); + inflater.inflate(R.layout.tag_divider, container); + container.addView(mUriRecord.getView(activity, inflater, container, offset)); + return container; + } else { + // Just a URI, return a view for it directly + return mUriRecord.getView(activity, inflater, parent, offset); + } + } + + /** + * Returns the first element of {@code elements} which is an instance of + * {@code type}, or {@code null} if no such element exists. + */ + private static T getFirstIfExists(Iterable elements, Class type) { + Iterable filtered = Iterables.filter(elements, type); + T instance = null; + if (!Iterables.isEmpty(filtered)) { + instance = Iterables.get(filtered, 0); + } + return instance; + } + + private enum RecommendedAction { + UNKNOWN((byte) -1), DO_ACTION((byte) 0), SAVE_FOR_LATER((byte) 1), OPEN_FOR_EDITING( + (byte) 2); + + private static final ImmutableMap LOOKUP; + static { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (RecommendedAction action : RecommendedAction.values()) { + builder.put(action.getByte(), action); + } + LOOKUP = builder.build(); + } + + private final byte mAction; + + private RecommendedAction(byte val) { + this.mAction = val; + } + + private byte getByte() { + return mAction; + } + } + + private static NdefRecord getByType(byte[] type, NdefRecord[] records) { + for (NdefRecord record : records) { + if (Arrays.equals(type, record.getType())) { + return record; + } + } + return null; + } + + private static final byte[] ACTION_RECORD_TYPE = new byte[] {'a', 'c', 't'}; + + private static RecommendedAction parseRecommendedAction(NdefRecord[] records) { + NdefRecord record = getByType(ACTION_RECORD_TYPE, records); + if (record == null) { + return RecommendedAction.UNKNOWN; + } + byte action = record.getPayload()[0]; + if (RecommendedAction.LOOKUP.containsKey(action)) { + return RecommendedAction.LOOKUP.get(action); + } + return RecommendedAction.UNKNOWN; + } + + private static final byte[] TYPE_TYPE = new byte[] {'t'}; + + private static String parseType(NdefRecord[] records) { + NdefRecord type = getByType(TYPE_TYPE, records); + if (type == null) { + return null; + } + return new String(type.getPayload(), Charsets.UTF_8); + } +} diff --git a/samples/NFCDemo/src/com/example/android/nfc/record/TextRecord.java b/samples/NFCDemo/src/com/example/android/nfc/record/TextRecord.java new file mode 100644 index 000000000..8041cca9b --- /dev/null +++ b/samples/NFCDemo/src/com/example/android/nfc/record/TextRecord.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2010 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.example.android.nfc.record; + +import android.app.Activity; +import android.nfc.NdefRecord; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.google.common.base.Preconditions; + +import com.example.android.nfc.R; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; + +/** + * An NFC Text Record + */ +public class TextRecord implements ParsedNdefRecord { + + /** ISO/IANA language code */ + private final String mLanguageCode; + + private final String mText; + + private TextRecord(String languageCode, String text) { + mLanguageCode = Preconditions.checkNotNull(languageCode); + mText = Preconditions.checkNotNull(text); + } + + public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent, int offset) { + TextView text = (TextView) inflater.inflate(R.layout.tag_text, parent, false); + text.setText(mText); + return text; + } + + public String getText() { + return mText; + } + + /** + * Returns the ISO/IANA language code associated with this text element. + */ + public String getLanguageCode() { + return mLanguageCode; + } + + // TODO: deal with text fields which span multiple NdefRecords + public static TextRecord parse(NdefRecord record) { + Preconditions.checkArgument(record.getTnf() == NdefRecord.TNF_WELL_KNOWN); + Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_TEXT)); + try { + byte[] payload = record.getPayload(); + /* + * payload[0] contains the "Status Byte Encodings" field, per the + * NFC Forum "Text Record Type Definition" section 3.2.1. + * + * bit7 is the Text Encoding Field. + * + * if (Bit_7 == 0): The text is encoded in UTF-8 if (Bit_7 == 1): + * The text is encoded in UTF16 + * + * Bit_6 is reserved for future use and must be set to zero. + * + * Bits 5 to 0 are the length of the IANA language code. + */ + String textEncoding = ((payload[0] & 0200) == 0) ? "UTF-8" : "UTF-16"; + int languageCodeLength = payload[0] & 0077; + String languageCode = new String(payload, 1, languageCodeLength, "US-ASCII"); + String text = + new String(payload, languageCodeLength + 1, + payload.length - languageCodeLength - 1, textEncoding); + return new TextRecord(languageCode, text); + } catch (UnsupportedEncodingException e) { + // should never happen unless we get a malformed tag. + throw new IllegalArgumentException(e); + } + } + + public static boolean isText(NdefRecord record) { + try { + parse(record); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } +} diff --git a/samples/NFCDemo/src/com/example/android/nfc/record/UriRecord.java b/samples/NFCDemo/src/com/example/android/nfc/record/UriRecord.java new file mode 100644 index 000000000..452ebb282 --- /dev/null +++ b/samples/NFCDemo/src/com/example/android/nfc/record/UriRecord.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2010 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.example.android.nfc.record; + +import android.app.Activity; +import android.net.Uri; +import android.nfc.NdefRecord; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.google.common.base.Preconditions; +import com.google.common.collect.BiMap; +import com.google.common.collect.ImmutableBiMap; +import com.google.common.primitives.Bytes; + +import com.example.android.nfc.R; + +import java.nio.charset.Charset; +import java.util.Arrays; + +/** + * A parsed record containing a Uri. + */ +public class UriRecord implements ParsedNdefRecord { + + private static final String TAG = "UriRecord"; + + public static final String RECORD_TYPE = "UriRecord"; + + /** + * NFC Forum "URI Record Type Definition" + * + * This is a mapping of "URI Identifier Codes" to URI string prefixes, + * per section 3.2.2 of the NFC Forum URI Record Type Definition document. + */ + private static final BiMap URI_PREFIX_MAP = ImmutableBiMap.builder() + .put((byte) 0x00, "") + .put((byte) 0x01, "http://www.") + .put((byte) 0x02, "https://www.") + .put((byte) 0x03, "http://") + .put((byte) 0x04, "https://") + .put((byte) 0x05, "tel:") + .put((byte) 0x06, "mailto:") + .put((byte) 0x07, "ftp://anonymous:anonymous@") + .put((byte) 0x08, "ftp://ftp.") + .put((byte) 0x09, "ftps://") + .put((byte) 0x0A, "sftp://") + .put((byte) 0x0B, "smb://") + .put((byte) 0x0C, "nfs://") + .put((byte) 0x0D, "ftp://") + .put((byte) 0x0E, "dav://") + .put((byte) 0x0F, "news:") + .put((byte) 0x10, "telnet://") + .put((byte) 0x11, "imap:") + .put((byte) 0x12, "rtsp://") + .put((byte) 0x13, "urn:") + .put((byte) 0x14, "pop:") + .put((byte) 0x15, "sip:") + .put((byte) 0x16, "sips:") + .put((byte) 0x17, "tftp:") + .put((byte) 0x18, "btspp://") + .put((byte) 0x19, "btl2cap://") + .put((byte) 0x1A, "btgoep://") + .put((byte) 0x1B, "tcpobex://") + .put((byte) 0x1C, "irdaobex://") + .put((byte) 0x1D, "file://") + .put((byte) 0x1E, "urn:epc:id:") + .put((byte) 0x1F, "urn:epc:tag:") + .put((byte) 0x20, "urn:epc:pat:") + .put((byte) 0x21, "urn:epc:raw:") + .put((byte) 0x22, "urn:epc:") + .put((byte) 0x23, "urn:nfc:") + .build(); + + private final Uri mUri; + + private UriRecord(Uri uri) { + this.mUri = Preconditions.checkNotNull(uri); + } + + public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent, int offset) { + TextView text = (TextView) inflater.inflate(R.layout.tag_text, parent, false); + text.setText(mUri.toString()); + return text; + } + + public Uri getUri() { + return mUri; + } + + /** + * Convert {@link android.nfc.NdefRecord} into a {@link android.net.Uri}. + * This will handle both TNF_WELL_KNOWN / RTD_URI and TNF_ABSOLUTE_URI. + * + * @throws IllegalArgumentException if the NdefRecord is not a record + * containing a URI. + */ + public static UriRecord parse(NdefRecord record) { + short tnf = record.getTnf(); + if (tnf == NdefRecord.TNF_WELL_KNOWN) { + return parseWellKnown(record); + } else if (tnf == NdefRecord.TNF_ABSOLUTE_URI) { + return parseAbsolute(record); + } + throw new IllegalArgumentException("Unknown TNF " + tnf); + } + + /** Parse and absolute URI record */ + private static UriRecord parseAbsolute(NdefRecord record) { + byte[] payload = record.getPayload(); + Uri uri = Uri.parse(new String(payload, Charset.forName("UTF-8"))); + return new UriRecord(uri); + } + + /** Parse an well known URI record */ + private static UriRecord parseWellKnown(NdefRecord record) { + Preconditions.checkArgument(Arrays.equals(record.getType(), NdefRecord.RTD_URI)); + byte[] payload = record.getPayload(); + /* + * payload[0] contains the URI Identifier Code, per the + * NFC Forum "URI Record Type Definition" section 3.2.2. + * + * payload[1]...payload[payload.length - 1] contains the rest of + * the URI. + */ + String prefix = URI_PREFIX_MAP.get(payload[0]); + byte[] fullUri = + Bytes.concat(prefix.getBytes(Charset.forName("UTF-8")), Arrays.copyOfRange(payload, 1, + payload.length)); + Uri uri = Uri.parse(new String(fullUri, Charset.forName("UTF-8"))); + return new UriRecord(uri); + } + + public static boolean isUri(NdefRecord record) { + try { + parse(record); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + private static final byte[] EMPTY = new byte[0]; +} diff --git a/samples/NFCDemo/src/com/example/android/nfc/simulator/FakeTagsActivity.java b/samples/NFCDemo/src/com/example/android/nfc/simulator/FakeTagsActivity.java new file mode 100644 index 000000000..313bab499 --- /dev/null +++ b/samples/NFCDemo/src/com/example/android/nfc/simulator/FakeTagsActivity.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2010 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.example.android.nfc.simulator; + +import android.app.ListActivity; +import android.content.Intent; +import android.nfc.NdefMessage; +import android.nfc.NdefRecord; +import android.nfc.NfcAdapter; +import android.os.Bundle; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ListView; + +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import com.google.common.primitives.Bytes; + +import java.nio.charset.Charset; +import java.util.Locale; + +/** + * A activity that launches tags as if they had been scanned. + */ +public class FakeTagsActivity extends ListActivity { + + static final String TAG = "FakeTagsActivity"; + + static final byte[] UID = new byte[] {0x05, 0x00, 0x03, 0x08}; + + ArrayAdapter mAdapter; + + public static NdefRecord newTextRecord(String text, Locale locale, boolean encodeInUtf8) { + Preconditions.checkNotNull(text); + Preconditions.checkNotNull(locale); + final byte[] langBytes = locale.getLanguage().getBytes(Charsets.US_ASCII); + final Charset utfEncoding = encodeInUtf8 ? Charsets.UTF_8 : Charset.forName("UTF-16"); + final byte[] textBytes = text.getBytes(utfEncoding); + final int utfBit = encodeInUtf8 ? 0 : (1 << 7); + final char status = (char) (utfBit + langBytes.length); + final byte[] data = Bytes.concat(new byte[] {(byte) status}, langBytes, textBytes); + return new NdefRecord(NdefRecord.TNF_WELL_KNOWN, NdefRecord.RTD_TEXT, new byte[0], data); + } + + public static NdefRecord newMimeRecord(String type, byte[] data) { + Preconditions.checkNotNull(type); + Preconditions.checkNotNull(data); + final byte[] typeBytes = type.getBytes(Charsets.US_ASCII); + return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, typeBytes, new byte[0], data); + } + + static final class TagDescription { + + public String title; + + public NdefMessage[] msgs; + + public TagDescription(String title, byte[] bytes) { + this.title = title; + try { + msgs = new NdefMessage[] {new NdefMessage(bytes)}; + } catch (final Exception e) { + throw new RuntimeException("Failed to create tag description", e); + } + } + + @Override + public String toString() { + return title; + } + } + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + final ArrayAdapter adapter = new ArrayAdapter( + this, android.R.layout.simple_list_item_1, android.R.id.text1); + adapter.add( + new TagDescription("Broadcast NFC Text Tag", MockNdefMessages.ENGLISH_PLAIN_TEXT)); + adapter.add(new TagDescription( + "Broadcast NFC SmartPoster URL & text", MockNdefMessages.SMART_POSTER_URL_AND_TEXT)); + adapter.add(new TagDescription( + "Broadcast NFC SmartPoster URL", MockNdefMessages.SMART_POSTER_URL_NO_TEXT)); + setListAdapter(adapter); + mAdapter = adapter; + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + final TagDescription description = mAdapter.getItem(position); + final Intent intent = new Intent(NfcAdapter.ACTION_TAG_DISCOVERED); + intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, description.msgs); + startActivity(intent); + } +} diff --git a/samples/NFCDemo/src/com/example/android/nfc/simulator/MockNdefMessages.java b/samples/NFCDemo/src/com/example/android/nfc/simulator/MockNdefMessages.java new file mode 100644 index 000000000..52a122f7e --- /dev/null +++ b/samples/NFCDemo/src/com/example/android/nfc/simulator/MockNdefMessages.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010 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.example.android.nfc.simulator; + +/** + * This class provides a list of fake NFC Ndef format Tags. + */ +public class MockNdefMessages { + + /** + * A Smart Poster containing a URL and no text. + */ + public static final byte[] SMART_POSTER_URL_NO_TEXT = + new byte[] {(byte) 0xd1, (byte) 0x02, (byte) 0x0f, (byte) 0x53, (byte) 0x70, (byte) 0xd1, + (byte) 0x01, (byte) 0x0b, (byte) 0x55, (byte) 0x01, (byte) 0x67, (byte) 0x6f, + (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x2e, (byte) 0x63, + (byte) 0x6f, (byte) 0x6d}; + + /** + * A plain text tag in english. + */ + public static final byte[] ENGLISH_PLAIN_TEXT = + new byte[] {(byte) 0xd1, (byte) 0x01, (byte) 0x1c, (byte) 0x54, (byte) 0x02, (byte) 0x65, + (byte) 0x6e, (byte) 0x53, (byte) 0x6f, (byte) 0x6d, (byte) 0x65, (byte) 0x20, + (byte) 0x72, (byte) 0x61, (byte) 0x6e, (byte) 0x64, (byte) 0x6f, (byte) 0x6d, + (byte) 0x20, (byte) 0x65, (byte) 0x6e, (byte) 0x67, (byte) 0x6c, (byte) 0x69, + (byte) 0x73, (byte) 0x68, (byte) 0x20, (byte) 0x74, (byte) 0x65, (byte) 0x78, + (byte) 0x74, (byte) 0x2e}; + + /** + * Smart Poster containing a URL and Text. + */ + public static final byte[] SMART_POSTER_URL_AND_TEXT = + new byte[] {(byte) 0xd1, (byte) 0x02, (byte) 0x1c, (byte) 0x53, (byte) 0x70, (byte) 0x91, + (byte) 0x01, (byte) 0x09, (byte) 0x54, (byte) 0x02, (byte) 0x65, (byte) 0x6e, + (byte) 0x47, (byte) 0x6f, (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, + (byte) 0x51, (byte) 0x01, (byte) 0x0b, (byte) 0x55, (byte) 0x01, (byte) 0x67, + (byte) 0x6f, (byte) 0x6f, (byte) 0x67, (byte) 0x6c, (byte) 0x65, (byte) 0x2e, + (byte) 0x63, (byte) 0x6f, (byte) 0x6d}; + + /** + * All the mock Ndef tags. + */ + public static final byte[][] ALL_MOCK_MESSAGES = + new byte[][] {SMART_POSTER_URL_NO_TEXT, ENGLISH_PLAIN_TEXT, SMART_POSTER_URL_AND_TEXT}; +}