Merge "Refactor notification tests to clean them up." into lmp-sprout-dev
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index 7b122c9..98044e4 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -1111,7 +1111,7 @@
             <meta-data android:name="test_category" android:value="@string/test_category_notifications" />
         </activity>
 
-        <activity android:name=".notifications.NotificationAttentionManagementVerifierActivity"
+        <activity android:name=".notifications.AttentionManagementVerifierActivity"
                 android:label="@string/attention_test">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -1129,7 +1129,8 @@
             </intent-filter>
         </service>
 
-        <service  android:name=".notifications.NotificationListenerVerifierActivity$DismissService"/>
+        <service  android:name=".notifications.InteractiveVerifierActivity$DismissService"/>
+
         <activity android:name=".security.CAInstallNotificationVerifierActivity"
                 android:label="@string/cacert_test">
             <intent-filter>
diff --git a/apps/CtsVerifier/res/drawable-hdpi/fs_clock.png b/apps/CtsVerifier/res/drawable-hdpi/fs_clock.png
new file mode 100644
index 0000000..209d78e
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable-hdpi/fs_clock.png
Binary files differ
diff --git a/apps/CtsVerifier/res/drawable-hdpi/ic_stat_alice.png b/apps/CtsVerifier/res/drawable-hdpi/ic_stat_alice.png
new file mode 100644
index 0000000..e4eea4b
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable-hdpi/ic_stat_alice.png
Binary files differ
diff --git a/apps/CtsVerifier/res/drawable-hdpi/ic_stat_bob.png b/apps/CtsVerifier/res/drawable-hdpi/ic_stat_bob.png
new file mode 100644
index 0000000..c67ff4f
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable-hdpi/ic_stat_bob.png
Binary files differ
diff --git a/apps/CtsVerifier/res/drawable-hdpi/ic_stat_charlie.png b/apps/CtsVerifier/res/drawable-hdpi/ic_stat_charlie.png
new file mode 100644
index 0000000..71afa3e
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable-hdpi/ic_stat_charlie.png
Binary files differ
diff --git a/apps/CtsVerifier/res/drawable-mdpi/fs_clock.png b/apps/CtsVerifier/res/drawable-mdpi/fs_clock.png
new file mode 100644
index 0000000..209d78e
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable-mdpi/fs_clock.png
Binary files differ
diff --git a/apps/CtsVerifier/res/drawable-mdpi/ic_stat_alice.png b/apps/CtsVerifier/res/drawable-mdpi/ic_stat_alice.png
new file mode 100644
index 0000000..3717827
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable-mdpi/ic_stat_alice.png
Binary files differ
diff --git a/apps/CtsVerifier/res/drawable-mdpi/ic_stat_bob.png b/apps/CtsVerifier/res/drawable-mdpi/ic_stat_bob.png
new file mode 100644
index 0000000..f266312
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable-mdpi/ic_stat_bob.png
Binary files differ
diff --git a/apps/CtsVerifier/res/drawable-mdpi/ic_stat_charlie.png b/apps/CtsVerifier/res/drawable-mdpi/ic_stat_charlie.png
new file mode 100644
index 0000000..49c4b9a
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable-mdpi/ic_stat_charlie.png
Binary files differ
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/AttentionManagementVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/AttentionManagementVerifierActivity.java
new file mode 100644
index 0000000..d8f196a
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/AttentionManagementVerifierActivity.java
@@ -0,0 +1,931 @@
+/*
+ * Copyright (C) 2014 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.notifications;
+
+import static com.android.cts.verifier.notifications.MockListener.JSON_AMBIENT;
+import static com.android.cts.verifier.notifications.MockListener.JSON_MATCHES_ZEN_FILTER;
+import static com.android.cts.verifier.notifications.MockListener.JSON_TAG;
+
+import android.app.Notification;
+import android.content.ContentProviderOperation;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.service.notification.NotificationListenerService;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.cts.verifier.R;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class AttentionManagementVerifierActivity
+        extends InteractiveVerifierActivity {
+    private static final String TAG = "NoListenerAttentionVerifier";
+
+    private static final String ALICE = "Alice";
+    private static final String ALICE_PHONE = "+16175551212";
+    private static final String ALICE_EMAIL = "alice@_foo._bar";
+    private static final String BOB = "Bob";
+    private static final String BOB_PHONE = "+16505551212";;
+    private static final String BOB_EMAIL = "bob@_foo._bar";
+    private static final String CHARLIE = "Charlie";
+    private static final String CHARLIE_PHONE = "+13305551212";
+    private static final String CHARLIE_EMAIL = "charlie@_foo._bar";
+    private static final int MODE_NONE = 0;
+    private static final int MODE_URI = 1;
+    private static final int MODE_PHONE = 2;
+    private static final int MODE_EMAIL = 3;
+
+    private Uri mAliceUri;
+    private Uri mBobUri;
+    private Uri mCharlieUri;
+
+    @Override
+    int getTitleResource() {
+        return R.string.attention_test;
+    }
+
+    @Override
+    int getInstructionsResource() {
+        return R.string.attention_info;
+    }
+
+    // Test Setup
+
+    @Override
+    protected List<InteractiveTestCase> createTestItems() {
+        List<InteractiveTestCase> tests = new ArrayList<>(17);
+        tests.add(new IsEnabledTest());
+        tests.add(new ServiceStartedTest());
+        tests.add(new InsertContactsTest());
+        tests.add(new SetModeNoneTest());
+        tests.add(new NoneInterceptsAllTest());
+        tests.add(new SetModePriorityTest());
+        tests.add(new PriorityInterceptsSomeTest());
+        tests.add(new SetModeAllTest());
+        tests.add(new AllInterceptsNothingTest());
+        tests.add(new DefaultOrderTest());
+        tests.add(new PrioritytOrderTest());
+        tests.add(new InterruptionOrderTest());
+        tests.add(new AmbientBitsTest());
+        tests.add(new LookupUriOrderTest());
+        tests.add(new EmailOrderTest());
+        tests.add(new PhoneOrderTest());
+        tests.add(new DeleteContactsTest());
+        return tests;
+    }
+
+    // Tests
+
+    protected class InsertContactsTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.attention_create_contacts);
+        }
+
+        @Override
+        void setUp() {
+            insertSingleContact(ALICE, ALICE_PHONE, ALICE_EMAIL, true);
+            insertSingleContact(BOB, BOB_PHONE, BOB_EMAIL, false);
+            // charlie is not in contacts
+            status = READY;
+            // wait for insertions to move through the system
+            delay();
+        }
+
+        @Override
+        void test() {
+            mAliceUri = lookupContact(ALICE_PHONE);
+            mBobUri = lookupContact(BOB_PHONE);
+            mCharlieUri = lookupContact(CHARLIE_PHONE);
+
+            status = PASS;
+            if (mAliceUri == null) { status = FAIL; }
+            if (mBobUri == null) { status = FAIL; }
+            if (mCharlieUri != null) { status = FAIL; }
+            next();
+        }
+    }
+
+    protected class DeleteContactsTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.attention_delete_contacts);
+        }
+
+        @Override
+        void test() {
+            final ArrayList<ContentProviderOperation> operationList = new ArrayList<>();
+            operationList.add(ContentProviderOperation.newDelete(mAliceUri).build());
+            operationList.add(ContentProviderOperation.newDelete(mBobUri).build());
+            try {
+                mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operationList);
+                status = READY;
+            } catch (RemoteException e) {
+                Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
+                status = FAIL;
+            } catch (OperationApplicationException e) {
+                Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
+                status = FAIL;
+            }
+            status = PASS;
+            next();
+        }
+    }
+
+    protected class SetModeNoneTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createRetryItem(parent, R.string.attention_filter_none);
+        }
+
+        @Override
+        void test() {
+            MockListener.probeFilter(mContext,
+                    new MockListener.IntegerResultCatcher() {
+                        @Override
+                        public void accept(int mode) {
+                            if (mode == NotificationListenerService.INTERRUPTION_FILTER_NONE) {
+                                status = PASS;
+                                next();
+                            } else {
+                                Log.i("SetModeNoneTest", "waiting, current mode is: " + mode);
+                                status = WAIT_FOR_USER;
+                            }
+                        }
+                    });
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+
+    protected class NoneInterceptsAllTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.attention_all_are_filtered);
+        }
+
+        @Override
+        void setUp() {
+            sendNotifications(MODE_URI, false, false);
+            status = READY;
+            // wait for notifications to move through the system
+            delay();
+        }
+
+        @Override
+        void test() {
+            MockListener.probeListenerPayloads(mContext,
+                    new MockListener.StringListResultCatcher() {
+                        @Override
+                        public void accept(List<String> result) {
+                            Set<String> found = new HashSet<String>();
+                            if (result == null || result.size() == 0) {
+                                status = FAIL;
+                                next();
+                                return;
+                            }
+                            boolean pass = true;
+                            for (String payloadData : result) {
+                                try {
+                                    JSONObject payload = new JSONObject(payloadData);
+                                    String tag = payload.getString(JSON_TAG);
+                                    boolean zen = payload.getBoolean(JSON_MATCHES_ZEN_FILTER);
+                                    Log.e(TAG, tag + (zen ? "" : " not") + " intercepted");
+                                    if (found.contains(tag)) {
+                                        // multiple entries for same notification!
+                                        pass = false;
+                                    } else if (ALICE.equals(tag)) {
+                                        found.add(ALICE);
+                                        pass &= !zen;
+                                    } else if (BOB.equals(tag)) {
+                                        found.add(BOB);
+                                        pass &= !zen;
+                                    } else if (CHARLIE.equals(tag)) {
+                                        found.add(CHARLIE);
+                                        pass &= !zen;
+                                    }
+                                } catch (JSONException e) {
+                                    pass = false;
+                                    Log.e(TAG, "failed to unpack data from mocklistener", e);
+                                }
+                            }
+                            pass &= found.size() == 3;
+                            status = pass ? PASS : FAIL;
+                            next();
+                        }
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+
+    }
+
+    protected class SetModeAllTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createRetryItem(parent, R.string.attention_filter_all);
+        }
+
+        @Override
+        void test() {
+            MockListener.probeFilter(mContext,
+                    new MockListener.IntegerResultCatcher() {
+                        @Override
+                        public void accept(int mode) {
+                            if (mode == NotificationListenerService.INTERRUPTION_FILTER_ALL) {
+                                status = PASS;
+                                next();
+                            } else {
+                                Log.i("SetModeAllTest", "waiting, current mode is: " + mode);
+                                status = WAIT_FOR_USER;
+                            }
+                        }
+                    });
+        }
+    }
+
+    protected class AllInterceptsNothingTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.attention_none_are_filtered);
+        }
+
+        @Override
+        void setUp() {
+            sendNotifications(MODE_URI, false, false);
+            status = READY;
+            // wait for notifications to move through the system
+            delay();
+        }
+
+        @Override
+        void test() {
+            MockListener.probeListenerPayloads(mContext,
+                    new MockListener.StringListResultCatcher() {
+                        @Override
+                        public void accept(List<String> result) {
+                            Set<String> found = new HashSet<String>();
+                            if (result == null || result.size() == 0) {
+                                status = FAIL;
+                                return;
+                            }
+                            boolean pass = true;
+                            for (String payloadData : result) {
+                                try {
+                                    JSONObject payload = new JSONObject(payloadData);
+                                    String tag = payload.getString(JSON_TAG);
+                                    boolean zen = payload.getBoolean(JSON_MATCHES_ZEN_FILTER);
+                                    Log.e(TAG, tag + (zen ? "" : " not") + " intercepted");
+                                    if (found.contains(tag)) {
+                                        // multiple entries for same notification!
+                                        pass = false;
+                                    } else if (ALICE.equals(tag)) {
+                                        found.add(ALICE);
+                                        pass &= zen;
+                                    } else if (BOB.equals(tag)) {
+                                        found.add(BOB);
+                                        pass &= zen;
+                                    } else if (CHARLIE.equals(tag)) {
+                                        found.add(CHARLIE);
+                                        pass &= zen;
+                                    }
+                                } catch (JSONException e) {
+                                    pass = false;
+                                    Log.e(TAG, "failed to unpack data from mocklistener", e);
+                                }
+                            }
+                            pass &= found.size() == 3;
+                            status = pass ? PASS : FAIL;
+                            next();
+                        }
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+
+    protected class SetModePriorityTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createRetryItem(parent, R.string.attention_filter_priority);
+        }
+
+        @Override
+        void test() {
+            MockListener.probeFilter(mContext,
+                    new MockListener.IntegerResultCatcher() {
+                        @Override
+                        public void accept(int mode) {
+                            if (mode == NotificationListenerService.INTERRUPTION_FILTER_PRIORITY) {
+                                status = PASS;
+                                next();
+                            } else {
+                                Log.i("SetModePriorityTest", "waiting, current mode is: " + mode);
+                                status = WAIT_FOR_USER;
+                            }
+                        }
+                    });
+        }
+    }
+
+    protected class PriorityInterceptsSomeTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.attention_some_are_filtered);
+        }
+
+        @Override
+        void setUp() {
+            sendNotifications(MODE_URI, false, false);
+            status = READY;
+            // wait for notifications to move through the system
+            delay();
+        }
+
+        @Override
+        void test() {
+            MockListener.probeListenerPayloads(mContext,
+                    new MockListener.StringListResultCatcher() {
+                        @Override
+                        public void accept(List<String> result) {
+                            Set<String> found = new HashSet<String>();
+                            if (result == null || result.size() == 0) {
+                                status = FAIL;
+                                return;
+                            }
+                            boolean pass = true;
+                            for (String payloadData : result) {
+                                try {
+                                    JSONObject payload = new JSONObject(payloadData);
+                                    String tag = payload.getString(JSON_TAG);
+                                    boolean zen = payload.getBoolean(JSON_MATCHES_ZEN_FILTER);
+                                    Log.e(TAG, tag + (zen ? "" : " not") + " intercepted");
+                                    if (found.contains(tag)) {
+                                        // multiple entries for same notification!
+                                        pass = false;
+                                    } else if (ALICE.equals(tag)) {
+                                        found.add(ALICE);
+                                        pass &= zen;
+                                    } else if (BOB.equals(tag)) {
+                                        found.add(BOB);
+                                        pass &= !zen;
+                                    } else if (CHARLIE.equals(tag)) {
+                                        found.add(CHARLIE);
+                                        pass &= !zen;
+                                    }
+                                } catch (JSONException e) {
+                                    pass = false;
+                                    Log.e(TAG, "failed to unpack data from mocklistener", e);
+                                }
+                            }
+                            pass &= found.size() == 3;
+                            status = pass ? PASS : FAIL;
+                            next();
+                        }
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+
+    // ordered by time: C, B, A
+    protected class DefaultOrderTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.attention_default_order);
+        }
+
+        @Override
+        void setUp() {
+            sendNotifications(MODE_NONE, false, false);
+            status = READY;
+            // wait for notifications to move through the system
+            delay();
+        }
+
+        @Override
+        void test() {
+            MockListener.probeListenerOrder(mContext,
+                    new MockListener.StringListResultCatcher() {
+                        @Override
+                        public void accept(List<String> orderedKeys) {
+                            int rankA = findTagInKeys(ALICE, orderedKeys);
+                            int rankB = findTagInKeys(BOB, orderedKeys);
+                            int rankC = findTagInKeys(CHARLIE, orderedKeys);
+                            if (rankC < rankB && rankB < rankA) {
+                                status = PASS;
+                            } else {
+                                logFail(rankA + ", " + rankB + ", " + rankC);
+                                status = FAIL;
+                            }
+                            next();
+                        }
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+
+    // ordered by priority: B, C, A
+    protected class PrioritytOrderTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.attention_priority_order);
+        }
+
+        @Override
+        void setUp() {
+            sendNotifications(MODE_NONE, true, false);
+            status = READY;
+            // wait for notifications to move through the system
+            delay();
+        }
+
+        @Override
+        void test() {
+            MockListener.probeListenerOrder(mContext,
+                    new MockListener.StringListResultCatcher() {
+                        @Override
+                        public void accept(List<String> orderedKeys) {
+                            int rankA = findTagInKeys(ALICE, orderedKeys);
+                            int rankB = findTagInKeys(BOB, orderedKeys);
+                            int rankC = findTagInKeys(CHARLIE, orderedKeys);
+                            if (rankB < rankC && rankC < rankA) {
+                                status = PASS;
+                            } else {
+                                logFail(rankA + ", " + rankB + ", " + rankC);
+                                status = FAIL;
+                            }
+                            next();
+                        }
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+
+    // A starts at the top then falls to the bottom
+    protected class InterruptionOrderTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.attention_interruption_order);
+        }
+
+        @Override
+        void setUp() {
+            sendNotifications(MODE_NONE, false, true);
+            status = READY;
+            // wait for notifications to move through the system
+            delay();
+        }
+
+        @Override
+        void test() {
+            if (status == READY) {
+                MockListener.probeListenerOrder(mContext,
+                        new MockListener.StringListResultCatcher() {
+                            @Override
+                            public void accept(List<String> orderedKeys) {
+                                int rankA = findTagInKeys(ALICE, orderedKeys);
+                                int rankB = findTagInKeys(BOB, orderedKeys);
+                                int rankC = findTagInKeys(CHARLIE, orderedKeys);
+                                if (rankA < rankB && rankA < rankC) {
+                                    status = RETEST;
+                                    delay(12000);
+                                } else {
+                                    logFail("noisy notification did not sort to top.");
+                                    status = FAIL;
+                                    next();
+                                }
+                            }
+                        });
+                delay();  // in case the catcher never returns
+            } else {
+                MockListener.probeListenerOrder(mContext,
+                        new MockListener.StringListResultCatcher() {
+                            @Override
+                            public void accept(List<String> orderedKeys) {
+                                int rankA = findTagInKeys(ALICE, orderedKeys);
+                                int rankB = findTagInKeys(BOB, orderedKeys);
+                                int rankC = findTagInKeys(CHARLIE, orderedKeys);
+                                if (rankA > rankB && rankA > rankC) {
+                                    status = PASS;
+                                } else {
+                                    logFail("noisy notification did not fade back into the list.");
+                                    status = FAIL;
+                                }
+                                next();
+                            }
+                        });
+                delay();  // in case the catcher never returns
+            }
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+
+    // B & C above the fold, A below
+    protected class AmbientBitsTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.attention_ambient_bit);
+        }
+
+        @Override
+        void setUp() {
+            sendNotifications(MODE_NONE, true, false);
+            status = READY;
+            // wait for notifications to move through the system
+            delay();
+        }
+
+        @Override
+        void test() {
+            MockListener.probeListenerPayloads(mContext,
+                    new MockListener.StringListResultCatcher() {
+                        @Override
+                        public void accept(List<String> result) {
+                            Set<String> found = new HashSet<String>();
+                            if (result == null || result.size() == 0) {
+                                status = FAIL;
+                                return;
+                            }
+                            boolean pass = true;
+                            for (String payloadData : result) {
+                                try {
+                                    JSONObject payload = new JSONObject(payloadData);
+                                    String tag = payload.getString(JSON_TAG);
+                                    boolean ambient = payload.getBoolean(JSON_AMBIENT);
+                                    Log.e(TAG, tag + (ambient ? " is" : " isn't") + " ambient");
+                                    if (found.contains(tag)) {
+                                        // multiple entries for same notification!
+                                        pass = false;
+                                    } else if (ALICE.equals(tag)) {
+                                        found.add(ALICE);
+                                        pass &= ambient;
+                                    } else if (BOB.equals(tag)) {
+                                        found.add(BOB);
+                                        pass &= !ambient;
+                                    } else if (CHARLIE.equals(tag)) {
+                                        found.add(CHARLIE);
+                                        pass &= !ambient;
+                                    }
+                                } catch (JSONException e) {
+                                    pass = false;
+                                    Log.e(TAG, "failed to unpack data from mocklistener", e);
+                                }
+                            }
+                            pass &= found.size() == 3;
+                            status = pass ? PASS : FAIL;
+                            next();
+                        }
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+
+    // ordered by contact affinity: A, B, C
+    protected class LookupUriOrderTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.attention_lookup_order);
+        }
+
+        @Override
+        void setUp() {
+            sendNotifications(MODE_URI, false, false);
+            status = READY;
+            // wait for notifications to move through the system
+            delay();
+        }
+
+        @Override
+        void test() {
+            MockListener.probeListenerOrder(mContext,
+                    new MockListener.StringListResultCatcher() {
+                        @Override
+                        public void accept(List<String> orderedKeys) {
+                            int rankA = findTagInKeys(ALICE, orderedKeys);
+                            int rankB = findTagInKeys(BOB, orderedKeys);
+                            int rankC = findTagInKeys(CHARLIE, orderedKeys);
+                            if (rankA < rankB && rankB < rankC) {
+                                status = PASS;
+                            } else {
+                                logFail(rankA + ", " + rankB + ", " + rankC);
+                                status = FAIL;
+                            }
+                            next();
+                        }
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+
+    // ordered by contact affinity: A, B, C
+    protected class EmailOrderTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.attention_email_order);
+        }
+
+        @Override
+        void setUp() {
+            sendNotifications(MODE_EMAIL, false, false);
+            status = READY;
+            // wait for notifications to move through the system
+            delay();
+        }
+
+        @Override
+        void test() {
+            MockListener.probeListenerOrder(mContext,
+                    new MockListener.StringListResultCatcher() {
+                        @Override
+                        public void accept(List<String> orderedKeys) {
+                            int rankA = findTagInKeys(ALICE, orderedKeys);
+                            int rankB = findTagInKeys(BOB, orderedKeys);
+                            int rankC = findTagInKeys(CHARLIE, orderedKeys);
+                            if (rankA < rankB && rankB < rankC) {
+                                status = PASS;
+                            } else {
+                                logFail(rankA + ", " + rankB + ", " + rankC);
+                                status = FAIL;
+                            }
+                            next();
+                        }
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+
+    // ordered by contact affinity: A, B, C
+    protected class PhoneOrderTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.attention_phone_order);
+        }
+
+        @Override
+        void setUp() {
+            sendNotifications(MODE_PHONE, false, false);
+            status = READY;
+            // wait for notifications to move through the system
+            delay();
+        }
+
+        @Override
+        void test() {
+            MockListener.probeListenerOrder(mContext,
+                    new MockListener.StringListResultCatcher() {
+                        @Override
+                        public void accept(List<String> orderedKeys) {
+                            int rankA = findTagInKeys(ALICE, orderedKeys);
+                            int rankB = findTagInKeys(BOB, orderedKeys);
+                            int rankC = findTagInKeys(CHARLIE, orderedKeys);
+                            if (rankA < rankB && rankB < rankC) {
+                                status = PASS;
+                            } else {
+                                logFail(rankA + ", " + rankB + ", " + rankC);
+                                status = FAIL;
+                            }
+                            next();
+                        }
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+
+    // Utilities
+
+    // usePriorities true: B, C, A
+    // usePriorities false:
+    //   MODE_NONE: C, B, A
+    //   otherwise: A, B ,C
+    private void sendNotifications(int annotationMode, boolean usePriorities, boolean noisy) {
+        // TODO(cwren) Fixes flakey tests due to bug 17644321. Remove this line when it is fixed.
+        int baseId = NOTIFICATION_ID + (noisy ? 3 : 0);
+
+        // C, B, A when sorted by time.  Times must be in the past.
+        long whenA = System.currentTimeMillis() - 4000000L;
+        long whenB = System.currentTimeMillis() - 2000000L;
+        long whenC = System.currentTimeMillis() - 1000000L;
+
+        // B, C, A when sorted by priorities
+        int priorityA = usePriorities ? Notification.PRIORITY_MIN : Notification.PRIORITY_DEFAULT;
+        int priorityB = usePriorities ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT;
+        int priorityC = usePriorities ? Notification.PRIORITY_LOW : Notification.PRIORITY_DEFAULT;
+
+        Notification.Builder alice = new Notification.Builder(mContext)
+                .setContentTitle(ALICE)
+                .setContentText(ALICE)
+                .setSmallIcon(R.drawable.ic_stat_alice)
+                .setPriority(priorityA)
+                .setCategory(Notification.CATEGORY_MESSAGE)
+                .setWhen(whenA);
+        alice.setDefaults(noisy ? Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE : 0);
+        addPerson(annotationMode, alice, mAliceUri, ALICE_PHONE, ALICE_EMAIL);
+        mNm.notify(ALICE, baseId + 1, alice.build());
+
+        Notification.Builder bob = new Notification.Builder(mContext)
+                .setContentTitle(BOB)
+                .setContentText(BOB)
+                .setSmallIcon(R.drawable.ic_stat_bob)
+                .setPriority(priorityB)
+                .setCategory(Notification.CATEGORY_MESSAGE)
+                .setWhen(whenB);
+        addPerson(annotationMode, bob, mBobUri, BOB_PHONE, BOB_EMAIL);
+        mNm.notify(BOB, baseId + 2, bob.build());
+
+        Notification.Builder charlie = new Notification.Builder(mContext)
+                .setContentTitle(CHARLIE)
+                .setContentText(CHARLIE)
+                .setSmallIcon(R.drawable.ic_stat_charlie)
+                .setPriority(priorityC)
+                .setCategory(Notification.CATEGORY_MESSAGE)
+                .setWhen(whenC);
+        addPerson(annotationMode, charlie, mCharlieUri, CHARLIE_PHONE, CHARLIE_EMAIL);
+        mNm.notify(CHARLIE, baseId + 3, charlie.build());
+    }
+
+    private void addPerson(int mode, Notification.Builder note,
+            Uri uri, String phone, String email) {
+        if (mode == MODE_URI && uri != null) {
+            note.addPerson(uri.toString());
+        } else if (mode == MODE_PHONE) {
+            note.addPerson(Uri.fromParts("tel", phone, null).toString());
+        } else if (mode == MODE_EMAIL) {
+            note.addPerson(Uri.fromParts("mailto", email, null).toString());
+        }
+    }
+
+    private void insertSingleContact(String name, String phone, String email, boolean starred) {
+        final ArrayList<ContentProviderOperation> operationList =
+                new ArrayList<ContentProviderOperation>();
+        ContentProviderOperation.Builder builder =
+                ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI);
+        builder.withValue(ContactsContract.RawContacts.STARRED, starred ? 1 : 0);
+        operationList.add(builder.build());
+
+        builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
+        builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0);
+        builder.withValue(ContactsContract.Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+        builder.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
+        operationList.add(builder.build());
+
+        if (phone != null) {
+            builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
+            builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0);
+            builder.withValue(ContactsContract.Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+            builder.withValue(Phone.TYPE, Phone.TYPE_MOBILE);
+            builder.withValue(Phone.NUMBER, phone);
+            builder.withValue(ContactsContract.Data.IS_PRIMARY, 1);
+            operationList.add(builder.build());
+        }
+        if (email != null) {
+            builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
+            builder.withValueBackReference(Email.RAW_CONTACT_ID, 0);
+            builder.withValue(ContactsContract.Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+            builder.withValue(Email.TYPE, Email.TYPE_HOME);
+            builder.withValue(Email.DATA, email);
+            operationList.add(builder.build());
+        }
+
+        try {
+            mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operationList);
+        } catch (RemoteException e) {
+            Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
+        } catch (OperationApplicationException e) {
+            Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
+        }
+    }
+
+    private Uri lookupContact(String phone) {
+        Cursor c = null;
+        try {
+            Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
+                    Uri.encode(phone));
+            String[] projection = new String[] { ContactsContract.Contacts._ID,
+                    ContactsContract.Contacts.LOOKUP_KEY };
+            c = mContext.getContentResolver().query(phoneUri, projection, null, null, null);
+            if (c != null && c.getCount() > 0) {
+                c.moveToFirst();
+                int lookupIdx = c.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
+                int idIdx = c.getColumnIndex(ContactsContract.Contacts._ID);
+                String lookupKey = c.getString(lookupIdx);
+                long contactId = c.getLong(idIdx);
+                return ContactsContract.Contacts.getLookupUri(contactId, lookupKey);
+            }
+        } catch (Throwable t) {
+            Log.w(TAG, "Problem getting content resolver or performing contacts query.", t);
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        return null;
+    }
+
+    /** Search a list of notification keys for a givcen tag. */
+    private int findTagInKeys(String tag, List<String> orderedKeys) {
+        for (int i = 0; i < orderedKeys.size(); i++) {
+            if (orderedKeys.get(i).contains(tag)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/InteractiveVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/InteractiveVerifierActivity.java
new file mode 100644
index 0000000..d65af80
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/InteractiveVerifierActivity.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2013 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.notifications;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.Settings.Secure;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+import com.android.cts.verifier.nfc.TagVerifierActivity;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import static com.android.cts.verifier.notifications.MockListener.*;
+
+public abstract class InteractiveVerifierActivity extends PassFailButtons.Activity
+        implements Runnable {
+    private static final String TAG = "InteractiveVerifier";
+    private static final String STATE = "state";
+    private static final String STATUS = "status";
+    private static LinkedBlockingQueue<String> sDeletedQueue = new LinkedBlockingQueue<String>();
+    protected static final String LISTENER_PATH = "com.android.cts.verifier/" +
+            "com.android.cts.verifier.notifications.MockListener";
+    protected static final int SETUP = 0;
+    protected static final int READY = 1;
+    protected static final int RETEST = 2;
+    protected static final int PASS = 3;
+    protected static final int FAIL = 4;
+    protected static final int WAIT_FOR_USER = 5;
+
+    protected static final int NOTIFICATION_ID = 1001;
+
+    // TODO remove these once b/10023397 is fixed
+    public static final String ENABLED_NOTIFICATION_LISTENERS = "enabled_notification_listeners";
+    public static final String NOTIFICATION_LISTENER_SETTINGS =
+            "android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS";
+
+    protected InteractiveTestCase mCurrentTest;
+    protected PackageManager mPackageManager;
+    protected NotificationManager mNm;
+    protected Context mContext;
+    protected Runnable mRunner;
+    protected View mHandler;
+    protected String mPackageString;
+
+    private LayoutInflater mInflater;
+    private ViewGroup mItemList;
+    private List<InteractiveTestCase> mTestList;
+    private Iterator<InteractiveTestCase> mTestOrder;
+
+    public static class DismissService extends Service {
+        @Override
+        public IBinder onBind(Intent intent) {
+            return null;
+        }
+
+        @Override
+        public void onStart(Intent intent, int startId) {
+            if(intent != null) { sDeletedQueue.offer(intent.getAction()); }
+        }
+    }
+
+    protected abstract class InteractiveTestCase {
+        int status;
+        private View view;
+
+        abstract View inflate(ViewGroup parent);
+        View getView(ViewGroup parent) {
+            if (view == null) {
+                view = inflate(parent);
+            }
+            return view;
+        }
+
+        /** @return true if the test should re-run when the test activity starts. */
+        boolean autoStart() {
+            return false;
+        }
+
+        /** Set status to {@link #READY} to proceed, or {@link #SETUP} to try again. */
+        void setUp() { status = READY; next(); };
+
+        /** Set status to {@link #PASS} or @{link #FAIL} to proceed, or {@link #READY} to retry. */
+        void test() { status = FAIL; next(); };
+
+        /** Do not modify status. */
+        void tearDown() { next(); };
+
+        protected void logFail() {
+            logFail(null);
+        }
+
+        protected void logFail(String message) {
+            logWithStack("failed " + this.getClass().getSimpleName() +
+                    ((message == null) ? "" : ": " + message));
+        }
+    }
+
+    abstract int getTitleResource();
+    abstract int getInstructionsResource();
+
+    protected void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        int savedStateIndex = (savedState == null) ? 0 : savedState.getInt(STATE, 0);
+        int savedStatus = (savedState == null) ? SETUP : savedState.getInt(STATUS, SETUP);
+        Log.i(TAG, "restored state(" + savedStateIndex + "}, status(" + savedStatus + ")");
+        mContext = this;
+        mRunner = this;
+        mNm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+        mPackageManager = getPackageManager();
+        mInflater = getLayoutInflater();
+        View view = mInflater.inflate(R.layout.nls_main, null);
+        mItemList = (ViewGroup) view.findViewById(R.id.nls_test_items);
+        mHandler = mItemList;
+        mTestList = new ArrayList<>();
+        mTestList.addAll(createTestItems());
+        for (InteractiveTestCase test: mTestList) {
+            mItemList.addView(test.getView(mItemList));
+        }
+        mTestOrder = mTestList.iterator();
+        for (int i = 0; i < savedStateIndex; i++) {
+            mCurrentTest = mTestOrder.next();
+            mCurrentTest.status = PASS;
+        }
+        mCurrentTest = mTestOrder.next();
+        mCurrentTest.status = savedStatus;
+
+        setContentView(view);
+        setPassFailButtonClickListeners();
+        getPassButton().setEnabled(false);
+
+        setInfoResources(getTitleResource(), getInstructionsResource(), -1);
+    }
+
+    @Override
+    protected void onSaveInstanceState (Bundle outState) {
+        final int stateIndex = mTestList.indexOf(mCurrentTest);
+        outState.putInt(STATE, stateIndex);
+        outState.putInt(STATUS, mCurrentTest.status);
+        Log.i(TAG, "saved state(" + stateIndex + "}, status(" + (mCurrentTest.status) + ")");
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        if (mCurrentTest.autoStart()) {
+            mCurrentTest.status = READY;
+        }
+        next();
+    }
+
+    // Interface Utilities
+
+    protected void markItem(InteractiveTestCase test) {
+        if (test == null) { return; }
+        View item = test.view;
+        ImageView status = (ImageView) item.findViewById(R.id.nls_status);
+        View button = item.findViewById(R.id.nls_action_button);
+        switch (test.status) {
+            case WAIT_FOR_USER:
+                status.setImageResource(R.drawable.fs_warning);
+                break;
+
+            case SETUP:
+            case READY:
+            case RETEST:
+                status.setImageResource(R.drawable.fs_clock);
+                break;
+
+            case FAIL:
+                status.setImageResource(R.drawable.fs_error);
+                button.setClickable(false);
+                button.setEnabled(false);
+                break;
+
+            case PASS:
+                status.setImageResource(R.drawable.fs_good);
+                button.setClickable(false);
+                button.setEnabled(false);
+                break;
+
+        }
+        status.invalidate();
+    }
+
+    protected View createNlsSettingsItem(ViewGroup parent, int messageId) {
+        return createUserItem(parent, messageId, R.string.nls_start_settings);
+    }
+
+    protected View createRetryItem(ViewGroup parent, int messageId) {
+        return createUserItem(parent, messageId, R.string.attention_ready);
+    }
+
+    protected View createUserItem(ViewGroup parent, int messageId, int actionId) {
+        View item = mInflater.inflate(R.layout.nls_item, parent, false);
+        TextView instructions = (TextView) item.findViewById(R.id.nls_instructions);
+        instructions.setText(messageId);
+        Button button = (Button) item.findViewById(R.id.nls_action_button);
+        button.setText(actionId);
+        button.setTag(actionId);
+        return item;
+    }
+
+    protected View  createAutoItem(ViewGroup parent, int stringId) {
+        View item = mInflater.inflate(R.layout.nls_item, parent, false);
+        TextView instructions = (TextView) item.findViewById(R.id.nls_instructions);
+        instructions.setText(stringId);
+        View button = item.findViewById(R.id.nls_action_button);
+        button.setVisibility(View.GONE);
+        return item;
+    }
+
+    // Test management
+
+    abstract protected List<InteractiveTestCase> createTestItems();
+
+    public void run() {
+        if (mCurrentTest == null) { return; }
+        markItem(mCurrentTest);
+        switch (mCurrentTest.status) {
+            case SETUP:
+                Log.i(TAG, "running setup for: " + mCurrentTest.getClass().getSimpleName());
+                mCurrentTest.setUp();
+                break;
+
+            case WAIT_FOR_USER:
+                Log.i(TAG, "waiting for user: " + mCurrentTest.getClass().getSimpleName());
+                break;
+
+            case READY:
+            case RETEST:
+                Log.i(TAG, "running test for: " + mCurrentTest.getClass().getSimpleName());
+                mCurrentTest.test();
+                break;
+
+            case FAIL:
+                Log.i(TAG, "FAIL: " + mCurrentTest.getClass().getSimpleName());
+                mCurrentTest = null;
+                break;
+
+            case PASS:
+                Log.i(TAG, "pass for: " + mCurrentTest.getClass().getSimpleName());
+                mCurrentTest.tearDown();
+                if (mTestOrder.hasNext()) {
+                    mCurrentTest = mTestOrder.next();
+                    Log.i(TAG, "next test is: " + mCurrentTest.getClass().getSimpleName());
+                } else {
+                    Log.i(TAG, "no more tests");
+                    mCurrentTest = null;
+                    getPassButton().setEnabled(true);
+                    mNm.cancelAll();
+                }
+                break;
+        }
+        markItem(mCurrentTest);
+    }
+
+    /**
+     * Return to the state machine to progress through the tests.
+     */
+    protected void next() {
+        mHandler.removeCallbacks(mRunner);
+        mHandler.post(mRunner);
+    }
+
+    /**
+     * Wait for things to settle before returning to the state machine.
+     */
+    protected void delay() {
+        delay(3000);
+    }
+
+    /**
+     * Wait for some time.
+     */
+    protected void delay(long waitTime) {
+        mHandler.removeCallbacks(mRunner);
+        mHandler.postDelayed(mRunner, waitTime);
+    }
+
+    // UI callbacks
+
+    public void launchSettings() {
+        startActivity(new Intent(NOTIFICATION_LISTENER_SETTINGS));
+    }
+
+    public void actionPressed(View v) {
+        Object tag = v.getTag();
+        if (tag instanceof Integer) {
+            int id = ((Integer) tag).intValue();
+            if (id == R.string.nls_start_settings) {
+                launchSettings();
+            } else if (id == R.string.attention_ready) {
+                mCurrentTest.status = READY;
+                next();
+            }
+        }
+    }
+
+    // Utilities
+
+    protected PendingIntent makeIntent(int code, String tag) {
+        Intent intent = new Intent(tag);
+        intent.setComponent(new ComponentName(mContext, DismissService.class));
+        PendingIntent pi = PendingIntent.getService(mContext, code, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT);
+        return pi;
+    }
+
+    protected boolean checkEquals(long expected, long actual, String message) {
+        if (expected == actual) {
+            return true;
+        }
+        logWithStack(String.format(message, expected, actual));
+        return false;
+    }
+
+    protected boolean checkEquals(String expected, String actual, String message) {
+        if (expected.equals(actual)) {
+            return true;
+        }
+        logWithStack(String.format(message, expected, actual));
+        return false;
+    }
+
+    protected boolean checkFlagSet(int expected, int actual, String message) {
+        if ((expected & actual) != 0) {
+            return true;
+        }
+        logWithStack(String.format(message, expected, actual));
+        return false;
+    };
+
+    protected void logWithStack(String message) {
+        Throwable stackTrace = new Throwable();
+        stackTrace.fillInStackTrace();
+        Log.e(TAG, message, stackTrace);
+    }
+
+    // Common Tests: useful for the side-effects they generate
+
+    protected class IsEnabledTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createNlsSettingsItem(parent, R.string.nls_enable_service);
+        }
+
+        @Override
+        boolean autoStart() {
+            return true;
+        }
+
+        @Override
+        void test() {
+            Intent settings = new Intent(NOTIFICATION_LISTENER_SETTINGS);
+            if (settings.resolveActivity(mPackageManager) == null) {
+                logFail("no settings activity");
+                status = FAIL;
+            } else {
+                String listeners = Secure.getString(getContentResolver(),
+                        ENABLED_NOTIFICATION_LISTENERS);
+                if (listeners != null && listeners.contains(LISTENER_PATH)) {
+                    status = PASS;
+                } else {
+                    status = WAIT_FOR_USER;
+                }
+                next();
+            }
+        }
+
+        void tearDown() {
+            // wait for the service to start
+            delay();
+        }
+    }
+
+    protected class ServiceStartedTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.nls_service_started);
+        }
+
+        @Override
+        void test() {
+            MockListener.probeListenerStatus(mContext,
+                    new MockListener.StatusCatcher() {
+                        @Override
+                        public void accept(int result) {
+                            if (result == Activity.RESULT_OK) {
+                                status = PASS;
+                                next();
+                            } else {
+                                logFail();
+                                status = RETEST;
+                                delay();
+                            }
+                        }
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/MockListener.java b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/MockListener.java
index b4863fa..75eaebd 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/MockListener.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/MockListener.java
@@ -238,6 +238,7 @@
         Log.d(TAG, "removed: " + sbn.getTag());
         mRemoved.add(sbn.getTag());
         mNotifications.remove(sbn.getKey());
+        mNotificationKeys.remove(sbn.getTag());
         onNotificationRankingUpdate(rankingMap);
         mNotificationKeys.remove(sbn.getTag());
     }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/NotificationAttentionManagementVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/NotificationAttentionManagementVerifierActivity.java
deleted file mode 100644
index b4e348f..0000000
--- a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/NotificationAttentionManagementVerifierActivity.java
+++ /dev/null
@@ -1,883 +0,0 @@
-/*
- * Copyright (C) 2014 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.notifications;
-
-import static com.android.cts.verifier.notifications.MockListener.JSON_AMBIENT;
-import static com.android.cts.verifier.notifications.MockListener.JSON_MATCHES_ZEN_FILTER;
-import static com.android.cts.verifier.notifications.MockListener.JSON_TAG;
-
-import android.app.Activity;
-import android.app.Notification;
-import android.content.ContentProviderOperation;
-import android.content.Intent;
-import android.content.OperationApplicationException;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.RemoteException;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds.Email;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.CommonDataKinds.StructuredName;
-import android.provider.Settings.Secure;
-import android.service.notification.NotificationListenerService;
-import android.util.Log;
-import com.android.cts.verifier.R;
-import com.android.cts.verifier.nfc.TagVerifierActivity;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-public class NotificationAttentionManagementVerifierActivity
-        extends NotificationListenerVerifierActivity {
-    private static final String TAG = TagVerifierActivity.class.getSimpleName();
-    private static final String ALICE = "Alice";
-    private static final String ALICE_PHONE = "+16175551212";
-    private static final String ALICE_EMAIL = "alice@_foo._bar";
-    private static final String BOB = "Bob";
-    private static final String BOB_PHONE = "+16505551212";;
-    private static final String BOB_EMAIL = "bob@_foo._bar";
-    private static final String CHARLIE = "Charlie";
-    private static final String CHARLIE_PHONE = "+13305551212";
-    private static final String CHARLIE_EMAIL = "charlie@_foo._bar";
-    private static final int MODE_NONE = 0;
-    private static final int MODE_URI = 1;
-    private static final int MODE_PHONE = 2;
-    private static final int MODE_EMAIL = 3;
-    private static final int DELAYED_SETUP = CLEARED;
-
-    private Uri mAliceUri;
-    private Uri mBobUri;
-    private Uri mCharlieUri;
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState, R.layout.nls_main);
-        setInfoResources(R.string.attention_test, R.string.attention_info, -1);
-    }
-
-    // Test Setup
-
-    @Override
-    protected void createTestItems() {
-        createNlsSettingsItem(R.string.nls_enable_service);
-        createAutoItem(R.string.nls_service_started);
-        createAutoItem(R.string.attention_create_contacts);
-        createRetryItem(R.string.attention_filter_none);
-        createAutoItem(R.string.attention_all_are_filtered);
-        createRetryItem(R.string.attention_filter_all);
-        createAutoItem(R.string.attention_none_are_filtered);
-        createAutoItem(R.string.attention_default_order);
-        createAutoItem(R.string.attention_interruption_order);
-        createAutoItem(R.string.attention_priority_order);
-        createAutoItem(R.string.attention_ambient_bit);
-        createAutoItem(R.string.attention_lookup_order);
-        createAutoItem(R.string.attention_email_order);
-        createAutoItem(R.string.attention_phone_order);
-        createRetryItem(R.string.attention_filter_priority);
-        createAutoItem(R.string.attention_some_are_filtered);
-        createAutoItem(R.string.attention_delete_contacts);
-    }
-
-    // Test management
-
-    @Override
-    protected void updateStateMachine() {
-        switch (mState) {
-            case 0:
-                testIsEnabled(mState);
-                break;
-            case 1:
-                testIsStarted(mState);
-                break;
-            case 2:
-                testInsertContacts(mState);
-                break;
-            case 3:
-                testModeNone(mState);
-                break;
-            case 4:
-                testNoneInterceptsAll(mState);
-                break;
-            case 5:
-                testModeAll(mState);
-                break;
-            case 6:
-                testAllInterceptsNothing(mState);
-                break;
-            case 7:
-                testDefaultOrder(mState);
-                break;
-            case 8:
-                testInterruptionOrder(mState);
-                break;
-            case 9:
-                testPrioritytOrder(mState);
-                break;
-            case 10:
-                testAmbientBits(mState);
-                break;
-            case 11:
-                testLookupUriOrder(mState);
-                break;
-            case 12:
-                testEmailOrder(mState);
-                break;
-            case 13:
-                testPhoneOrder(mState);
-                break;
-            case 14:
-                testModePriority(mState);
-                break;
-            case 15:
-                testPriorityInterceptsSome(mState);
-                break;
-            case 16:
-                testDeleteContacts(mState);
-                break;
-            case 17:
-                getPassButton().setEnabled(true);
-                mNm.cancelAll();
-                break;
-        }
-    }
-
-    // usePriorities true: B, C, A
-    // usePriorities false:
-    //   MODE_NONE: C, B, A
-    //   otherwise: A, B ,C
-    private void sendNotifications(int annotationMode, boolean usePriorities, boolean noisy) {
-        // TODO(cwren) Fixes flakey tests due to bug 17644321. Remove this line when it is fixed.
-        int baseId = NOTIFICATION_ID + (noisy ? 3 : 0);
-
-        // C, B, A when sorted by time.  Times must be in the past.
-        long whenA = System.currentTimeMillis() - 4000000L;
-        long whenB = System.currentTimeMillis() - 2000000L;
-        long whenC = System.currentTimeMillis() - 1000000L;
-
-        // B, C, A when sorted by priorities
-        int priorityA = usePriorities ? Notification.PRIORITY_MIN : Notification.PRIORITY_DEFAULT;
-        int priorityB = usePriorities ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT;
-        int priorityC = usePriorities ? Notification.PRIORITY_LOW : Notification.PRIORITY_DEFAULT;
-
-        Notification.Builder alice = new Notification.Builder(mContext)
-                .setContentTitle(ALICE)
-                .setContentText(ALICE)
-                .setSmallIcon(R.drawable.fs_good)
-                .setPriority(priorityA)
-                .setCategory(Notification.CATEGORY_MESSAGE)
-                .setWhen(whenA);
-        alice.setDefaults(noisy ? Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE : 0);
-        addPerson(annotationMode, alice, mAliceUri, ALICE_PHONE, ALICE_EMAIL);
-        mNm.notify(ALICE, baseId + 1, alice.build());
-
-        Notification.Builder bob = new Notification.Builder(mContext)
-                .setContentTitle(BOB)
-                .setContentText(BOB)
-                .setSmallIcon(R.drawable.fs_warning)
-                .setPriority(priorityB)
-                .setCategory(Notification.CATEGORY_MESSAGE)
-                .setWhen(whenB);
-        addPerson(annotationMode, bob, mBobUri, BOB_PHONE, BOB_EMAIL);
-        mNm.notify(BOB, baseId + 2, bob.build());
-
-        Notification.Builder charlie = new Notification.Builder(mContext)
-                .setContentTitle(CHARLIE)
-                .setContentText(CHARLIE)
-                .setSmallIcon(R.drawable.fs_error)
-                .setPriority(priorityC)
-                .setCategory(Notification.CATEGORY_MESSAGE)
-                .setWhen(whenC);
-        addPerson(annotationMode, charlie, mCharlieUri, CHARLIE_PHONE, CHARLIE_EMAIL);
-        mNm.notify(CHARLIE, baseId + 3, charlie.build());
-    }
-
-    private void addPerson(int mode, Notification.Builder note,
-            Uri uri, String phone, String email) {
-        if (mode == MODE_URI && uri != null) {
-            note.addPerson(uri.toString());
-        } else if (mode == MODE_PHONE) {
-            note.addPerson(Uri.fromParts("tel", phone, null).toString());
-        } else if (mode == MODE_EMAIL) {
-            note.addPerson(Uri.fromParts("mailto", email, null).toString());
-        }
-    }
-
-    // Tests
-
-    private void testIsEnabled(int i) {
-        // no setup required
-        Intent settings = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
-        if (settings.resolveActivity(mPackageManager) == null) {
-            logWithStack("failed testIsEnabled: no settings activity");
-            mStatus[i] = FAIL;
-        } else {
-            // TODO: find out why Secure.ENABLED_NOTIFICATION_LISTENERS is hidden
-            String listeners = Secure.getString(getContentResolver(),
-                    "enabled_notification_listeners");
-            if (listeners != null && listeners.contains(LISTENER_PATH)) {
-                mStatus[i] = PASS;
-            } else {
-                mStatus[i] = WAIT_FOR_USER;
-            }
-        }
-        next();
-    }
-
-    private void testIsStarted(final int i) {
-        if (mStatus[i] == SETUP) {
-            mStatus[i] = READY;
-            // wait for the service to start
-            delay();
-        } else {
-            MockListener.probeListenerStatus(mContext,
-                    new MockListener.StatusCatcher() {
-                        @Override
-                        public void accept(int result) {
-                            if (result == Activity.RESULT_OK) {
-                                mStatus[i] = PASS;
-                            } else {
-                                logWithStack("failed testIsStarted: " + result);
-                                mStatus[i] = FAIL;
-                            }
-                            next();
-                        }
-                    });
-        }
-    }
-
-    private void testModeAll(final int i) {
-        if (mStatus[i] == READY || mStatus[i] == SETUP) {
-            MockListener.probeFilter(mContext,
-                    new MockListener.IntegerResultCatcher() {
-                        @Override
-                        public void accept(int mode) {
-                            if (mode == NotificationListenerService.INTERRUPTION_FILTER_ALL) {
-                                mStatus[i] = PASS;
-                            } else {
-                                logWithStack("waiting testModeAll: " + mode);
-                                mStatus[i] = WAIT_FOR_USER;
-                            }
-                            next();
-                        }
-                    });
-        }
-    }
-
-    private void testModePriority(final int i) {
-        if (mStatus[i] == READY || mStatus[i] == SETUP) {
-            MockListener.probeFilter(mContext,
-                    new MockListener.IntegerResultCatcher() {
-                        @Override
-                        public void accept(int mode) {
-                            if (mode == NotificationListenerService.INTERRUPTION_FILTER_PRIORITY) {
-                                mStatus[i] = PASS;
-                            } else {
-                                logWithStack("waiting testModePriority: " + mode);
-                                mStatus[i] = WAIT_FOR_USER;
-                            }
-                            next();
-                        }
-                    });
-        }
-    }
-
-    private void testModeNone(final int i) {
-        if (mStatus[i] == READY || mStatus[i] == SETUP) {
-            MockListener.probeFilter(mContext,
-                    new MockListener.IntegerResultCatcher() {
-                        @Override
-                        public void accept(int mode) {
-                            if (mode == NotificationListenerService.INTERRUPTION_FILTER_NONE) {
-                                mStatus[i] = PASS;
-                            } else {
-                                logWithStack("waiting testModeNone: " + mode);
-                                mStatus[i] = WAIT_FOR_USER;
-                            }
-                            next();
-                        }
-                    });
-        }
-    }
-
-
-    private void insertSingleContact(String name, String phone, String email, boolean starred) {
-        final ArrayList<ContentProviderOperation> operationList =
-                new ArrayList<ContentProviderOperation>();
-        ContentProviderOperation.Builder builder =
-                ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI);
-        builder.withValue(ContactsContract.RawContacts.STARRED, starred ? 1 : 0);
-        operationList.add(builder.build());
-
-        builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
-        builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0);
-        builder.withValue(ContactsContract.Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
-        builder.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
-        operationList.add(builder.build());
-
-        if (phone != null) {
-            builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
-            builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0);
-            builder.withValue(ContactsContract.Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
-            builder.withValue(Phone.TYPE, Phone.TYPE_MOBILE);
-            builder.withValue(Phone.NUMBER, phone);
-            builder.withValue(ContactsContract.Data.IS_PRIMARY, 1);
-            operationList.add(builder.build());
-        }
-        if (email != null) {
-            builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
-            builder.withValueBackReference(Email.RAW_CONTACT_ID, 0);
-            builder.withValue(ContactsContract.Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
-            builder.withValue(Email.TYPE, Email.TYPE_HOME);
-            builder.withValue(Email.DATA, email);
-            operationList.add(builder.build());
-        }
-
-        try {
-            mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operationList);
-        } catch (RemoteException e) {
-            Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
-        } catch (OperationApplicationException e) {
-            Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
-        }
-    }
-
-    private Uri lookupContact(String phone) {
-        Cursor c = null;
-        try {
-            Uri phoneUri = Uri.withAppendedPath(ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
-                    Uri.encode(phone));
-            String[] projection = new String[] { ContactsContract.Contacts._ID,
-                    ContactsContract.Contacts.LOOKUP_KEY };
-            c = mContext.getContentResolver().query(phoneUri, projection, null, null, null);
-            if (c != null && c.getCount() > 0) {
-                c.moveToFirst();
-                int lookupIdx = c.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY);
-                int idIdx = c.getColumnIndex(ContactsContract.Contacts._ID);
-                String lookupKey = c.getString(lookupIdx);
-                long contactId = c.getLong(idIdx);
-                return ContactsContract.Contacts.getLookupUri(contactId, lookupKey);
-            }
-        } catch (Throwable t) {
-            Log.w(TAG, "Problem getting content resolver or performing contacts query.", t);
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        return null;
-    }
-
-    private void testInsertContacts(final int i) {
-        if (mStatus[i] == SETUP) {
-            insertSingleContact(ALICE, ALICE_PHONE, ALICE_EMAIL, true);
-            insertSingleContact(BOB, BOB_PHONE, BOB_EMAIL, false);
-            // charlie is not in contacts
-            mStatus[i] = READY;
-            // wait for insertions to move through the system
-            delay();
-        } else {
-            mAliceUri = lookupContact(ALICE_PHONE);
-            mBobUri = lookupContact(BOB_PHONE);
-            mCharlieUri = lookupContact(CHARLIE_PHONE);
-
-            mStatus[i] = PASS;
-            if (mAliceUri == null) { mStatus[i] = FAIL; }
-            if (mBobUri == null) { mStatus[i] = FAIL; }
-            if (mCharlieUri != null) { mStatus[i] = FAIL; }
-            next();
-        }
-    }
-
-    // ordered by time: C, B, A
-    private void testDefaultOrder(final int i) {
-        if (mStatus[i] == SETUP) {
-            mNm.cancelAll();
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
-            sendNotifications(MODE_NONE, false, false);
-            mStatus[i] = READY;
-            // wait for notifications to move through the system
-            delay();
-        } else {
-            MockListener.probeListenerOrder(mContext,
-                    new MockListener.StringListResultCatcher() {
-                        @Override
-                        public void accept(List<String> orderedKeys) {
-                            int rankA = findTagInKeys(ALICE, orderedKeys);
-                            int rankB = findTagInKeys(BOB, orderedKeys);
-                            int rankC = findTagInKeys(CHARLIE, orderedKeys);
-                            if (rankC < rankB && rankB < rankA) {
-                                mStatus[i] = PASS;
-                            } else {
-                                logWithStack("failed testDefaultOrder : "
-                                        + rankA + ", " + rankB + ", " + rankC);
-                                mStatus[i] = FAIL;
-                            }
-                            next();
-                        }
-                    });
-        }
-    }
-
-    // ordered by priority: B, C, A
-    private void testPrioritytOrder(final int i) {
-        if (mStatus[i] == SETUP) {
-            mNm.cancelAll();
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
-            sendNotifications(MODE_PHONE, true, false);
-            mStatus[i] = READY;
-            // wait for notifications to move through the system
-            delay();
-        } else {
-            MockListener.probeListenerOrder(mContext,
-                    new MockListener.StringListResultCatcher() {
-                        @Override
-                        public void accept(List<String> orderedKeys) {
-                            int rankA = findTagInKeys(ALICE, orderedKeys);
-                            int rankB = findTagInKeys(BOB, orderedKeys);
-                            int rankC = findTagInKeys(CHARLIE, orderedKeys);
-                            if (rankB < rankC && rankC < rankA) {
-                                mStatus[i] = PASS;
-                            } else {
-                                logWithStack("failed testPrioritytOrder : "
-                                        + rankA + ", " + rankB + ", " + rankC);
-                                mStatus[i] = FAIL;
-                            }
-                            next();
-                        }
-                    });
-        }
-    }
-
-    // B & C above the fold, A below
-    private void testAmbientBits(final int i) {
-        if (mStatus[i] == SETUP) {
-            mNm.cancelAll();
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
-            sendNotifications(MODE_PHONE, true, false);
-            mStatus[i] = READY;
-            // wait for notifications to move through the system
-            delay();
-        } else {
-            MockListener.probeListenerPayloads(mContext,
-                    new MockListener.StringListResultCatcher() {
-                        @Override
-                        public void accept(List<String> result) {
-                            boolean pass = false;
-                            Set<String> found = new HashSet<String>();
-                            if (result != null && result.size() > 0) {
-                                pass = true;
-                                for (String payloadData : result) {
-                                    try {
-                                        JSONObject payload = new JSONObject(payloadData);
-                                        String tag = payload.getString(JSON_TAG);
-                                        if (found.contains(tag)) {
-                                            // multiple entries for same notification!
-                                            pass = false;
-                                        } else if (ALICE.equals(tag)) {
-                                            found.add(ALICE);
-                                            pass &= payload.getBoolean(JSON_AMBIENT);
-                                        } else if (BOB.equals(tag)) {
-                                            found.add(BOB);
-                                            pass &= !payload.getBoolean(JSON_AMBIENT);
-                                        } else if (CHARLIE.equals(tag)) {
-                                            found.add(CHARLIE);
-                                            pass &= !payload.getBoolean(JSON_AMBIENT);
-                                        }
-                                    } catch (JSONException e) {
-                                        pass = false;
-                                        Log.e(TAG, "failed to unpack data from mocklistener", e);
-                                    }
-                                }
-                            }
-                            pass &= found.size() == 3;
-                            mStatus[i] = pass ? PASS : FAIL;
-                            next();
-                        }
-                    });
-        }
-    }
-
-    // ordered by contact affinity: A, B, C
-    private void testLookupUriOrder(final int i) {
-        if (mStatus[i] == SETUP) {
-            mNm.cancelAll();
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
-            sendNotifications(MODE_URI, false, false);
-            mStatus[i] = READY;
-            // wait for notifications to move through the system
-            delay();
-        } else {
-            MockListener.probeListenerOrder(mContext,
-                    new MockListener.StringListResultCatcher() {
-                        @Override
-                        public void accept(List<String> orderedKeys) {
-                            int rankA = findTagInKeys(ALICE, orderedKeys);
-                            int rankB = findTagInKeys(BOB, orderedKeys);
-                            int rankC = findTagInKeys(CHARLIE, orderedKeys);
-                            if (rankA < rankB && rankB < rankC) {
-                                mStatus[i] = PASS;
-                            } else {
-                                logWithStack("failed testLookupUriOrder : "
-                                        + rankA + ", " + rankB + ", " + rankC);
-                                mStatus[i] = FAIL;
-                            }
-                            next();
-                        }
-                    });
-        }
-    }
-
-    // ordered by contact affinity: A, B, C
-    private void testEmailOrder(final int i) {
-        if (mStatus[i] == SETUP) {
-            mNm.cancelAll();
-            MockListener.resetListenerData(this);
-            mStatus[i] = DELAYED_SETUP;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == DELAYED_SETUP) {
-            sendNotifications(MODE_EMAIL, false, false);
-            mStatus[i] = READY;
-            // wait for notifications to move through the system
-            delay();
-        } else {
-            MockListener.probeListenerOrder(mContext,
-                    new MockListener.StringListResultCatcher() {
-                        @Override
-                        public void accept(List<String> orderedKeys) {
-                            int rankA = findTagInKeys(ALICE, orderedKeys);
-                            int rankB = findTagInKeys(BOB, orderedKeys);
-                            int rankC = findTagInKeys(CHARLIE, orderedKeys);
-                            if (rankA < rankB && rankB < rankC) {
-                                mStatus[i] = PASS;
-                            } else {
-                                logWithStack("failed testEmailOrder : "
-                                        + rankA + ", " + rankB + ", " + rankC);
-                                mStatus[i] = FAIL;
-                            }
-                            next();
-                        }
-                    });
-        }
-    }
-
-    // ordered by contact affinity: A, B, C
-    private void testPhoneOrder(final int i) {
-        if (mStatus[i] == SETUP) {
-            mNm.cancelAll();
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
-            sendNotifications(MODE_PHONE, false, false);
-            mStatus[i] = READY;
-            // wait for notifications to move through the system
-            delay();
-        } else {
-            MockListener.probeListenerOrder(mContext,
-                    new MockListener.StringListResultCatcher() {
-                        @Override
-                        public void accept(List<String> orderedKeys) {
-                            int rankA = findTagInKeys(ALICE, orderedKeys);
-                            int rankB = findTagInKeys(BOB, orderedKeys);
-                            int rankC = findTagInKeys(CHARLIE, orderedKeys);
-                            if (rankA < rankB && rankB < rankC) {
-                                mStatus[i] = PASS;
-                            } else {
-                                logWithStack("failed testPhoneOrder : "
-                                        + rankA + ", " + rankB + ", " + rankC);
-                                mStatus[i] = FAIL;
-                            }
-                            next();
-                        }
-                    });
-        }
-    }
-
-    // A starts at the top then falls to the bottom
-    private void testInterruptionOrder(final int i) {
-        if (mStatus[i] == SETUP) {
-            mNm.cancelAll();
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
-            sendNotifications(MODE_NONE, false, true);
-            mStatus[i] = READY;
-            // wait for notifications to move through the system
-            delay();
-        } else if (mStatus[i] == READY) {
-            MockListener.probeListenerOrder(mContext,
-                    new MockListener.StringListResultCatcher() {
-                        @Override
-                        public void accept(List<String> orderedKeys) {
-                            int rankA = findTagInKeys(ALICE, orderedKeys);
-                            int rankB = findTagInKeys(BOB, orderedKeys);
-                            int rankC = findTagInKeys(CHARLIE, orderedKeys);
-                            if (rankA < rankB && rankA < rankC) {
-                                mStatus[i] = RETRY;
-                                delay(12000);
-                            } else {
-                                logWithStack("noisy notification did not sort to top.");
-                                mStatus[i] = FAIL;
-                                next();
-                            }
-                        }
-                    });
-        } else if (mStatus[i] == RETRY) {
-            MockListener.probeListenerOrder(mContext,
-                    new MockListener.StringListResultCatcher() {
-                        @Override
-                        public void accept(List<String> orderedKeys) {
-                            int rankA = findTagInKeys(ALICE, orderedKeys);
-                            int rankB = findTagInKeys(BOB, orderedKeys);
-                            int rankC = findTagInKeys(CHARLIE, orderedKeys);
-                            if (rankA > rankB && rankA > rankC) {
-                                mStatus[i] = PASS;
-                            } else {
-                                logWithStack("noisy notification did not fade back into the list.");
-                                mStatus[i] = FAIL;
-                            }
-                            next();
-                        }
-                    });
-        }
-    }
-
-    // Nothing should be filtered when mode is ALL
-    private void testAllInterceptsNothing(final int i) {
-        if (mStatus[i] == SETUP) {
-            mNm.cancelAll();
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
-            sendNotifications(MODE_URI, false, false);
-            mStatus[i] = READY;
-            // wait for notifications to move through the system
-            delay();
-        } else {
-            MockListener.probeListenerPayloads(mContext,
-                    new MockListener.StringListResultCatcher() {
-                        @Override
-                        public void accept(List<String> result) {
-                            boolean pass = false;
-                            Set<String> found = new HashSet<String>();
-                            if (result != null && result.size() > 0) {
-                                pass = true;
-                                for (String payloadData : result) {
-                                    try {
-                                        JSONObject payload = new JSONObject(payloadData);
-                                        String tag = payload.getString(JSON_TAG);
-                                        if (found.contains(tag)) {
-                                            // multiple entries for same notification!
-                                            pass = false;
-                                        } else if (ALICE.equals(tag)) {
-                                            found.add(ALICE);
-                                            pass &= payload.getBoolean(JSON_MATCHES_ZEN_FILTER);
-                                        } else if (BOB.equals(tag)) {
-                                            found.add(BOB);
-                                            pass &= payload.getBoolean(JSON_MATCHES_ZEN_FILTER);
-                                        } else if (CHARLIE.equals(tag)) {
-                                            found.add(CHARLIE);
-                                            pass &= payload.getBoolean(JSON_MATCHES_ZEN_FILTER);
-                                        }
-                                    } catch (JSONException e) {
-                                        pass = false;
-                                        Log.e(TAG, "failed to unpack data from mocklistener", e);
-                                    }
-                                }
-                            }
-                            pass &= found.size() == 3;
-                            mStatus[i] = pass ? PASS : FAIL;
-                            next();
-                        }
-                    });
-        }
-    }
-
-    // A should be filtered when mode is Priority/Starred.
-    private void testPriorityInterceptsSome(final int i) {
-        if (mStatus[i] == SETUP) {
-            mNm.cancelAll();
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
-            sendNotifications(MODE_URI, false, false);
-            mStatus[i] = READY;
-            // wait for notifications to move through the system
-            delay();
-        } else {
-            MockListener.probeListenerPayloads(mContext,
-                    new MockListener.StringListResultCatcher() {
-                        @Override
-                        public void accept(List<String> result) {
-                            boolean pass = false;
-                            Set<String> found = new HashSet<String>();
-                            if (result != null && result.size() > 0) {
-                                pass = true;
-                                for (String payloadData : result) {
-                                    try {
-                                        JSONObject payload = new JSONObject(payloadData);
-                                        String tag = payload.getString(JSON_TAG);
-                                        if (found.contains(tag)) {
-                                            // multiple entries for same notification!
-                                            pass = false;
-                                        } else if (ALICE.equals(tag)) {
-                                            found.add(ALICE);
-                                            pass &= payload.getBoolean(JSON_MATCHES_ZEN_FILTER);
-                                        } else if (BOB.equals(tag)) {
-                                            found.add(BOB);
-                                            pass &= !payload.getBoolean(JSON_MATCHES_ZEN_FILTER);
-                                        } else if (CHARLIE.equals(tag)) {
-                                            found.add(CHARLIE);
-                                            pass &= !payload.getBoolean(JSON_MATCHES_ZEN_FILTER);
-                                        }
-                                    } catch (JSONException e) {
-                                        pass = false;
-                                        Log.e(TAG, "failed to unpack data from mocklistener", e);
-                                    }
-                                }
-                            }
-                            pass &= found.size() == 3;
-                            mStatus[i] = pass ? PASS : FAIL;
-                            next();
-                        }
-                    });
-        }
-    }
-
-    // Nothing should get through when mode is None.
-    private void testNoneInterceptsAll(final int i) {
-        if (mStatus[i] == SETUP) {
-            mNm.cancelAll();
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
-            sendNotifications(MODE_URI, false, false);
-            mStatus[i] = READY;
-            // wait for notifications to move through the system
-            delay();
-        } else {
-            MockListener.probeListenerPayloads(mContext,
-                    new MockListener.StringListResultCatcher() {
-                        @Override
-                        public void accept(List<String> result) {
-                            boolean pass = false;
-                            Set<String> found = new HashSet<String>();
-                            if (result != null && result.size() > 0) {
-                                pass = true;
-                                for (String payloadData : result) {
-                                    try {
-                                        JSONObject payload = new JSONObject(payloadData);
-                                        String tag = payload.getString(JSON_TAG);
-                                        if (found.contains(tag)) {
-                                            // multiple entries for same notification!
-                                            pass = false;
-                                        } else if (ALICE.equals(tag)) {
-                                            found.add(ALICE);
-                                            pass &= !payload.getBoolean(JSON_MATCHES_ZEN_FILTER);
-                                        } else if (BOB.equals(tag)) {
-                                            found.add(BOB);
-                                            pass &= !payload.getBoolean(JSON_MATCHES_ZEN_FILTER);
-                                        } else if (CHARLIE.equals(tag)) {
-                                            found.add(CHARLIE);
-                                            pass &= !payload.getBoolean(JSON_MATCHES_ZEN_FILTER);
-                                        }
-                                    } catch (JSONException e) {
-                                        pass = false;
-                                        Log.e(TAG, "failed to unpack data from mocklistener", e);
-                                    }
-                                }
-                            }
-                            pass &= found.size() == 3;
-                            mStatus[i] = pass ? PASS : FAIL;
-                            next();
-                        }
-                    });
-        }
-    }
-
-    /** Search a list of notification keys for a givcen tag. */
-    private int findTagInKeys(String tag, List<String> orderedKeys) {
-        for (int i = 0; i < orderedKeys.size(); i++) {
-            if (orderedKeys.get(i).contains(tag)) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
-    private void testDeleteContacts(final int i) {
-        if (mStatus[i] == SETUP) {
-            final ArrayList<ContentProviderOperation> operationList =
-                    new ArrayList<ContentProviderOperation>();
-            operationList.add(ContentProviderOperation.newDelete(mAliceUri).build());
-            operationList.add(ContentProviderOperation.newDelete(mBobUri).build());
-            try {
-                mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operationList);
-                mStatus[i] = READY;
-            } catch (RemoteException e) {
-                Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
-                mStatus[i] = FAIL;
-            } catch (OperationApplicationException e) {
-                Log.e(TAG, String.format("%s: %s", e.toString(), e.getMessage()));
-                mStatus[i] = FAIL;
-            }
-            // wait for deletions to move through the system
-            delay(3000);
-        } else if (mStatus[i] == READY) {
-            mAliceUri = lookupContact(ALICE_PHONE);
-            mBobUri = lookupContact(BOB_PHONE);
-            mCharlieUri = lookupContact(CHARLIE_PHONE);
-
-            mStatus[i] = PASS;
-            if (mAliceUri != null) { mStatus[i] = FAIL; }
-            if (mBobUri != null) { mStatus[i] = FAIL; }
-            if (mCharlieUri != null) { mStatus[i] = FAIL; }
-            next();
-        }
-    }
-}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/NotificationListenerVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/NotificationListenerVerifierActivity.java
index 0ef595b..ace194c 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/notifications/NotificationListenerVerifierActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/notifications/NotificationListenerVerifierActivity.java
@@ -16,76 +16,30 @@
 
 package com.android.cts.verifier.notifications;
 
-import static com.android.cts.verifier.notifications.MockListener.JSON_FLAGS;
-import static com.android.cts.verifier.notifications.MockListener.JSON_ICON;
-import static com.android.cts.verifier.notifications.MockListener.JSON_ID;
-import static com.android.cts.verifier.notifications.MockListener.JSON_PACKAGE;
-import static com.android.cts.verifier.notifications.MockListener.JSON_TAG;
-import static com.android.cts.verifier.notifications.MockListener.JSON_WHEN;
-
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.os.IBinder;
 import android.provider.Settings.Secure;
 import android.util.Log;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
 
-import com.android.cts.verifier.PassFailButtons;
 import com.android.cts.verifier.R;
-import com.android.cts.verifier.nfc.TagVerifierActivity;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.UUID;
-import java.util.concurrent.LinkedBlockingQueue;
 
-public class NotificationListenerVerifierActivity extends PassFailButtons.Activity
-implements Runnable {
-    private static final String TAG = TagVerifierActivity.class.getSimpleName();
-    private static final String STATE = "state";
-    private static LinkedBlockingQueue<String> sDeletedQueue = new LinkedBlockingQueue<String>();
+import static com.android.cts.verifier.notifications.MockListener.*;
 
-    protected static final String LISTENER_PATH = "com.android.cts.verifier/" +
-            "com.android.cts.verifier.notifications.MockListener";
-    protected static final int SETUP = 0;
-    protected static final int PASS = 1;
-    protected static final int FAIL = 2;
-    protected static final int WAIT_FOR_USER = 3;
-    protected static final int CLEARED = 4;
-    protected static final int READY = 5;
-    protected static final int RETRY = 6;
-
-    protected static final int NOTIFICATION_ID = 1001;
-
-    protected int mState;
-    protected int[] mStatus;
-    protected PackageManager mPackageManager;
-    protected NotificationManager mNm;
-    protected Context mContext;
-    protected Runnable mRunner;
-    protected View mHandler;
-    protected String mPackageString;
-
-    private LayoutInflater mInflater;
-    private ViewGroup mItemList;
+public class NotificationListenerVerifierActivity extends InteractiveVerifierActivity
+        implements Runnable {
+    private static final String TAG = "NoListenerVerifier";
 
     private String mTag1;
     private String mTag2;
@@ -103,199 +57,31 @@
     private int mFlag2;
     private int mFlag3;
 
-    public static class DismissService extends Service {
-        @Override
-        public IBinder onBind(Intent intent) {
-            return null;
-        }
-
-        @Override
-        public void onStart(Intent intent, int startId) {
-            sDeletedQueue.offer(intent.getAction());
-        }
+    @Override
+    int getTitleResource() {
+        return R.string.nls_test;
     }
 
     @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        onCreate(savedInstanceState, R.layout.nls_main);
-        setInfoResources(R.string.nls_test, R.string.nls_info, -1);
+    int getInstructionsResource() {
+        return R.string.nls_info;
     }
 
-    protected void onCreate(Bundle savedInstanceState, int layoutId) {
-        super.onCreate(savedInstanceState);
-
-        if (savedInstanceState != null) {
-            mState = savedInstanceState.getInt(STATE, 0);
-        }
-        mContext = this;
-        mRunner = this;
-        mNm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-        mPackageManager = getPackageManager();
-        mInflater = getLayoutInflater();
-        View view = mInflater.inflate(layoutId, null);
-        mItemList = (ViewGroup) view.findViewById(R.id.nls_test_items);
-        mHandler = mItemList;
-        createTestItems();
-        mStatus = new int[mItemList.getChildCount()];
-        setContentView(view);
-
-        setPassFailButtonClickListeners();
-        getPassButton().setEnabled(false);
-    }
+    // Test Setup
 
     @Override
-    protected void onSaveInstanceState (Bundle outState) {
-        outState.putInt(STATE, mState);
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        next();
-    }
-
-    // Interface Utilities
-
-    protected void createTestItems() {
-        createNlsSettingsItem(R.string.nls_enable_service);
-        createAutoItem(R.string.nls_service_started);
-        createAutoItem(R.string.nls_note_received);
-        createAutoItem(R.string.nls_payload_intact);
-        createAutoItem(R.string.nls_clear_one);
-        createAutoItem(R.string.nls_clear_all);
-        createNlsSettingsItem(R.string.nls_disable_service);
-        createAutoItem(R.string.nls_service_stopped);
-        createAutoItem(R.string.nls_note_missed);
-    }
-
-    protected void setItemState(int index, boolean passed) {
-        ViewGroup item = (ViewGroup) mItemList.getChildAt(index);
-        ImageView status = (ImageView) item.findViewById(R.id.nls_status);
-        status.setImageResource(passed ? R.drawable.fs_good : R.drawable.fs_error);
-        View button = item.findViewById(R.id.nls_action_button);
-        button.setClickable(false);
-        button.setEnabled(false);
-        status.invalidate();
-    }
-
-    protected void markItemWaiting(int index) {
-        ViewGroup item = (ViewGroup) mItemList.getChildAt(index);
-        ImageView status = (ImageView) item.findViewById(R.id.nls_status);
-        status.setImageResource(R.drawable.fs_warning);
-        status.invalidate();
-    }
-
-    protected View createNlsSettingsItem(int messageId) {
-        return createUserItem(messageId, R.string.nls_start_settings);
-    }
-
-    protected View createRetryItem(int messageId) {
-        return createUserItem(messageId, R.string.attention_ready);
-    }
-
-    protected View createUserItem(int messageId, int actionId) {
-        View item = mInflater.inflate(R.layout.nls_item, mItemList, false);
-        TextView instructions = (TextView) item.findViewById(R.id.nls_instructions);
-        instructions.setText(messageId);
-        Button button = (Button) item.findViewById(R.id.nls_action_button);
-        button.setText(actionId);
-        mItemList.addView(item);
-        button.setTag(actionId);
-        return item;
-    }
-
-    protected View createAutoItem(int stringId) {
-        View item = mInflater.inflate(R.layout.nls_item, mItemList, false);
-        TextView instructions = (TextView) item.findViewById(R.id.nls_instructions);
-        instructions.setText(stringId);
-        View button = item.findViewById(R.id.nls_action_button);
-        button.setVisibility(View.GONE);
-        mItemList.addView(item);
-        return item;
-    }
-
-    // Test management
-
-    public void run() {
-        while (mState < mStatus.length && mStatus[mState] != WAIT_FOR_USER) {
-            if (mStatus[mState] == PASS) {
-                setItemState(mState, true);
-                mState++;
-            } else if (mStatus[mState] == FAIL) {
-                setItemState(mState, false);
-                return;
-            } else {
-                break;
-            }
-        }
-
-        if (mState < mStatus.length && mStatus[mState] == WAIT_FOR_USER) {
-            markItemWaiting(mState);
-        }
-
-        updateStateMachine();
-    }
-
-    protected void updateStateMachine() {
-        switch (mState) {
-            case 0:
-                testIsEnabled(mState);
-                break;
-            case 1:
-                testIsStarted(mState);
-                break;
-            case 2:
-                testNotificationRecieved(mState);
-                break;
-            case 3:
-                testDataIntact(mState);
-                break;
-            case 4:
-                testDismissOne(mState);
-                break;
-            case 5:
-                testDismissAll(mState);
-                break;
-            case 6:
-                testIsDisabled(mState);
-                break;
-            case 7:
-                testIsStopped(mState);
-                break;
-            case 8:
-                testNotificationNotRecieved(mState);
-                break;
-            case 9:
-                getPassButton().setEnabled(true);
-                mNm.cancelAll();
-                break;
-        }
-    }
-
-    public void launchSettings() {
-        startActivity(
-                new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"));
-    }
-
-    public void actionPressed(View v) {
-        Object tag = v.getTag();
-        if (tag instanceof Integer) {
-            int id = ((Integer) tag).intValue();
-            if (id == R.string.nls_start_settings) {
-                launchSettings();
-            } else if (id == R.string.attention_ready) {
-                mStatus[mState] = READY;
-                next();
-            }
-        }
-    }
-
-    protected PendingIntent makeIntent(int code, String tag) {
-        Intent intent = new Intent(tag);
-        intent.setComponent(new ComponentName(mContext, DismissService.class));
-        PendingIntent pi = PendingIntent.getService(mContext, code, intent,
-                PendingIntent.FLAG_UPDATE_CURRENT);
-        return pi;
+    protected List<InteractiveTestCase> createTestItems() {
+        List<InteractiveTestCase> tests = new ArrayList<>(9);
+        tests.add(new IsEnabledTest());
+        tests.add(new ServiceStartedTest());
+        tests.add(new NotificationRecievedTest());
+        tests.add(new DataIntactTest());
+        tests.add(new DismissOneTest());
+        tests.add(new DismissAllTest());
+        tests.add(new IsDisabledTest());
+        tests.add(new ServiceStoppedTest());
+        tests.add(new NotificationNotReceivedTest());
+        return tests;
     }
 
     @SuppressLint("NewApi")
@@ -310,9 +96,9 @@
         mWhen2 = System.currentTimeMillis() + 2;
         mWhen3 = System.currentTimeMillis() + 3;
 
-        mIcon1 = R.drawable.fs_good;
-        mIcon2 = R.drawable.fs_error;
-        mIcon3 = R.drawable.fs_warning;
+        mIcon1 = R.drawable.ic_stat_alice;
+        mIcon2 = R.drawable.ic_stat_bob;
+        mIcon3 = R.drawable.ic_stat_charlie;
 
         mId1 = NOTIFICATION_ID + 1;
         mId2 = NOTIFICATION_ID + 2;
@@ -321,356 +107,352 @@
         mPackageString = "com.android.cts.verifier";
 
         Notification n1 = new Notification.Builder(mContext)
-        .setContentTitle("ClearTest 1")
-        .setContentText(mTag1.toString())
-        .setPriority(Notification.PRIORITY_LOW)
-        .setSmallIcon(mIcon1)
-        .setWhen(mWhen1)
-        .setDeleteIntent(makeIntent(1, mTag1))
-        .setOnlyAlertOnce(true)
-        .build();
+                .setContentTitle("ClearTest 1")
+                .setContentText(mTag1.toString())
+                .setPriority(Notification.PRIORITY_LOW)
+                .setSmallIcon(mIcon1)
+                .setWhen(mWhen1)
+                .setDeleteIntent(makeIntent(1, mTag1))
+                .setOnlyAlertOnce(true)
+                .build();
         mNm.notify(mTag1, mId1, n1);
         mFlag1 = Notification.FLAG_ONLY_ALERT_ONCE;
 
         Notification n2 = new Notification.Builder(mContext)
-        .setContentTitle("ClearTest 2")
-        .setContentText(mTag2.toString())
-        .setPriority(Notification.PRIORITY_HIGH)
-        .setSmallIcon(mIcon2)
-        .setWhen(mWhen2)
-        .setDeleteIntent(makeIntent(2, mTag2))
-        .setAutoCancel(true)
-        .build();
+                .setContentTitle("ClearTest 2")
+                .setContentText(mTag2.toString())
+                .setPriority(Notification.PRIORITY_HIGH)
+                .setSmallIcon(mIcon2)
+                .setWhen(mWhen2)
+                .setDeleteIntent(makeIntent(2, mTag2))
+                .setAutoCancel(true)
+                .build();
         mNm.notify(mTag2, mId2, n2);
         mFlag2 = Notification.FLAG_AUTO_CANCEL;
 
         Notification n3 = new Notification.Builder(mContext)
-        .setContentTitle("ClearTest 3")
-        .setContentText(mTag3.toString())
-        .setPriority(Notification.PRIORITY_LOW)
-        .setSmallIcon(mIcon3)
-        .setWhen(mWhen3)
-        .setDeleteIntent(makeIntent(3, mTag3))
-        .setAutoCancel(true)
-        .setOnlyAlertOnce(true)
-        .build();
+                .setContentTitle("ClearTest 3")
+                .setContentText(mTag3.toString())
+                .setPriority(Notification.PRIORITY_LOW)
+                .setSmallIcon(mIcon3)
+                .setWhen(mWhen3)
+                .setDeleteIntent(makeIntent(3, mTag3))
+                .setAutoCancel(true)
+                .setOnlyAlertOnce(true)
+                .build();
         mNm.notify(mTag3, mId3, n3);
         mFlag3 = Notification.FLAG_ONLY_ALERT_ONCE | Notification.FLAG_AUTO_CANCEL;
     }
 
-    /**
-     * Return to the state machine to progress through the tests.
-     */
-    protected void next() {
-        mHandler.removeCallbacks(mRunner);
-        mHandler.post(mRunner);
-    }
-
-    /**
-     * Wait for things to settle before returning to the state machine.
-     */
-    protected void delay() {
-        delay(2000);
-    }
-
-    /**
-     * Wait for some time.
-     */
-    protected void delay(long waitTime) {
-        mHandler.removeCallbacks(mRunner);
-        mHandler.postDelayed(mRunner, waitTime);
-    }
-
-    protected boolean checkEquals(long expected, long actual, String message) {
-        if (expected == actual) {
-            return true;
-        }
-        logWithStack(String.format(message, expected, actual));
-        return false;
-    }
-
-    protected boolean checkEquals(String expected, String actual, String message) {
-        if (expected.equals(actual)) {
-            return true;
-        }
-        logWithStack(String.format(message, expected, actual));
-        return false;
-    }
-
-    protected boolean checkFlagSet(int expected, int actual, String message) {
-        if ((expected & actual) != 0) {
-            return true;
-        }
-        logWithStack(String.format(message, expected, actual));
-        return false;
-    };
-
-    protected void logWithStack(String message) {
-        Throwable stackTrace = new Throwable();
-        stackTrace.fillInStackTrace();
-        Log.e(TAG, message, stackTrace);
-    }
-
     // Tests
 
-    private void testIsEnabled(int i) {
-        // no setup required
-        Intent settings = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
-        if (settings.resolveActivity(mPackageManager) == null) {
-            logWithStack("failed testIsEnabled: no settings activity");
-            mStatus[i] = FAIL;
-        } else {
-            // TODO: find out why Secure.ENABLED_NOTIFICATION_LISTENERS is hidden
-            String listeners = Secure.getString(getContentResolver(),
-                    "enabled_notification_listeners");
-            if (listeners != null && listeners.contains(LISTENER_PATH)) {
-                mStatus[i] = PASS;
-            } else {
-                mStatus[i] = WAIT_FOR_USER;
-            }
-        }
-        next();
-    }
+    private class NotificationRecievedTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.nls_note_received);
 
-    private void testIsStarted(final int i) {
-        if (mStatus[i] == SETUP) {
-            mStatus[i] = READY;
-            // wait for the service to start
-            delay();
-        } else {
-            MockListener.probeListenerStatus(mContext,
-                    new MockListener.StatusCatcher() {
-                @Override
-                public void accept(int result) {
-                    if (result == Activity.RESULT_OK) {
-                        mStatus[i] = PASS;
-                    } else {
-                        logWithStack("failed testIsStarted: " + result);
-                        mStatus[i] = FAIL;
-                    }
-                    next();
-                }
-            });
         }
-    }
 
-    private void testNotificationRecieved(final int i) {
-        if (mStatus[i] == SETUP) {
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
+        @Override
+        void setUp() {
             sendNotifications();
-            mStatus[i] = READY;
+            status = READY;
             // wait for notifications to move through the system
             delay();
-        } else {
+        }
+
+        @Override
+        void test() {
             MockListener.probeListenerPosted(mContext,
                     new MockListener.StringListResultCatcher() {
-                @Override
-                public void accept(List<String> result) {
-                    if (result != null && result.size() > 0 && result.contains(mTag1)) {
-                        mStatus[i] = PASS;
-                    } else {
-                        logWithStack("failed testNotificationRecieved");
-                        mStatus[i] = FAIL;
-                    }
-                    next();
-                }});
-        }
-    }
-
-    private void testDataIntact(final int i) {
-        // no setup required
-        MockListener.probeListenerPayloads(mContext,
-                new MockListener.StringListResultCatcher() {
-            @Override
-            public void accept(List<String> result) {
-                boolean pass = false;
-                Set<String> found = new HashSet<String>();
-                if (result != null && result.size() > 0) {
-                    pass = true;
-                    for(String payloadData : result) {
-                        try {
-                            JSONObject payload = new JSONObject(payloadData);
-                            pass &= checkEquals(mPackageString, payload.getString(JSON_PACKAGE),
-                                    "data integrity test fail: notification package (%s, %s)");
-                            String tag = payload.getString(JSON_TAG);
-                            if (mTag1.equals(tag)) {
-                                found.add(mTag1);
-                                pass &= checkEquals(mIcon1, payload.getInt(JSON_ICON),
-                                        "data integrity test fail: notification icon (%d, %d)");
-                                pass &= checkFlagSet(mFlag1, payload.getInt(JSON_FLAGS),
-                                        "data integrity test fail: notification flags (%d, %d)");
-                                pass &= checkEquals(mId1, payload.getInt(JSON_ID),
-                                        "data integrity test fail: notification ID (%d, %d)");
-                                pass &= checkEquals(mWhen1, payload.getLong(JSON_WHEN),
-                                        "data integrity test fail: notification when (%d, %d)");
-                            } else if (mTag2.equals(tag)) {
-                                found.add(mTag2);
-                                pass &= checkEquals(mIcon2, payload.getInt(JSON_ICON),
-                                        "data integrity test fail: notification icon (%d, %d)");
-                                pass &= checkFlagSet(mFlag2, payload.getInt(JSON_FLAGS),
-                                        "data integrity test fail: notification flags (%d, %d)");
-                                pass &= checkEquals(mId2, payload.getInt(JSON_ID),
-                                        "data integrity test fail: notification ID (%d, %d)");
-                                pass &= checkEquals(mWhen2, payload.getLong(JSON_WHEN),
-                                        "data integrity test fail: notification when (%d, %d)");
-                            } else if (mTag3.equals(tag)) {
-                                found.add(mTag3);
-                                pass &= checkEquals(mIcon3, payload.getInt(JSON_ICON),
-                                        "data integrity test fail: notification icon (%d, %d)");
-                                pass &= checkFlagSet(mFlag3, payload.getInt(JSON_FLAGS),
-                                        "data integrity test fail: notification flags (%d, %d)");
-                                pass &= checkEquals(mId3, payload.getInt(JSON_ID),
-                                        "data integrity test fail: notification ID (%d, %d)");
-                                pass &= checkEquals(mWhen3, payload.getLong(JSON_WHEN),
-                                        "data integrity test fail: notification when (%d, %d)");
+                        @Override
+                        public void accept(List<String> result) {
+                            if (result != null && result.size() > 0 && result.contains(mTag1)) {
+                                status = PASS;
                             } else {
-                                pass = false;
-                                logWithStack("failed on unexpected notification tag: " + tag);
+                                logFail();
+                                status = FAIL;
                             }
-                        } catch (JSONException e) {
-                            pass = false;
-                            Log.e(TAG, "failed to unpack data from mocklistener", e);
-                        }
-                    }
-                }
-                pass &= found.size() == 3;
-                mStatus[i] = pass ? PASS : FAIL;
-                next();
-            }});
-    }
-
-    private void testDismissOne(final int i) {
-        if (mStatus[i] == SETUP) {
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
-            MockListener.clearOne(mContext, mTag1, NOTIFICATION_ID + 1);
-            mStatus[i] = READY;
-            delay();
-        } else {
-            MockListener.probeListenerRemoved(mContext,
-                    new MockListener.StringListResultCatcher() {
-                @Override
-                public void accept(List<String> result) {
-                    if (result != null && result.size() > 0 && result.contains(mTag1)) {
-                        mStatus[i] = PASS;
-                        next();
-                    } else {
-                        if (mStatus[i] == RETRY) {
-                            logWithStack("failed testDismissOne");
-                            mStatus[i] = FAIL;
                             next();
-                        } else {
-                            logWithStack("failed testDismissOne, once: retrying");
-                            mStatus[i] = RETRY;
-                            delay();
                         }
-                    }
-                }});
+                    });
+            delay();  // in case the catcher never returns
         }
     }
 
-    private void testDismissAll(final int i) {
-        if (mStatus[i] == SETUP) {
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
-            MockListener.clearAll(mContext);
-            mStatus[i] = READY;
-            delay();
-        } else {
-            MockListener.probeListenerRemoved(mContext,
+    private class DataIntactTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.nls_payload_intact);
+        }
+
+        @Override
+        void test() {
+            MockListener.probeListenerPayloads(mContext,
                     new MockListener.StringListResultCatcher() {
-                @Override
-                public void accept(List<String> result) {
-                    if (result != null && result.size() == 2
-                            && result.contains(mTag2) && result.contains(mTag3)) {
-                        mStatus[i] = PASS;
-                        next();
-                    } else {
-                        if (mStatus[i] == RETRY) {
-                            logWithStack("failed testDismissAll");
-                            mStatus[i] = FAIL;
+                        @Override
+                        public void accept(List<String> result) {
+                            Set<String> found = new HashSet<String>();
+                            if (result == null || result.size() == 0) {
+                                status = FAIL;
+                                return;
+                            }
+                            boolean pass = true;
+                            for (String payloadData : result) {
+                                try {
+                                    JSONObject payload = new JSONObject(payloadData);
+                                    pass &= checkEquals(mPackageString,
+                                            payload.getString(JSON_PACKAGE),
+                                            "data integrity test: notification package (%s, %s)");
+                                    String tag = payload.getString(JSON_TAG);
+                                    if (mTag1.equals(tag)) {
+                                        found.add(mTag1);
+                                        pass &= checkEquals(mIcon1, payload.getInt(JSON_ICON),
+                                                "data integrity test: notification icon (%d, %d)");
+                                        pass &= checkFlagSet(mFlag1, payload.getInt(JSON_FLAGS),
+                                                "data integrity test: notification flags (%d, %d)");
+                                        pass &= checkEquals(mId1, payload.getInt(JSON_ID),
+                                                "data integrity test: notification ID (%d, %d)");
+                                        pass &= checkEquals(mWhen1, payload.getLong(JSON_WHEN),
+                                                "data integrity test: notification when (%d, %d)");
+                                    } else if (mTag2.equals(tag)) {
+                                        found.add(mTag2);
+                                        pass &= checkEquals(mIcon2, payload.getInt(JSON_ICON),
+                                                "data integrity test: notification icon (%d, %d)");
+                                        pass &= checkFlagSet(mFlag2, payload.getInt(JSON_FLAGS),
+                                                "data integrity test: notification flags (%d, %d)");
+                                        pass &= checkEquals(mId2, payload.getInt(JSON_ID),
+                                                "data integrity test: notification ID (%d, %d)");
+                                        pass &= checkEquals(mWhen2, payload.getLong(JSON_WHEN),
+                                                "data integrity test: notification when (%d, %d)");
+                                    } else if (mTag3.equals(tag)) {
+                                        found.add(mTag3);
+                                        pass &= checkEquals(mIcon3, payload.getInt(JSON_ICON),
+                                                "data integrity test: notification icon (%d, %d)");
+                                        pass &= checkFlagSet(mFlag3, payload.getInt(JSON_FLAGS),
+                                                "data integrity test: notification flags (%d, %d)");
+                                        pass &= checkEquals(mId3, payload.getInt(JSON_ID),
+                                                "data integrity test: notification ID (%d, %d)");
+                                        pass &= checkEquals(mWhen3, payload.getLong(JSON_WHEN),
+                                                "data integrity test: notification when (%d, %d)");
+                                    } else {
+                                        pass = false;
+                                        logFail("unexpected notification tag: " + tag);
+                                    }
+                                } catch (JSONException e) {
+                                    pass = false;
+                                    Log.e(TAG, "failed to unpack data from mocklistener", e);
+                                }
+                            }
+
+                            pass &= found.size() == 3;
+                            status = pass ? PASS : FAIL;
                             next();
-                        } else {
-                            logWithStack("failed testDismissAll, once: retrying");
-                            mStatus[i] = RETRY;
-                            delay();
                         }
-                    }
-                }
-            });
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
         }
     }
 
-    private void testIsDisabled(int i) {
-        // no setup required
-        // TODO: find out why Secure.ENABLED_NOTIFICATION_LISTENERS is hidden
-        String listeners = Secure.getString(getContentResolver(),
-                "enabled_notification_listeners");
-        if (listeners == null || !listeners.contains(LISTENER_PATH)) {
-            mStatus[i] = PASS;
+    private class DismissOneTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.nls_clear_one);
+        }
+
+        @Override
+        void setUp() {
+            sendNotifications();
+            status = READY;
+            delay();
+        }
+
+        @Override
+        void test() {
+            if (status == READY) {
+                MockListener.clearOne(mContext, mTag1, mId1);
+                status = RETEST;
+            } else {
+                MockListener.probeListenerRemoved(mContext,
+                        new MockListener.StringListResultCatcher() {
+                            @Override
+                            public void accept(List<String> result) {
+                                if (result != null && result.size() != 0
+                                        && result.contains(mTag1)
+                                        && !result.contains(mTag2)
+                                        && !result.contains(mTag3)) {
+                                    status = PASS;
+                                } else {
+                                    logFail();
+                                    status = FAIL;
+                                }
+                                next();
+                            }
+                        });
+            }
+            delay();
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+
+    private class DismissAllTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.nls_clear_all);
+        }
+
+        @Override
+        void setUp() {
+            sendNotifications();
+            status = READY;
+            delay();
+        }
+
+        @Override
+        void test() {
+            if (status == READY) {
+                MockListener.clearAll(mContext);
+                status = RETEST;
+            } else {
+                MockListener.probeListenerRemoved(mContext,
+                        new MockListener.StringListResultCatcher() {
+                            @Override
+                            public void accept(List<String> result) {
+                                if (result != null && result.size() != 0
+                                        && result.contains(mTag1)
+                                        && result.contains(mTag2)
+                                        && result.contains(mTag3)) {
+                                    status = PASS;
+                                } else {
+                                    logFail();
+                                    status = FAIL;
+                                }
+                                next();
+                            }
+                        });
+            }
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
+        }
+    }
+
+    private class IsDisabledTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createNlsSettingsItem(parent, R.string.nls_disable_service);
+        }
+
+        @Override
+        boolean autoStart() {
+            return true;
+        }
+
+        @Override
+        void test() {
+            String listeners = Secure.getString(getContentResolver(),
+                    ENABLED_NOTIFICATION_LISTENERS);
+            if (listeners == null || !listeners.contains(LISTENER_PATH)) {
+                status = PASS;
+            } else {
+                status = WAIT_FOR_USER;
+            }
             next();
-        } else {
-            mStatus[i] = WAIT_FOR_USER;
+        }
+
+        @Override
+        void tearDown() {
+            MockListener.resetListenerData(mContext);
             delay();
         }
     }
 
-    private void testIsStopped(final int i) {
-        if (mStatus[i] == SETUP) {
-            mStatus[i] = READY;
-            // wait for the service to start
-            delay();
-        } else {
+    private class ServiceStoppedTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.nls_service_stopped);
+        }
+
+        @Override
+        void test() {
             MockListener.probeListenerStatus(mContext,
                     new MockListener.StatusCatcher() {
-                @Override
-                public void accept(int result) {
-                    if (result == Activity.RESULT_OK) {
-                        logWithStack("failed testIsStopped");
-                        mStatus[i] = FAIL;
-                    } else {
-                        mStatus[i] = PASS;
-                    }
-                    next();
-                }
-            });
+                        @Override
+                        public void accept(int result) {
+                            if (result == Activity.RESULT_OK) {
+                                logFail();
+                                status = FAIL;
+                            } else {
+                                status = PASS;
+                            }
+                            next();
+                        }
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            // wait for intent to move through the system
+            delay();
         }
     }
 
-    private void testNotificationNotRecieved(final int i) {
-        if (mStatus[i] == SETUP) {
-            MockListener.resetListenerData(this);
-            mStatus[i] = CLEARED;
-            // wait for intent to move through the system
-            delay();
-        } else if (mStatus[i] == CLEARED) {
-            // setup for testNotificationRecieved
+    private class NotificationNotReceivedTest extends InteractiveTestCase {
+        @Override
+        View inflate(ViewGroup parent) {
+            return createAutoItem(parent, R.string.nls_note_missed);
+
+        }
+
+        @Override
+        void setUp() {
             sendNotifications();
-            mStatus[i] = READY;
+            status = READY;
             delay();
-        } else {
+        }
+
+        @Override
+        void test() {
             MockListener.probeListenerPosted(mContext,
                     new MockListener.StringListResultCatcher() {
-                @Override
-                public void accept(List<String> result) {
-                    if (result == null || result.size() == 0) {
-                        mStatus[i] = PASS;
-                    } else {
-                        logWithStack("failed testNotificationNotRecieved");
-                        mStatus[i] = FAIL;
-                    }
-                    next();
-                }});
+                        @Override
+                        public void accept(List<String> result) {
+                            if (result == null || result.size() == 0) {
+                                status = PASS;
+                            } else {
+                                logFail();
+                                status = FAIL;
+                            }
+                            next();
+                        }
+                    });
+            delay();  // in case the catcher never returns
+        }
+
+        @Override
+        void tearDown() {
+            mNm.cancelAll();
+            MockListener.resetListenerData(mContext);
+            delay();
         }
     }
 }