CTS Verifier NFC Tests

Bug 4539818

Adds the following tests:

- NDEF Push Sender - sends a specific NDEF message that another device
  running the NDEF Push Receiver will receive and validate

- NDEF Push Receiver - waits to recieve the message from the NDEF
  Push Sender and makes sure it matches

- NDEF and MifareUltralight Tag Verifiers - these tests write random
  data to tags and then prompts the user to scan them and checks that
  the data was properly written

Change-Id: I9c0604869a6027b475c056e79079a21c9760be91
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index 2713300..79f4e3e 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -20,10 +20,12 @@
       android:versionCode="2"
       android:versionName="2.3_r2">
       
-    <uses-sdk android:minSdkVersion="5"></uses-sdk>
+    <!-- Using 10 for more complete NFC support... -->
+    <uses-sdk android:minSdkVersion="10"></uses-sdk>
 
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+    <uses-permission android:name="android.permission.NFC" />
     <uses-permission android:name="android.permission.RECORD_AUDIO" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     
@@ -215,6 +217,29 @@
             <meta-data android:name="test_category" android:value="@string/test_category_features" />
         </activity>
 
+        <activity android:name=".nfc.NfcTestActivity"
+                android:label="@string/nfc_test"
+                android:configChanges="keyboardHidden|orientation">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/test_category_hardware" />
+            <meta-data android:name="test_required_features" android:value="android.hardware.nfc" />
+        </activity>
+
+        <activity android:name=".nfc.NdefPushSenderActivity"
+                android:label="@string/nfc_ndef_push_sender"
+                android:configChanges="keyboardHidden|orientation" />
+
+        <activity android:name=".nfc.NdefPushReceiverActivity"
+                android:label="@string/nfc_ndef_push_receiver"
+                android:configChanges="keyboardHidden|orientation" />
+
+        <activity android:name=".nfc.TagVerifierActivity"
+                android:label="@string/nfc_tag_verifier"
+                android:configChanges="keyboardHidden|orientation" />
+
         <activity android:name=".sensors.AccelerometerTestActivity" android:label="@string/snsr_accel_test"
                 android:screenOrientation="nosensor">
             <intent-filter>
diff --git a/apps/CtsVerifier/res/layout/nfc_tag.xml b/apps/CtsVerifier/res/layout/nfc_tag.xml
new file mode 100644
index 0000000..64da622
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/nfc_tag.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        >
+
+    <ListView android:id="@id/android:list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            />
+
+    <TextView android:id="@id/android:empty"
+            android:gravity="center"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            style="@style/InstructionsFont"
+            />
+
+    <include layout="@layout/pass_fail_buttons" />
+
+</LinearLayout>
diff --git a/apps/CtsVerifier/res/layout/pass_fail_text.xml b/apps/CtsVerifier/res/layout/pass_fail_text.xml
new file mode 100644
index 0000000..432f26d
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/pass_fail_text.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        >
+
+    <ScrollView android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            >
+        <TextView android:id="@+id/text"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                style="@style/InstructionsFont"
+                />
+    </ScrollView>
+
+    <include layout="@layout/pass_fail_buttons" />
+
+</LinearLayout>
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index 4b39842..19cfcaf 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -27,6 +27,7 @@
     <string name="test_list_title">Manual Test List</string>
     <string name="test_category_audio">Audio</string>
     <string name="test_category_device_admin">Device Administration</string>
+    <string name="test_category_hardware">Hardware</string>
     <string name="test_category_networking">Networking</string>
     <string name="test_category_sensors">Sensors</string>
     <string name="test_category_security">Security</string>
@@ -192,6 +193,67 @@
 
     <string name="empty"></string>
 
+    <!-- Strings for NfcTestActivity -->
+    <string name="nfc_test">NFC Test</string>
+    <string name="nfc_test_info">The Peer-to-Peer Data Exchange tests require two devices with
+        NFC enabled to exchange messages. One device must be the candidate device running the
+        software build to be tested, while the other device must be an implementation already
+        known to be compatible.\n\nThe Tag Verification tests check that your
+        device can properly read and write to tags of different technologies. The MIFARE
+        Ultralight test is only applicable for devices that support it.
+    </string>
+
+    <string name="nfc_not_enabled">NFC is not enabled!</string>
+    <string name="nfc_not_enabled_message">These tests require NFC to be enabled. Click the 
+        button below to goto Settings and enable it.</string>
+    <string name="nfc_settings">NFC Settings</string>
+
+    <string name="nfc_pee_2_pee">Peer-to-Peer Data Exchange</string>
+    <string name="nfc_ndef_push_sender">NDEF Push Sender</string>
+    <string name="nfc_ndef_push_receiver">NDEF Push Receiver</string>
+
+    <string name="nfc_tag_verification">Tag Verification</string>
+    <string name="nfc_ndef">NDEF</string>
+    <string name="nfc_mifare_ultralight">MIFARE Ultralight</string>
+
+    <string name="nfc_ndef_push_sender_info">Start the \"CTS Verifier NDEF Receiver\" test on
+        another device and touch the devices back to back. The receiver should show a
+        dialog indicating it has successfully received the correct message!</string>
+    <string name="nfc_ndef_push_sender_instructions">Touch this device to the back of another
+        device running the \"CTS Verifier NDEF Receiver\"...</string>
+
+    <string name="nfc_ndef_push_receiver_info">Start the \"CTS Verifier NDEF Sender\" test on
+        another device and touch the devices back to back. The receiver should show a
+        dialog indicating it has successfully received the correct message!</string>
+    <string name="nfc_ndef_push_receiver_instructions">Touch this device to the back of another
+        device running the \"CTS Verifier NDEF Sender\"...</string>
+    <string name="nfc_ndef_push_receive_success">Successfully received the correct NDEF push
+        message.</string>
+    <string name="nfc_ndef_push_receive_failure">Failed to receive the correct NDEF push 
+        message.</string>
+
+    <string name="nfc_tag_verifier">NFC Tag Verifier</string>
+    <string name="nfc_tag_verifier_info">Follow the on-screen instructions to write and read
+        a tag of the chosen technology.</string>
+
+    <string name="nfc_scan_tag">Place device on a writable %s tag...</string>
+    <string name="nfc_write_tag_title">Writable tag discovered!</string>
+    <string name="nfc_write_tag_message">Press OK to write to this tag...</string>
+    <string name="nfc_scan_tag_again">Tap the same %s tag again to confirm that its contents match...</string>
+    <string name="nfc_wrong_tag_title">Wrong type of tag scanned</string>
+    <string name="nfc_no_tech">No tag technologies detected...</string>
+
+    <string name="nfc_writing_tag">Writing NFC tag...</string>
+    <string name="nfc_writing_tag_error">Error writing NFC tag...</string>
+    <string name="nfc_reading_tag">Reading NFC tag...</string>
+    <string name="nfc_reading_tag_error">Error reading NFC tag...</string>
+
+    <string name="nfc_result_success">Test passed!</string>
+    <string name="nfc_result_failure">Test failed!</string>
+
+    <string name="nfc_result_message">Written data:\n%1$s\n\nRead data:\n%2$s</string>
+    <string name="nfc_ndef_content">Id: %1$s\nMime: %2$s\nPayload: %3$s</string>
+
     <!-- Strings for AccelerometerTestActivity and GyroscopeTestActivity -->
     <string name="snsr_accel_test">Accelerometer Test</string>
     <string name="snsr_accel_test_info">This test verifies that the accelerometer is working properly. As you move the device around through space, the triangle should always point down (i.e. in the direction of gravity.) If it does not, the accelerometer is improperly configured.</string>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
index 52d9b32..2cc79fb 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
@@ -83,11 +83,20 @@
         /** Features necessary to run this test. */
         final String[] requiredFeatures;
 
+        public static TestListItem newTest(Context context, int titleResId, String testName,
+                Intent intent, String[] requiredFeatures) {
+            return newTest(context.getString(titleResId), testName, intent, requiredFeatures);
+        }
+
         public static TestListItem newTest(String title, String testName, Intent intent,
                 String[] requiredFeatures) {
             return new TestListItem(title, testName, intent, requiredFeatures);
         }
 
+        public static TestListItem newCategory(Context context, int titleResId) {
+            return newCategory(context.getString(titleResId));
+        }
+
         public static TestListItem newCategory(String title) {
             return new TestListItem(title, null, null, null);
         }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushReceiverActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushReceiverActivity.java
new file mode 100644
index 0000000..f976a45
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushReceiverActivity.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2011 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.android.cts.verifier.nfc;
+
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.nfc.tech.NfcUtils;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.nfc.NdefMessage;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcManager;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.widget.TextView;
+
+/**
+ * Test activity that waits to receive a particular NDEF Push message from another NFC device.
+ */
+public class NdefPushReceiverActivity extends PassFailButtons.Activity {
+
+    private static final int NFC_NOT_ENABLED_DIALOG_ID = 1;
+
+    private static final int RESULT_DIALOG_ID = 2;
+
+    private static final String IS_MATCH_ARG = "isMatch";
+
+    private NfcAdapter mNfcAdapter;
+
+    private PendingIntent mPendingIntent;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.pass_fail_text);
+        setInfoResources(R.string.nfc_ndef_push_receiver, R.string.nfc_ndef_push_receiver_info, 0);
+        setPassFailButtonClickListeners();
+        getPassButton().setEnabled(false);
+
+        TextView text = (TextView) findViewById(R.id.text);
+        text.setText(R.string.nfc_ndef_push_receiver_instructions);
+
+        NfcManager nfcManager = (NfcManager) getSystemService(NFC_SERVICE);
+        mNfcAdapter = nfcManager.getDefaultAdapter();
+        mPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass())
+                .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
+
+        if (!mNfcAdapter.isEnabled()) {
+            showDialog(NFC_NOT_ENABLED_DIALOG_ID);
+        }
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, null, null);
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        mNfcAdapter.disableForegroundDispatch(this);
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        super.onNewIntent(intent);
+
+        NdefMessage[] messages = getNdefMessages(intent);
+        boolean isMatch = messages != null
+                && messages.length > 0
+                && NfcUtils.areMessagesEqual(messages[0], NdefPushSenderActivity.TEST_MESSAGE);
+
+        getPassButton().setEnabled(isMatch);
+
+        Bundle args = new Bundle();
+        args.putBoolean(IS_MATCH_ARG, isMatch);
+        showDialog(RESULT_DIALOG_ID, args);
+    }
+
+    private NdefMessage[] getNdefMessages(Intent intent) {
+        Parcelable[] rawMessages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
+        if (rawMessages != null) {
+            NdefMessage[] messages = new NdefMessage[rawMessages.length];
+            for (int i = 0; i < messages.length; i++) {
+                messages[i] = (NdefMessage) rawMessages[i];
+            }
+            return messages;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public Dialog onCreateDialog(int id, Bundle args) {
+        switch (id) {
+            case NFC_NOT_ENABLED_DIALOG_ID:
+                return NfcDialogs.createNotEnabledDialog(this);
+
+            case RESULT_DIALOG_ID:
+                // Set placeholder titles and messages for now. Final titles and messages will
+                // be set in onPrepareDialog.
+                return new AlertDialog.Builder(this)
+                        .setIcon(android.R.drawable.ic_dialog_info)
+                        .setTitle(R.string.nfc_result_failure)
+                        .setMessage("")
+                        .setPositiveButton(android.R.string.ok, null)
+                        .show();
+
+            default:
+                return super.onCreateDialog(id, args);
+        }
+    }
+
+    @Override
+    protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
+        switch (id) {
+            case RESULT_DIALOG_ID:
+                boolean isMatch = args.getBoolean(IS_MATCH_ARG);
+                AlertDialog alert = (AlertDialog) dialog;
+                alert.setTitle(isMatch
+                        ? R.string.nfc_result_success
+                        : R.string.nfc_result_failure);
+                alert.setMessage(isMatch
+                        ? getString(R.string.nfc_ndef_push_receive_success)
+                        : getString(R.string.nfc_ndef_push_receive_failure));
+                break;
+
+            default:
+                super.onPrepareDialog(id, dialog, args);
+                break;
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushSenderActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushSenderActivity.java
new file mode 100644
index 0000000..2a507d3
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushSenderActivity.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2011 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.android.cts.verifier.nfc;
+
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+
+import android.app.Dialog;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcManager;
+import android.os.Bundle;
+import android.widget.TextView;
+
+import java.nio.charset.Charset;
+
+/**
+ * Test activity that sends a particular NDEF Push message to another NFC device.
+ */
+public class NdefPushSenderActivity extends PassFailButtons.Activity {
+
+    static final NdefMessage TEST_MESSAGE = getTestMessage();
+
+    private static final int NFC_NOT_ENABLED_DIALOG_ID = 1;
+
+    private NfcAdapter mNfcAdapter;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.pass_fail_text);
+        setInfoResources(R.string.nfc_ndef_push_sender, R.string.nfc_ndef_push_sender_info, 0);
+        setPassFailButtonClickListeners();
+
+        TextView text = (TextView) findViewById(R.id.text);
+        text.setText(R.string.nfc_ndef_push_sender_instructions);
+
+        NfcManager nfcManager = (NfcManager) getSystemService(NFC_SERVICE);
+        mNfcAdapter = nfcManager.getDefaultAdapter();
+
+        if (!mNfcAdapter.isEnabled()) {
+            showDialog(NFC_NOT_ENABLED_DIALOG_ID);
+        }
+    }
+
+    private static NdefMessage getTestMessage() {
+        byte[] mimeBytes = "application/com.android.cts.verifier.nfc"
+                .getBytes(Charset.forName("US-ASCII"));
+        byte[] id = new byte[] {1, 3, 3, 7};
+        byte[] payload = "CTS Verifier NDEF Push Tag".getBytes(Charset.forName("US-ASCII"));
+        return new NdefMessage(new NdefRecord[] {
+                new NdefRecord(NdefRecord.TNF_MIME_MEDIA, mimeBytes, id, payload)
+        });
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mNfcAdapter.enableForegroundNdefPush(this, TEST_MESSAGE);
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        mNfcAdapter.disableForegroundNdefPush(this);
+    }
+
+    @Override
+    public Dialog onCreateDialog(int id, Bundle args) {
+        switch (id) {
+            case NFC_NOT_ENABLED_DIALOG_ID:
+                return NfcDialogs.createNotEnabledDialog(this);
+
+            default:
+                return super.onCreateDialog(id, args);
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NfcDialogs.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NfcDialogs.java
new file mode 100644
index 0000000..1130f2d
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NfcDialogs.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2011 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.android.cts.verifier.nfc;
+
+import com.android.cts.verifier.R;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.provider.Settings;
+
+/** Class containing methods to create common dialogs for NFC activities. */
+class NfcDialogs {
+
+    static AlertDialog createNotEnabledDialog(final Context context) {
+        return new AlertDialog.Builder(context)
+                .setIcon(android.R.drawable.ic_dialog_alert)
+                .setTitle(R.string.nfc_not_enabled)
+                .setMessage(R.string.nfc_not_enabled_message)
+                .setPositiveButton(R.string.nfc_settings, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        Intent intent = new Intent(Settings.ACTION_WIRELESS_SETTINGS);
+                        context.startActivity(intent);
+                    }
+                })
+                .create();
+    }
+
+    private NfcDialogs() {
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NfcTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NfcTestActivity.java
new file mode 100644
index 0000000..3a85860
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NfcTestActivity.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2011 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.android.cts.verifier.nfc;
+
+import com.android.cts.verifier.ArrayTestListAdapter;
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.TestListAdapter.TestListItem;
+
+import android.content.Intent;
+import android.nfc.tech.MifareUltralight;
+import android.nfc.tech.Ndef;
+import android.nfc.tech.TagTechnology;
+import android.os.Bundle;
+
+/** Activity that lists all the NFC tests. */
+public class NfcTestActivity extends PassFailButtons.TestListActivity {
+
+    private static final String NDEF_ID =
+            TagVerifierActivity.getTagTestId(Ndef.class);
+
+    private static final String MIFARE_ULTRALIGHT_ID =
+            TagVerifierActivity.getTagTestId(MifareUltralight.class);
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.pass_fail_list);
+        setInfoResources(R.string.nfc_test, R.string.nfc_test_info, 0);
+        setPassFailButtonClickListeners();
+
+        ArrayTestListAdapter adapter = new ArrayTestListAdapter(this);
+
+        adapter.add(TestListItem.newCategory(this, R.string.nfc_pee_2_pee));
+        adapter.add(TestListItem.newTest(this, R.string.nfc_ndef_push_sender,
+                NdefPushSenderActivity.class.getName(),
+                new Intent(this, NdefPushSenderActivity.class), null));
+        adapter.add(TestListItem.newTest(this, R.string.nfc_ndef_push_receiver,
+                NdefPushReceiverActivity.class.getName(),
+                new Intent(this, NdefPushReceiverActivity.class), null));
+
+        adapter.add(TestListItem.newCategory(this, R.string.nfc_tag_verification));
+        adapter.add(TestListItem.newTest(this, R.string.nfc_ndef,
+                NDEF_ID, getTagIntent(Ndef.class), null));
+        adapter.add(TestListItem.newTest(this, R.string.nfc_mifare_ultralight,
+                MIFARE_ULTRALIGHT_ID, getTagIntent(MifareUltralight.class), null));
+
+        setTestListAdapter(adapter);
+    }
+
+    private Intent getTagIntent(Class<? extends TagTechnology> primaryTech) {
+        return new Intent(this, TagVerifierActivity.class)
+                .putExtra(TagVerifierActivity.EXTRA_TECH, primaryTech.getName());
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/TagVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/TagVerifierActivity.java
new file mode 100644
index 0000000..082c143
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/TagVerifierActivity.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2011 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.android.cts.verifier.nfc;
+
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.nfc.tech.MifareUltralightTagTester;
+import com.android.cts.verifier.nfc.tech.NdefTagTester;
+import com.android.cts.verifier.nfc.tech.TagTester;
+import com.android.cts.verifier.nfc.tech.TagVerifier;
+import com.android.cts.verifier.nfc.tech.TagVerifier.Result;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.PendingIntent;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcManager;
+import android.nfc.Tag;
+import android.nfc.tech.MifareUltralight;
+import android.nfc.tech.Ndef;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import android.widget.Toast;
+
+/**
+ * Test activity for reading and writing NFC tags using different technologies.
+ * First, it asks the user to write some random data to the tag. Then it asks
+ * the user to scan that tag again to verify that the data was properly written
+ * and read back.
+ */
+public class TagVerifierActivity<T> extends PassFailButtons.ListActivity {
+
+    static final String TAG = TagVerifierActivity.class.getSimpleName();
+
+    /** Non-optional argument specifying the tag technology to be used to read and write tags. */
+    static final String EXTRA_TECH = "tech";
+
+    private static final int NFC_NOT_ENABLED_DIALOG_ID = 1;
+    private static final int TESTABLE_TAG_DISCOVERED_DIALOG_ID = 2;
+    private static final int TESTABLE_TAG_REMINDER_DIALOG_ID = 3;
+    private static final int WRITE_PROGRESS_DIALOG_ID = 4;
+    private static final int READ_PROGRESS_DIALOG_ID = 5;
+    private static final int VERIFY_RESULT_DIALOG_ID = 6;
+
+    // Arguments used for the dialog showing what was written to the tag and read from the tag.
+    private static final String EXPECTED_CONTENT_ID = "expectedContent";
+    private static final String ACTUAL_CONTENT_ID = "actualContent";
+    private static final String IS_MATCH_ID = "isMatch";
+
+    // The test activity has two states - writing data to a tag and then verifying it.
+    private static final int WRITE_STEP = 0;
+    private static final int VERIFY_STEP = 1;
+
+    private NfcAdapter mNfcAdapter;
+    private PendingIntent mPendingIntent;
+    private Class<?> mTechClass;
+
+    private int mStep;
+    private TagTester mTagTester;
+    private TagVerifier mTagVerifier;
+    private Tag mTag;
+    private ArrayAdapter<String> mTechListAdapter;
+    private TextView mEmptyText;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.nfc_tag);
+        setInfoResources(R.string.nfc_tag_verifier, R.string.nfc_tag_verifier_info, 0);
+        setPassFailButtonClickListeners();
+        getPassButton().setEnabled(false);
+
+        parseIntentExtras();
+        if (mTechClass != null) {
+            mTagTester = getTagTester(mTechClass);
+
+            mEmptyText = (TextView) findViewById(android.R.id.empty);
+
+            mTechListAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
+            setListAdapter(mTechListAdapter);
+
+            NfcManager nfcManager = (NfcManager) getSystemService(NFC_SERVICE);
+            mNfcAdapter = nfcManager.getDefaultAdapter();
+            mPendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, getClass())
+                    .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0);
+
+            if (!mNfcAdapter.isEnabled()) {
+                showDialog(NFC_NOT_ENABLED_DIALOG_ID);
+            }
+
+            goToWriteStep();
+        } else {
+            finish();
+        }
+    }
+
+    private void parseIntentExtras() {
+        try {
+            String tech = getIntent().getStringExtra(EXTRA_TECH);
+            if (tech != null) {
+                mTechClass = Class.forName(tech);
+            }
+        } catch (ClassNotFoundException e) {
+            Log.e(TAG, "Couldn't find tech for class", e);
+        }
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mNfcAdapter.enableForegroundDispatch(this, mPendingIntent, null, null);
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        mNfcAdapter.disableForegroundDispatch(this);
+    }
+
+    private TagTester getTagTester(Class<?> techClass) {
+        if (Ndef.class.equals(techClass)) {
+            return new NdefTagTester(this);
+        } else if (MifareUltralight.class.equals(techClass)) {
+            return new MifareUltralightTagTester();
+        } else {
+            throw new IllegalArgumentException("Unsupported technology: " + techClass);
+        }
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        Tag tag = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
+        if (tag != null) {
+            mTag = tag;
+            updateTechListAdapter(tag);
+            switch (mStep) {
+                case WRITE_STEP:
+                    handleWriteStep(tag);
+                    break;
+
+                case VERIFY_STEP:
+                    handleVerifyStep();
+                    break;
+            }
+        }
+    }
+
+    private void handleWriteStep(Tag tag) {
+        if (mTagTester.isTestableTag(tag)) {
+            brutallyDismissDialog(TESTABLE_TAG_REMINDER_DIALOG_ID);
+            showDialog(TESTABLE_TAG_DISCOVERED_DIALOG_ID);
+        } else {
+            brutallyDismissDialog(TESTABLE_TAG_DISCOVERED_DIALOG_ID);
+            showDialog(TESTABLE_TAG_REMINDER_DIALOG_ID);
+        }
+    }
+
+    private void brutallyDismissDialog(int id) {
+        try {
+            dismissDialog(id);
+        } catch (IllegalArgumentException e) {
+            // Don't care if it hasn't been shown before...
+        }
+    }
+
+    private void handleVerifyStep() {
+        new VerifyTagTask().execute(mTag);
+    }
+
+    private void updateTechListAdapter(Tag tag) {
+        mEmptyText.setText(R.string.nfc_no_tech);
+        String[] techList = tag.getTechList();
+        mTechListAdapter.clear();
+        for (String tech : techList) {
+            mTechListAdapter.add(tech);
+        }
+    }
+
+    class WriteTagTask extends AsyncTask<Tag, Void, TagVerifier> {
+
+        @Override
+        protected void onPreExecute() {
+            super.onPreExecute();
+            showDialog(WRITE_PROGRESS_DIALOG_ID);
+        }
+
+        @Override
+        protected TagVerifier doInBackground(Tag... tags) {
+            try {
+                return mTagTester.writeTag(tags[0]);
+            } catch (Exception e) {
+                Log.e(TAG, "Error writing NFC tag...", e);
+                return null;
+            }
+        }
+
+        @Override
+        protected void onPostExecute(TagVerifier tagVerifier) {
+            dismissDialog(WRITE_PROGRESS_DIALOG_ID);
+            mTagVerifier = tagVerifier;
+            if (tagVerifier != null) {
+                goToVerifyStep();
+            } else {
+                Toast.makeText(TagVerifierActivity.this, R.string.nfc_writing_tag_error,
+                        Toast.LENGTH_SHORT).show();
+                goToWriteStep();
+            }
+        }
+    }
+
+    private void goToWriteStep() {
+        mStep = WRITE_STEP;
+        mEmptyText.setText(getString(R.string.nfc_scan_tag, mTechClass.getSimpleName()));
+        mTechListAdapter.clear();
+    }
+
+    private void goToVerifyStep() {
+        mStep = VERIFY_STEP;
+        mEmptyText.setText(getString(R.string.nfc_scan_tag_again, mTechClass.getSimpleName()));
+        mTechListAdapter.clear();
+    }
+
+    class VerifyTagTask extends AsyncTask<Tag, Void, Result> {
+
+        @Override
+        protected void onPreExecute() {
+            super.onPreExecute();
+            showDialog(READ_PROGRESS_DIALOG_ID);
+        }
+
+        @Override
+        protected Result doInBackground(Tag... tags) {
+            try {
+                return mTagVerifier.verifyTag(tags[0]);
+            } catch (Exception e) {
+                Log.e(TAG, "Error verifying NFC tag...", e);
+                return null;
+            }
+        }
+
+        @Override
+        protected void onPostExecute(Result result) {
+            super.onPostExecute(result);
+            dismissDialog(READ_PROGRESS_DIALOG_ID);
+            mTagVerifier = null;
+            if (result != null) {
+                getPassButton().setEnabled(result.isMatch());
+
+                Bundle args = new Bundle();
+                args.putCharSequence(EXPECTED_CONTENT_ID, result.getExpectedContent());
+                args.putCharSequence(ACTUAL_CONTENT_ID, result.getActualContent());
+                args.putBoolean(IS_MATCH_ID, result.isMatch());
+                showDialog(VERIFY_RESULT_DIALOG_ID, args);
+
+                goToWriteStep();
+            } else {
+                Toast.makeText(TagVerifierActivity.this, R.string.nfc_reading_tag_error,
+                        Toast.LENGTH_SHORT).show();
+                goToWriteStep();
+            }
+        }
+    }
+
+    @Override
+    public Dialog onCreateDialog(int id, Bundle args) {
+        switch (id) {
+            case NFC_NOT_ENABLED_DIALOG_ID:
+                return NfcDialogs.createNotEnabledDialog(this);
+
+            case TESTABLE_TAG_DISCOVERED_DIALOG_ID:
+                return createTestableTagDiscoveredDialog();
+
+            case TESTABLE_TAG_REMINDER_DIALOG_ID:
+                return createTestableTagReminderDialog();
+
+            case WRITE_PROGRESS_DIALOG_ID:
+                return createWriteProgressDialog();
+
+            case READ_PROGRESS_DIALOG_ID:
+                return createReadProgressDialog();
+
+            case VERIFY_RESULT_DIALOG_ID:
+                return createVerifyResultDialog();
+
+            default:
+                return super.onCreateDialog(id, args);
+        }
+    }
+
+    private AlertDialog createTestableTagDiscoveredDialog() {
+        return new AlertDialog.Builder(this)
+                .setIcon(android.R.drawable.ic_dialog_info)
+                .setTitle(R.string.nfc_write_tag_title)
+                .setMessage(R.string.nfc_write_tag_message)
+                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        new WriteTagTask().execute(mTag);
+                    }
+                })
+                .setOnCancelListener(new DialogInterface.OnCancelListener() {
+                    @Override
+                    public void onCancel(DialogInterface dialog) {
+                        goToWriteStep();
+                    }
+                })
+                .show();
+    }
+
+    private AlertDialog createTestableTagReminderDialog() {
+        return new AlertDialog.Builder(this)
+                .setIcon(android.R.drawable.ic_dialog_alert)
+                .setTitle(R.string.nfc_wrong_tag_title)
+                .setMessage(getString(R.string.nfc_scan_tag, mTechClass.getSimpleName()))
+                .setPositiveButton(android.R.string.ok, null)
+                .show();
+    }
+
+    private ProgressDialog createWriteProgressDialog() {
+        ProgressDialog dialog = new ProgressDialog(this);
+        dialog.setMessage(getString(R.string.nfc_writing_tag));
+        return dialog;
+    }
+
+    private ProgressDialog createReadProgressDialog() {
+        ProgressDialog dialog = new ProgressDialog(this);
+        dialog.setMessage(getString(R.string.nfc_reading_tag));
+        return dialog;
+    }
+
+    private AlertDialog createVerifyResultDialog() {
+        // Placeholder title and message that will be set properly in onPrepareDialog
+        return new AlertDialog.Builder(this)
+                .setIcon(android.R.drawable.ic_dialog_alert)
+                .setTitle(R.string.nfc_result_failure)
+                .setMessage("")
+                .setPositiveButton(android.R.string.ok, null)
+                .create();
+    }
+
+    @Override
+    protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
+        switch (id) {
+            case VERIFY_RESULT_DIALOG_ID:
+                prepareVerifyResultDialog(dialog, args);
+                break;
+
+            default:
+                super.onPrepareDialog(id, dialog, args);
+                break;
+        }
+    }
+
+    private void prepareVerifyResultDialog(Dialog dialog, Bundle args) {
+        CharSequence expectedContent = args.getCharSequence(EXPECTED_CONTENT_ID);
+        CharSequence actualContent = args.getCharSequence(ACTUAL_CONTENT_ID);
+        boolean isMatch = args.getBoolean(IS_MATCH_ID);
+
+        AlertDialog alert = (AlertDialog) dialog;
+        alert.setTitle(isMatch
+                ? R.string.nfc_result_success
+                : R.string.nfc_result_failure);
+        alert.setMessage(getString(R.string.nfc_result_message, expectedContent, actualContent));
+    }
+
+    @Override
+    public String getTestId() {
+        return getTagTestId(mTechClass);
+    }
+
+    static String getTagTestId(Class<?> primaryTech) {
+        return NfcTestActivity.class.getName() + "_" + primaryTech.getSimpleName();
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/MifareUltralightTagTester.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/MifareUltralightTagTester.java
new file mode 100644
index 0000000..23f4762
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/MifareUltralightTagTester.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2011 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.android.cts.verifier.nfc.tech;
+
+import android.nfc.Tag;
+import android.nfc.tech.MifareUltralight;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * {@link TagTester} for MIFARE Ultralight tags. It writes random bytes to the
+ * tag's first user page and verifies that it matches when scanned later.
+ */
+public class MifareUltralightTagTester implements TagTester {
+
+    private static final int USER_PAGE_OFFSET = 4;
+
+    private static final int NUM_PAGES = 4;
+
+    @Override
+    public boolean isTestableTag(Tag tag) {
+        if (tag != null) {
+            for (String tech : tag.getTechList()) {
+                if (tech.equals(MifareUltralight.class.getName())) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public TagVerifier writeTag(Tag tag) throws IOException {
+        Random random = new Random();
+        MifareUltralight ultralight = MifareUltralight.get(tag);
+        ultralight.connect();
+
+        final byte[] fourPages = new byte[NUM_PAGES * MifareUltralight.PAGE_SIZE];
+        byte[] onePage = new byte[MifareUltralight.PAGE_SIZE];
+        for (int i = 0; i < NUM_PAGES; i++) {
+            random.nextBytes(onePage);
+            System.arraycopy(onePage, 0, fourPages, i * onePage.length, onePage.length);
+            ultralight.writePage(USER_PAGE_OFFSET + i, onePage);
+        }
+
+        final CharSequence expectedContent = NfcUtils.displayByteArray(fourPages);
+
+        return new TagVerifier() {
+            @Override
+            public Result verifyTag(Tag tag) throws IOException {
+                MifareUltralight ultralight = MifareUltralight.get(tag);
+                if (ultralight != null) {
+                    ultralight.connect();
+                    byte[] actualFourPages = ultralight.readPages(USER_PAGE_OFFSET);
+                    CharSequence actualContent = NfcUtils.displayByteArray(actualFourPages);
+                    return new Result(expectedContent, actualContent,
+                            Arrays.equals(fourPages, actualFourPages));
+                } else {
+                    return new Result(expectedContent, null, false);
+                }
+            }
+        };
+    }
+}
\ No newline at end of file
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/NdefTagTester.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/NdefTagTester.java
new file mode 100644
index 0000000..668e526
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/NdefTagTester.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2011 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.android.cts.verifier.nfc.tech;
+
+import com.android.cts.verifier.R;
+
+import android.content.Context;
+import android.nfc.FormatException;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.Tag;
+import android.nfc.tech.Ndef;
+import android.util.Log;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Random;
+
+/**
+ * {@link TagTester} for NDEF tags. It writes a semi-random NDEF tag with a random id but
+ * constant mime type and payload.
+ */
+public class NdefTagTester implements TagTester {
+
+    private static final String TAG = NdefTagTester.class.getSimpleName();
+
+    private static final String MIME_TYPE = "application/com.android.cts.verifier.nfc";
+
+    private static final String PAYLOAD = "CTS Verifier NDEF Tag";
+
+    private final Context mContext;
+
+    public NdefTagTester(Context context) {
+        this.mContext = context;
+    }
+
+    @Override
+    public boolean isTestableTag(Tag tag) {
+        if (tag != null) {
+            for (String tech : tag.getTechList()) {
+                if (tech.equals(Ndef.class.getName())) {
+                    Ndef ndef = Ndef.get(tag);
+                    return ndef != null && ndef.isWritable();
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public TagVerifier writeTag(Tag tag) throws IOException, FormatException {
+        Random random = new Random();
+        NdefRecord mimeRecord = createRandomMimeRecord(random);
+        NdefRecord[] expectedRecords = new NdefRecord[] {mimeRecord};
+
+        final NdefMessage expectedMessage = new NdefMessage(expectedRecords);
+        writeMessage(tag, expectedMessage);
+
+        final String expectedContent = mContext.getString(R.string.nfc_ndef_content,
+                NfcUtils.displayByteArray(mimeRecord.getId()), MIME_TYPE, PAYLOAD);
+
+        return new TagVerifier() {
+            @Override
+            public Result verifyTag(Tag tag) throws IOException, FormatException {
+                String actualContent;
+                NdefMessage message = readMessage(tag);
+                NdefRecord[] records = message.getRecords();
+
+                if (records.length > 0) {
+                    NdefRecord record = records[0];
+                    actualContent = mContext.getString(R.string.nfc_ndef_content,
+                            NfcUtils.displayByteArray(record.getId()),
+                            new String(record.getType(), Charset.forName("US-ASCII")),
+                            new String(record.getPayload(), Charset.forName("US-ASCII")));
+                } else {
+                    actualContent = null;
+                }
+
+                return new Result(expectedContent, actualContent,
+                        NfcUtils.areMessagesEqual(message, expectedMessage));
+            }
+        };
+    }
+
+    private NdefRecord createRandomMimeRecord(Random random) {
+        byte[] mimeBytes = MIME_TYPE.getBytes(Charset.forName("US-ASCII"));
+        byte[] id = new byte[4];
+        random.nextBytes(id);
+        byte[] payload = PAYLOAD.getBytes(Charset.forName("US-ASCII"));
+        return new NdefRecord(NdefRecord.TNF_MIME_MEDIA, mimeBytes, id, payload);
+    }
+
+    private void writeMessage(Tag tag, NdefMessage message) throws IOException, FormatException {
+        Ndef ndef = null;
+        try {
+            ndef = Ndef.get(tag);
+            ndef.connect();
+            ndef.writeNdefMessage(message);
+        } finally {
+            if (ndef != null) {
+                try {
+                    ndef.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "IOException while closing NDEF...", e);
+                }
+            }
+        }
+    }
+
+    private NdefMessage readMessage(Tag tag) throws IOException, FormatException {
+        Ndef ndef = null;
+        try {
+            ndef = Ndef.get(tag);
+            if (ndef != null) {
+                ndef.connect();
+                return ndef.getNdefMessage();
+            }
+        } finally {
+            if (ndef != null) {
+                try {
+                    ndef.close();
+                } catch (IOException e) {
+                    Log.e(TAG, "Error closing Ndef...", e);
+                }
+            }
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/NfcUtils.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/NfcUtils.java
new file mode 100644
index 0000000..80e3989
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/NfcUtils.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2011 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.android.cts.verifier.nfc.tech;
+
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+
+import java.util.Arrays;
+
+/** Class with utility methods for testing equality of messages and displaying byte payloads. */
+public class NfcUtils {
+
+    public static boolean areMessagesEqual(NdefMessage message, NdefMessage otherMessage) {
+        return message != null && otherMessage != null
+                && areRecordArraysEqual(message.getRecords(), otherMessage.getRecords());
+    }
+
+    private static boolean areRecordArraysEqual(NdefRecord[] records, NdefRecord[] otherRecords) {
+        if (records.length == otherRecords.length) {
+            for (int i = 0; i < records.length; i++) {
+                if (!areRecordsEqual(records[i], otherRecords[i])) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private static boolean areRecordsEqual(NdefRecord record, NdefRecord otherRecord) {
+        return Arrays.equals(record.toByteArray(), otherRecord.toByteArray());
+    }
+
+    static CharSequence displayByteArray(byte[] bytes) {
+        StringBuilder builder = new StringBuilder().append("[");
+        for (int i = 0; i < bytes.length; i++) {
+            builder.append(Byte.toString(bytes[i]));
+            if (i + 1 < bytes.length) {
+                builder.append(", ");
+            }
+        }
+        return builder.append("]");
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/TagTester.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/TagTester.java
new file mode 100644
index 0000000..d0dd67c
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/TagTester.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 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.android.cts.verifier.nfc.tech;
+
+import android.nfc.Tag;
+
+/** Tag tester that writes data to the tag and returns a way to confirm a successful write. */
+public interface TagTester {
+
+    /** @return true if the tag is testable by this {@link TagTester} */
+    boolean isTestableTag(Tag tag);
+
+    /** Writes some data to the tag and returns a {@link TagVerifier} to confirm it. */
+    TagVerifier writeTag(Tag tag) throws Exception;
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/TagVerifier.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/TagVerifier.java
new file mode 100644
index 0000000..8c55610
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/tech/TagVerifier.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2011 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.android.cts.verifier.nfc.tech;
+
+import android.nfc.FormatException;
+import android.nfc.Tag;
+
+import java.io.IOException;
+
+/** Tag verifier for checking that the {@link Tag} has some expected value. */
+public interface TagVerifier {
+
+    /** @return true if the tag has the expected value */
+    Result verifyTag(Tag tag) throws FormatException, IOException;
+
+    /** Class with info necessary to show the user what was written and read from a tag. */
+    public static class Result {
+
+        private final CharSequence mExpectedContent;
+
+        private final CharSequence mActualContent;
+
+        private final boolean mIsMatch;
+
+        public Result(CharSequence expectedContent, CharSequence actualContent, boolean isMatch) {
+            this.mExpectedContent = expectedContent;
+            this.mActualContent = actualContent;
+            this.mIsMatch = isMatch;
+        }
+
+        /** @return {@link CharSequence} representation of the data written to the tag */
+        public CharSequence getExpectedContent() {
+            return mExpectedContent;
+        }
+
+        /** @return {@link CharSequence} representation of the data read back from the tag */
+        public CharSequence getActualContent() {
+            return mActualContent;
+        }
+
+        /** @return whether or not the expected content matched the actual content of the tag */
+        public boolean isMatch() {
+            return mIsMatch;
+        }
+    }
+}