CTS Verifier Sub Tests Implementation

Refactor the Bluetooth test to use the same test results storage
as the main test list. This will make it easy for future tests
to have subtests like the Bluetooth test one does. Also it will
be possible to send a report that has the results for the sub
tests since all the data is in one place.

Create subtests by adding a meta data with the tag "test_parent"
to specify the class that this test belongs to.

Change-Id: Id7ec5da311751bb8c1af7915137dacc89286a23c
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index 9bdb37a..fddbcae 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -56,15 +56,63 @@
         
         <activity android:name=".bluetooth.BluetoothToggleActivity"
                 android:label="@string/bt_toggle_bluetooth"
-                android:configChanges="keyboardHidden|orientation" />
+                android:configChanges="keyboardHidden|orientation">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/bt_control" />
+            <meta-data android:name="test_parent" android:value="com.android.cts.verifier.bluetooth.BluetoothTestActivity" />
+        </activity>
 
+        <activity android:name=".bluetooth.SecureServerActivity"
+                android:label="@string/bt_secure_server"
+                android:configChanges="keyboardHidden|orientation">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/bt_device_communication" />
+            <meta-data android:name="test_parent" android:value="com.android.cts.verifier.bluetooth.BluetoothTestActivity" />
+        </activity>
+        
+        <activity android:name=".bluetooth.InsecureServerActivity"
+                android:label="@string/bt_insecure_server"
+                android:configChanges="keyboardHidden|orientation">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/bt_device_communication" />
+            <meta-data android:name="test_parent" android:value="com.android.cts.verifier.bluetooth.BluetoothTestActivity" />
+        </activity>
+
+        <activity android:name=".bluetooth.SecureClientActivity"
+                android:label="@string/bt_secure_client"
+                android:configChanges="keyboardHidden|orientation">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/bt_device_communication" />
+            <meta-data android:name="test_parent" android:value="com.android.cts.verifier.bluetooth.BluetoothTestActivity" />
+        </activity>
+        
+        <activity android:name=".bluetooth.InsecureClientActivity"
+                android:label="@string/bt_insecure_client"
+                android:configChanges="keyboardHidden|orientation">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+            </intent-filter>
+            <meta-data android:name="test_category" android:value="@string/bt_device_communication" />
+            <meta-data android:name="test_parent" android:value="com.android.cts.verifier.bluetooth.BluetoothTestActivity" />
+        </activity>       
+        
         <activity android:name=".bluetooth.DevicePickerActivity"
                 android:label="@string/bt_device_picker"
                 android:configChanges="keyboardHidden|orientation" />
 
-        <activity android:name=".bluetooth.MessageTestActivity"
-                android:configChanges="keyboardHidden|orientation" />
-
         <activity android:name=".suid.SuidFilesActivity" 
                 android:label="@string/suid_files"
                 android:configChanges="keyboardHidden|orientation">
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
index c563e13..244caab 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
@@ -19,12 +19,8 @@
 import com.android.cts.verifier.TestListAdapter.TestListItem;
 
 import android.app.ListActivity;
-import android.content.ContentResolver;
-import android.content.ContentValues;
 import android.content.Intent;
-import android.database.ContentObserver;
 import android.os.Bundle;
-import android.os.Handler;
 import android.text.ClipboardManager;
 import android.view.Menu;
 import android.view.MenuInflater;
@@ -37,17 +33,14 @@
 public class TestListActivity extends ListActivity {
 
     private static final int LAUNCH_TEST_REQUEST_CODE = 1;
+    private TestListAdapter mAdapter;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
-        TestListAdapter adapter = new TestListAdapter(this);
-        setListAdapter(adapter);
-
-        TestResultContentObserver observer = new TestResultContentObserver(adapter);
-        ContentResolver resolver = getContentResolver();
-        resolver.registerContentObserver(TestResultsProvider.RESULTS_CONTENT_URI, true, observer);
+        mAdapter = new TestListAdapter(this, null);
+        setListAdapter(mAdapter);
+        mAdapter.loadTestResults();
     }
 
     /** Launch the activity when its {@link ListView} item is clicked. */
@@ -59,17 +52,11 @@
     }
 
     private Intent getIntent(int position) {
-        TestListAdapter adapter = getListAdapter();
-        TestListItem item = adapter.getItem(position);
+        TestListItem item = mAdapter.getItem(position);
         return item.intent;
     }
 
     @Override
-    public TestListAdapter getListAdapter() {
-        return (TestListAdapter) super.getListAdapter();
-    }
-
-    @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
         switch (requestCode) {
@@ -85,18 +72,7 @@
     private void handleLaunchTestResult(int resultCode, Intent data) {
         if (resultCode == RESULT_OK) {
             TestResult testResult = TestResult.fromActivityResult(resultCode, data);
-            ContentValues values = new ContentValues(2);
-            values.put(TestResultsProvider.COLUMN_TEST_RESULT, testResult.getResult());
-            values.put(TestResultsProvider.COLUMN_TEST_NAME, testResult.getName());
-
-            ContentResolver resolver = getContentResolver();
-            int numUpdated = resolver.update(TestResultsProvider.RESULTS_CONTENT_URI, values,
-                    TestResultsProvider.COLUMN_TEST_NAME + " = ?",
-                    new String[] {testResult.getName()});
-
-            if (numUpdated == 0) {
-                resolver.insert(TestResultsProvider.RESULTS_CONTENT_URI, values);
-            }
+            mAdapter.setTestResult(testResult);
         }
     }
 
@@ -128,13 +104,12 @@
     }
 
     private void handleClearItemSelected() {
-        ContentResolver resolver = getContentResolver();
-        resolver.delete(TestResultsProvider.RESULTS_CONTENT_URI, "1", null);
+        mAdapter.clearTestResults();
         Toast.makeText(this, R.string.test_results_cleared, Toast.LENGTH_SHORT).show();
     }
 
     private void handleCopyItemSelected() {
-        TestResultsReport report = new TestResultsReport(this, getListAdapter());
+        TestResultsReport report = new TestResultsReport(this, mAdapter);
         ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
         clipboardManager.setText(report.getBody());
         Toast.makeText(this, R.string.test_results_copied, Toast.LENGTH_SHORT).show();
@@ -144,31 +119,9 @@
         Intent target = new Intent(Intent.ACTION_SEND);
         target.setType("text/plain");
 
-        TestResultsReport report = new TestResultsReport(this, getListAdapter());
+        TestResultsReport report = new TestResultsReport(this, mAdapter);
         target.putExtra(Intent.EXTRA_SUBJECT, report.getSubject());
         target.putExtra(Intent.EXTRA_TEXT, report.getBody());
         startActivity(Intent.createChooser(target, getString(R.string.share_test_results)));
     }
-
-    /**
-     * {@link ContentResolver} that refreshes the {@link TestListAdapter} and thus
-     * the {@link ListView} when the test results change.
-     */
-    private static class TestResultContentObserver extends ContentObserver {
-
-        private final TestListAdapter mAdapter;
-
-        public TestResultContentObserver(TestListAdapter adapter) {
-            super(new Handler());
-            this.mAdapter = adapter;
-        }
-
-        @Override
-        public void onChange(boolean selfChange) {
-            super.onChange(selfChange);
-
-            // TODO: Could be improved by just refreshing the particular test result.
-            mAdapter.refreshTestResults();
-        }
-    }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
index 420408e..f6e6f1b 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
@@ -17,13 +17,17 @@
 package com.android.cts.verifier;
 
 import android.content.ContentResolver;
+import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.database.ContentObserver;
 import android.database.Cursor;
+import android.os.AsyncTask;
 import android.os.Bundle;
+import android.os.Handler;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -59,15 +63,22 @@
  *             <meta-data android:name="test_category" android:value="@string/test_category_security" />
  *         </pre>
  *     </li>
+ *     <li>OPTIONAL: Add a meta data attribute to indicate whether this test has a parent test.
+ *         <pre>
+ *             <meta-data android:name="test_parent" android:value="com.android.cts.verifier.bluetooth.BluetoothTestActivity" />
+ *         </pre>
+ *     </li>
  * </ol>
  */
-class TestListAdapter extends BaseAdapter {
+public class TestListAdapter extends BaseAdapter {
 
     /** Activities implementing {@link Intent#ACTION_MAIN} and this will appear in the list. */
     public static final String CATEGORY_MANUAL_TEST = "android.cts.intent.category.MANUAL_TEST";
 
     private static final String TEST_CATEGORY_META_DATA = "test_category";
 
+    private static final String TEST_PARENT_META_DATA = "test_parent";
+
     /** View type for a category of tests like "Sensors" or "Features" */
     private static final int CATEGORY_HEADER_VIEW_TYPE = 0;
 
@@ -79,8 +90,10 @@
 
     private final Context mContext;
 
+    private final String mTestParent;
+
     /** Immutable data of tests like the test's title and launch intent. */
-    private final List<TestListItem> mRows;
+    private final List<TestListItem> mRows = new ArrayList<TestListAdapter.TestListItem>();
 
     /** Mutable test results that will change as each test activity finishes. */
     private final Map<String, Integer> mTestResults = new HashMap<String, Integer>();
@@ -88,7 +101,7 @@
     private final LayoutInflater mLayoutInflater;
 
     /** {@link ListView} row that is either a test category header or a test. */
-    static class TestListItem {
+    public static class TestListItem {
 
         /** Title shown in the {@link ListView}. */
         final String title;
@@ -99,6 +112,9 @@
         /** Intent used to launch the activity from the list. Null for categories. */
         final Intent intent;
 
+        /** Tests within this test. For instance, the Bluetooth test contains more tests. */
+        final List<TestListItem> subItems = new ArrayList<TestListItem>();
+
         static TestListItem newTest(String title, String className, Intent intent) {
             return new TestListItem(title, className, intent);
         }
@@ -113,26 +129,81 @@
             this.intent = intent;
         }
 
+        public Intent getIntent() {
+            return intent;
+        }
+
         boolean isTest() {
             return intent != null;
         }
+
+        void addTestListItem(TestListItem item) {
+            subItems.add(item);
+        }
     }
 
-    TestListAdapter(Context context) {
+    public TestListAdapter(Context context, String testParent) {
         this.mContext = context;
-        this.mRows = getRows(context);
+        this.mTestParent = testParent;
         this.mLayoutInflater =
                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        updateTestResults(mContext, mTestResults);
+
+        TestResultContentObserver observer = new TestResultContentObserver();
+        ContentResolver resolver = context.getContentResolver();
+        resolver.registerContentObserver(TestResultsProvider.RESULTS_CONTENT_URI, true, observer);
     }
 
-    static List<TestListItem> getRows(Context context) {
+    public void loadTestResults() {
+        new RefreshTestResultsTask().execute();
+    }
+
+    public void clearTestResults() {
+        new ClearTestResultsTask().execute();
+    }
+
+    public void setTestResult(TestResult testResult) {
+        new SetTestResultTask(testResult.getName(), testResult.getResult()).execute();
+    }
+
+    class RefreshTestResultsTask extends AsyncTask<Void, Void, RefreshResult> {
+        @Override
+        protected RefreshResult doInBackground(Void... params) {
+            List<TestListItem> rows = getRows();
+            Map<String, Integer> results = getTestResults();
+            return new RefreshResult(rows, results);
+        }
+
+        @Override
+        protected void onPostExecute(RefreshResult result) {
+            super.onPostExecute(result);
+            mRows.clear();
+            mRows.addAll(result.mItems);
+            mTestResults.clear();
+            mTestResults.putAll(result.mResults);
+            notifyDataSetChanged();
+        }
+    }
+
+    static class RefreshResult {
+        List<TestListItem> mItems;
+        Map<String, Integer> mResults;
+
+        RefreshResult(List<TestListItem> items, Map<String, Integer> results) {
+            mItems = items;
+            mResults = results;
+        }
+    }
+
+    List<TestListItem> getRows() {
+
         /*
-         * 1. Get all the tests keyed by their category.
-         * 2. Flatten the tests and categories into one giant list for the list view.
+         * 1. Get all the tests belonging to the test parent.
+         * 2. Get all the tests keyed by their category.
+         * 3. Flatten the tests and categories into one giant list for the list view.
          */
 
-        Map<String, List<TestListItem>> testsByCategory = getTestsByCategory(context);
+        List<ResolveInfo> infos = getResolveInfosForParent();
+        Map<String, List<TestListItem>> testsByCategory = getTestsByCategory(infos);
 
         List<String> testCategories = new ArrayList<String>(testsByCategory.keySet());
         Collections.sort(testCategories);
@@ -152,25 +223,41 @@
         return allRows;
     }
 
-    static Map<String, List<TestListItem>> getTestsByCategory(Context context) {
-        Map<String, List<TestListItem>> testsByCategory =
-                new HashMap<String, List<TestListItem>>();
-
+    List<ResolveInfo> getResolveInfosForParent() {
         Intent mainIntent = new Intent(Intent.ACTION_MAIN);
         mainIntent.addCategory(CATEGORY_MANUAL_TEST);
 
-        PackageManager packageManager = context.getPackageManager();
+        PackageManager packageManager = mContext.getPackageManager();
         List<ResolveInfo> list = packageManager.queryIntentActivities(mainIntent,
                 PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
+        int size = list.size();
 
-        for (int i = 0; i < list.size(); i++) {
+        List<ResolveInfo> matchingList = new ArrayList<ResolveInfo>();
+        for (int i = 0; i < size; i++) {
             ResolveInfo info = list.get(i);
-            String testCategory = getTestCategory(context, info.activityInfo.metaData);
-            String title = getTitle(context, info.activityInfo);
+            String parent = getTestParent(mContext, info.activityInfo.metaData);
+            if ((mTestParent == null && parent == null)
+                    || (mTestParent != null && mTestParent.equals(parent))) {
+                matchingList.add(info);
+            }
+        }
+        return matchingList;
+    }
+
+    Map<String, List<TestListItem>> getTestsByCategory(List<ResolveInfo> list) {
+        Map<String, List<TestListItem>> testsByCategory =
+                new HashMap<String, List<TestListItem>>();
+
+        int size = list.size();
+        for (int i = 0; i < size; i++) {
+            ResolveInfo info = list.get(i);
+            String title = getTitle(mContext, info.activityInfo);
             String className = info.activityInfo.name;
             Intent intent = getActivityIntent(info.activityInfo);
+            TestListItem item = TestListItem.newTest(title, className, intent);
 
-            addTestToCategory(testsByCategory, testCategory, title, className, intent);
+            String testCategory = getTestCategory(mContext, info.activityInfo.metaData);
+            addTestToCategory(testsByCategory, testCategory, item);
         }
 
         return testsByCategory;
@@ -188,6 +275,10 @@
         }
     }
 
+    static String getTestParent(Context context, Bundle metaData) {
+        return metaData != null ? metaData.getString(TEST_PARENT_META_DATA) : null;
+    }
+
     static String getTitle(Context context, ActivityInfo activityInfo) {
         if (activityInfo.labelRes != 0) {
             return context.getString(activityInfo.labelRes);
@@ -203,7 +294,7 @@
     }
 
     static void addTestToCategory(Map<String, List<TestListItem>> testsByCategory,
-            String testCategory, String title, String className, Intent intent) {
+            String testCategory, TestListItem item) {
         List<TestListItem> tests;
         if (testsByCategory.containsKey(testCategory)) {
             tests = testsByCategory.get(testCategory);
@@ -211,12 +302,12 @@
             tests = new ArrayList<TestListItem>();
         }
         testsByCategory.put(testCategory, tests);
-        tests.add(TestListItem.newTest(title, className, intent));
+        tests.add(item);
     }
 
-    static void updateTestResults(Context context, Map<String, Integer> testResults) {
-        testResults.clear();
-        ContentResolver resolver = context.getContentResolver();
+    Map<String, Integer> getTestResults() {
+        Map<String, Integer> results = new HashMap<String, Integer>();
+        ContentResolver resolver = mContext.getContentResolver();
         Cursor cursor = null;
         try {
             cursor = resolver.query(TestResultsProvider.RESULTS_CONTENT_URI,
@@ -225,7 +316,7 @@
                 do {
                     String className = cursor.getString(1);
                     int testResult = cursor.getInt(2);
-                    testResults.put(className, testResult);
+                    results.put(className, testResult);
                 } while (cursor.moveToNext());
             }
         } finally {
@@ -233,11 +324,59 @@
                 cursor.close();
             }
         }
+        return results;
     }
 
-    public void refreshTestResults() {
-        updateTestResults(mContext, mTestResults);
-        notifyDataSetChanged();
+    class ClearTestResultsTask extends AsyncTask<Void, Void, Void> {
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            ContentResolver resolver = mContext.getContentResolver();
+            resolver.delete(TestResultsProvider.RESULTS_CONTENT_URI, "1", null);
+            return null;
+        }
+    }
+
+    class SetTestResultTask extends AsyncTask<Void, Void, Void> {
+
+        private final String mTestName;
+
+        private final int mResult;
+
+        SetTestResultTask(String testName, int result) {
+            mTestName = testName;
+            mResult = result;
+        }
+
+        @Override
+        protected Void doInBackground(Void... params) {
+            ContentValues values = new ContentValues(2);
+            values.put(TestResultsProvider.COLUMN_TEST_RESULT, mResult);
+            values.put(TestResultsProvider.COLUMN_TEST_NAME, mTestName);
+
+            ContentResolver resolver = mContext.getContentResolver();
+            int numUpdated = resolver.update(TestResultsProvider.RESULTS_CONTENT_URI, values,
+                    TestResultsProvider.COLUMN_TEST_NAME + " = ?",
+                    new String[] {mTestName});
+
+            if (numUpdated == 0) {
+                resolver.insert(TestResultsProvider.RESULTS_CONTENT_URI, values);
+            }
+            return null;
+        }
+    }
+
+    class TestResultContentObserver extends ContentObserver {
+
+        public TestResultContentObserver() {
+            super(new Handler());
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            super.onChange(selfChange);
+            loadTestResults();
+        }
     }
 
     @Override
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/BluetoothTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/BluetoothTestActivity.java
index b617fc2..2beff93 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/BluetoothTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/BluetoothTestActivity.java
@@ -18,62 +18,21 @@
 
 import com.android.cts.verifier.PassFailButtons;
 import com.android.cts.verifier.R;
+import com.android.cts.verifier.TestListAdapter;
+import com.android.cts.verifier.TestListAdapter.TestListItem;
 import com.android.cts.verifier.TestResult;
 
 import android.app.AlertDialog;
 import android.bluetooth.BluetoothAdapter;
-import android.content.ContentValues;
-import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.os.AsyncTask;
 import android.os.Bundle;
-import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
 import android.widget.ListView;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
 
 public class BluetoothTestActivity extends PassFailButtons.ListActivity {
 
-    public static final int TEST_BLUETOOTH_TOGGLE = 0;
-    public static final int TEST_SECURE_SERVER = 1;
-    public static final int TEST_INSECURE_SERVER = 2;
-    public static final int TEST_SECURE_CLIENT = 3;
-    public static final int TEST_INSECURE_CLIENT = 4;
-
-    private static final int START_TOGGLE_BLUETOOTH_TEST_REQUEST = 1;
-    private static final int START_SECURE_SERVER_REQUEST = 2;
-    private static final int START_INSECURE_SERVER_REQUEST = 3;
-    private static final int START_SECURE_PICK_SERVER_REQUEST = 4;
-    private static final int START_INSECURE_PICK_SERVER_REQUEST = 5;
-    private static final int START_SECURE_CLIENT_REQUEST = 6;
-    private static final int START_INSECURE_CLIENT_REQUEST = 7;
-
-    private TestListItem mBluetoothToggleTest;
-    private TestListItem mSecureServerTest;
-    private TestListItem mInsecureServerTest;
-    private TestListItem mSecureClientTest;
-    private TestListItem mInsecureClientTest;
-
-    private static final String TABLE_NAME = "results";
-    private static final String _ID = "_id";
-    private static final String COLUMN_TEST_ID = "test_id";
-    private static final String COLUMN_TEST_RESULT = "test_result";
-    private static final String[] ALL_COLUMNS = {
-          _ID,
-          COLUMN_TEST_ID,
-          COLUMN_TEST_RESULT,
-    };
+    private static final int LAUNCH_TEST_REQUEST_CODE = 1;
 
     private TestListAdapter mAdapter;
 
@@ -84,430 +43,52 @@
         setPassFailButtonClickListeners();
         setInfoResources(R.string.bluetooth_test, R.string.bluetooth_test_info, -1);
 
-        mBluetoothToggleTest = TestListItem.newTest(R.string.bt_toggle_bluetooth,
-                TEST_BLUETOOTH_TOGGLE);
-        mSecureServerTest = TestListItem.newTest(R.string.bt_secure_server,
-                TEST_SECURE_SERVER);
-        mInsecureServerTest = TestListItem.newTest(R.string.bt_insecure_server,
-                TEST_INSECURE_SERVER);
-        mSecureClientTest = TestListItem.newTest(R.string.bt_secure_client,
-                TEST_SECURE_CLIENT);
-        mInsecureClientTest = TestListItem.newTest(R.string.bt_insecure_client,
-                TEST_INSECURE_CLIENT);
-
-        mAdapter = new TestListAdapter(this);
-        mAdapter.add(TestListItem.newCategory(R.string.bt_control));
-        mAdapter.add(mBluetoothToggleTest);
-
-        mAdapter.add(TestListItem.newCategory(R.string.bt_device_communication));
-        mAdapter.add(mSecureServerTest);
-        mAdapter.add(mInsecureServerTest);
-        mAdapter.add(mSecureClientTest);
-        mAdapter.add(mInsecureClientTest);
-
+        mAdapter = new TestListAdapter(this, getClass().getName());
         setListAdapter(mAdapter);
-        refreshTestResults();
+        mAdapter.loadTestResults();
 
         if (BluetoothAdapter.getDefaultAdapter() == null) {
             showNoBluetoothDialog();
         }
     }
 
-    private void refreshTestResults() {
-        new RefreshTask().execute();
-    }
-
     private void showNoBluetoothDialog() {
         new AlertDialog.Builder(this)
-        .setIcon(android.R.drawable.ic_dialog_alert)
-        .setTitle(R.string.bt_not_available_title)
-        .setMessage(R.string.bt_not_available_message)
-        .setCancelable(false)
-        .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
-            @Override
-            public void onClick(DialogInterface dialog, int which) {
-                finish();
-            }
-        })
-        .show();
+            .setIcon(android.R.drawable.ic_dialog_alert)
+            .setTitle(R.string.bt_not_available_title)
+            .setMessage(R.string.bt_not_available_message)
+            .setCancelable(false)
+            .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    finish();
+                }
+            })
+            .show();
     }
 
     @Override
     protected void onListItemClick(ListView l, View v, int position, long id) {
         super.onListItemClick(l, v, position, id);
         TestListItem testItem = (TestListItem) l.getItemAtPosition(position);
-        switch (testItem.mId) {
-            case TEST_BLUETOOTH_TOGGLE:
-                startToggleBluetoothActivity();
-                break;
-
-            case TEST_SECURE_SERVER:
-                startServerActivity(true);
-                break;
-
-            case TEST_INSECURE_SERVER:
-                startServerActivity(false);
-                break;
-
-            case TEST_SECURE_CLIENT:
-                startDevicePickerActivity(true);
-                break;
-
-            case TEST_INSECURE_CLIENT:
-                startDevicePickerActivity(false);
-                break;
-        }
-    }
-
-    private void startToggleBluetoothActivity() {
-        Intent intent = new Intent(this, BluetoothToggleActivity.class);
-        startActivityForResult(intent, START_TOGGLE_BLUETOOTH_TEST_REQUEST);
-    }
-
-    private void startServerActivity(boolean secure) {
-        Intent intent = new Intent(this, MessageTestActivity.class)
-                .putExtra(MessageTestActivity.EXTRA_SECURE, secure);
-        startActivityForResult(intent, secure
-                ? START_SECURE_SERVER_REQUEST
-                : START_INSECURE_SERVER_REQUEST);
-    }
-
-    private void startDevicePickerActivity(boolean secure) {
-        Intent intent = new Intent(this, DevicePickerActivity.class);
-        startActivityForResult(intent, secure
-                ? START_SECURE_PICK_SERVER_REQUEST
-                : START_INSECURE_PICK_SERVER_REQUEST);
+        Intent intent = testItem.getIntent();
+        startActivityForResult(intent, LAUNCH_TEST_REQUEST_CODE);
     }
 
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
         switch (requestCode) {
-            case START_TOGGLE_BLUETOOTH_TEST_REQUEST:
-                handleEnableBluetoothResult(resultCode, data);
-                break;
-
-            case START_SECURE_SERVER_REQUEST:
-                handleServerResult(resultCode, data, true);
-                break;
-
-            case START_INSECURE_SERVER_REQUEST:
-                handleServerResult(resultCode, data, false);
-                break;
-
-            case START_SECURE_PICK_SERVER_REQUEST:
-                handleDevicePickerResult(resultCode, data, true);
-                break;
-
-            case START_INSECURE_PICK_SERVER_REQUEST:
-                handleDevicePickerResult(resultCode, data, false);
-                break;
-
-            case START_SECURE_CLIENT_REQUEST:
-                handleClientResult(resultCode, data, true);
-                break;
-
-            case START_INSECURE_CLIENT_REQUEST:
-                handleClientResult(resultCode, data, false);
+            case LAUNCH_TEST_REQUEST_CODE:
+                handleLaunchTestResult(resultCode, data);
                 break;
         }
     }
 
-    private void handleEnableBluetoothResult(int resultCode, Intent data) {
-        if (data != null) {
-            TestResult result = TestResult.fromActivityResult(resultCode, data);
-            mBluetoothToggleTest.setResult(result.getResult());
-            updateTest(mBluetoothToggleTest);
-        }
-    }
-
-    private void updateTest(TestListItem item) {
-        new UpdateTask().execute(item.mId, item.mResult);
-    }
-
-    private void handleServerResult(int resultCode, Intent data, boolean secure) {
-        if (data != null) {
-            TestResult result = TestResult.fromActivityResult(resultCode, data);
-            TestListItem test = secure ? mSecureServerTest : mInsecureServerTest;
-            test.setResult(result.getResult());
-            updateTest(test);
-        }
-    }
-
-    private void handleDevicePickerResult(int resultCode, Intent data, boolean secure) {
+    private void handleLaunchTestResult(int resultCode, Intent data) {
         if (resultCode == RESULT_OK) {
-            String address = data.getStringExtra(DevicePickerActivity.EXTRA_DEVICE_ADDRESS);
-            startClientActivity(address, secure);
-        }
-    }
-
-    private void startClientActivity(String address, boolean secure) {
-        Intent intent = new Intent(this, MessageTestActivity.class)
-                .putExtra(MessageTestActivity.EXTRA_DEVICE_ADDRESS, address)
-                .putExtra(MessageTestActivity.EXTRA_SECURE, secure);
-        startActivityForResult(intent, secure
-                ? START_SECURE_CLIENT_REQUEST
-                : START_INSECURE_CLIENT_REQUEST);
-    }
-
-    private void handleClientResult(int resultCode, Intent data, boolean secure) {
-        if (data != null) {
-            TestResult result = TestResult.fromActivityResult(resultCode, data);
-            TestListItem test = secure ? mSecureClientTest : mInsecureClientTest;
-            test.setResult(result.getResult());
-            updateTest(test);
-        }
-    }
-
-    private class UpdateTask extends AsyncTask<Integer, Void, Void> {
-
-        @Override
-        protected Void doInBackground(Integer[] resultPairs) {
-            TestResultsHelper openHelper = new TestResultsHelper(BluetoothTestActivity.this);
-            SQLiteDatabase db = openHelper.getWritableDatabase();
-
-            int testId = resultPairs[0];
-            int testResult = resultPairs[1];
-
-            ContentValues values = new ContentValues(2);
-            values.put(COLUMN_TEST_ID, testId);
-            values.put(COLUMN_TEST_RESULT, testResult);
-
-            try {
-                if (0 == db.update(TABLE_NAME, values, COLUMN_TEST_ID + " = ?",
-                        new String[] {Integer.toString(testId)})) {
-                    db.insert(TABLE_NAME, null, values);
-                }
-            } finally {
-                if (db != null) {
-                    db.close();
-                }
-            }
-
-            return null;
-        }
-
-        @Override
-        protected void onPostExecute(Void result) {
-            super.onPostExecute(result);
-            refreshTestResults();
-        }
-    }
-
-    private class RefreshTask extends AsyncTask<Void, Void, Map<Integer, Integer>> {
-
-        @Override
-        protected Map<Integer, Integer> doInBackground(Void... params) {
-            Map<Integer, Integer> results = new HashMap<Integer, Integer>();
-            TestResultsHelper openHelper = new TestResultsHelper(BluetoothTestActivity.this);
-            SQLiteDatabase db = openHelper.getReadableDatabase();
-            Cursor cursor = null;
-            try {
-                cursor = db.query(TABLE_NAME, ALL_COLUMNS, null, null, null, null, null, null);
-                while (cursor.moveToNext()) {
-                    int testId = cursor.getInt(1);
-                    int testResult = cursor.getInt(2);
-                    results.put(testId, testResult);
-                }
-            } finally {
-                if (cursor != null) {
-                    cursor.close();
-                }
-                if (db != null) {
-                    db.close();
-                }
-            }
-            return results;
-        }
-
-        @Override
-        protected void onPostExecute(Map<Integer, Integer> results) {
-            super.onPostExecute(results);
-            for (Integer testId : results.keySet()) {
-                TestListItem item = mAdapter.getTest(testId);
-                if (item != null) {
-                    item.setResult(results.get(testId));
-                }
-            }
-            mAdapter.notifyDataSetChanged();
-        }
-    }
-
-    static class TestListItem {
-
-        static final int NUM_VIEW_TYPES = 2;
-
-        static final int VIEW_TYPE_CATEGORY = 0;
-
-        static final int VIEW_TYPE_TEST = 1;
-
-        final int mViewType;
-
-        final int mTitle;
-
-        final int mId;
-
-        int mResult;
-
-        static TestListItem newTest(int title, int id) {
-            return new TestListItem(VIEW_TYPE_TEST, title, id);
-        }
-
-        static TestListItem newCategory(int title) {
-            return new TestListItem(VIEW_TYPE_CATEGORY, title, -1);
-        }
-
-        private TestListItem(int viewType, int title, int id) {
-            this.mViewType = viewType;
-            this.mTitle = title;
-            this.mId = id;
-        }
-
-        public boolean isTest() {
-            return mViewType == VIEW_TYPE_TEST;
-        }
-
-        public void setResult(int result) {
-            mResult = result;
-        }
-    }
-
-    static class TestListAdapter extends BaseAdapter {
-
-        private static final int PADDING = 10;
-
-        private final List<TestListItem> mItems = new ArrayList<TestListItem>();
-
-        private final Map<Integer, TestListItem> mTestsById = new HashMap<Integer, TestListItem>();
-
-        private final LayoutInflater mInflater;
-
-        public TestListAdapter(Context context) {
-            mInflater = (LayoutInflater) context.getSystemService(LAYOUT_INFLATER_SERVICE);
-        }
-
-        public void add(TestListItem item) {
-            mItems.add(item);
-            if (item.isTest()) {
-                mTestsById.put(item.mId, item);
-            }
-        }
-
-        @Override
-        public View getView(int position, View convertView, ViewGroup parent) {
-            int backgroundResource = 0;
-            int iconResource = 0;
-
-            TestListItem item = getItem(position);
-
-            TextView textView = null;
-            if (convertView == null) {
-                int layout = getLayout(position);
-                textView = (TextView) mInflater.inflate(layout, parent, false);
-            } else {
-                textView = (TextView) convertView;
-            }
-
-            textView.setText(item.mTitle);
-
-            if (item.isTest()) {
-                switch (item.mResult) {
-                    case TestResult.TEST_RESULT_PASSED:
-                        backgroundResource = R.drawable.test_pass_gradient;
-                        iconResource = R.drawable.fs_good;
-                        break;
-
-                    case TestResult.TEST_RESULT_FAILED:
-                        backgroundResource = R.drawable.test_fail_gradient;
-                        iconResource = R.drawable.fs_error;
-                        break;
-
-                    case TestResult.TEST_RESULT_NOT_EXECUTED:
-                        break;
-
-                    default:
-                        throw new IllegalArgumentException("Unknown test result: " + item.mResult);
-                }
-
-                textView.setBackgroundResource(backgroundResource);
-                textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, iconResource, 0);
-                textView.setPadding(PADDING, 0, PADDING, 0);
-                textView.setCompoundDrawablePadding(PADDING);
-            }
-
-            return textView;
-        }
-
-        private int getLayout(int position) {
-            int viewType = getItemViewType(position);
-            switch (viewType) {
-                case TestListItem.VIEW_TYPE_CATEGORY:
-                    return R.layout.test_category_row;
-                case TestListItem.VIEW_TYPE_TEST:
-                    return android.R.layout.simple_list_item_1;
-                default:
-                    throw new IllegalArgumentException("Illegal view type: " + viewType);
-
-            }
-        }
-
-        public TestListItem getTest(int id) {
-            return mTestsById.get(id);
-        }
-
-        @Override
-        public int getCount() {
-            return mItems.size();
-        }
-
-        @Override
-        public TestListItem getItem(int position) {
-            return mItems.get(position);
-        }
-
-        @Override
-        public long getItemId(int position) {
-            return getItem(position).mId;
-        }
-
-        @Override
-        public int getViewTypeCount() {
-            return TestListItem.NUM_VIEW_TYPES;
-        }
-
-        @Override
-        public int getItemViewType(int position) {
-            return getItem(position).mViewType;
-        }
-
-        @Override
-        public boolean isEnabled(int position) {
-            return getItemViewType(position) != TestListItem.VIEW_TYPE_CATEGORY;
-        }
-    }
-
-    class TestResultsHelper extends SQLiteOpenHelper {
-
-        private static final String DATABASE_NAME = "bluetooth_results.db";
-
-        private static final int DATABASE_VERSION = 1;
-
-        TestResultsHelper(Context context) {
-            super(context, DATABASE_NAME, null, DATABASE_VERSION);
-        }
-
-        @Override
-        public void onCreate(SQLiteDatabase db) {
-            db.execSQL("CREATE TABLE " + TABLE_NAME + " ("
-                    + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
-                    + COLUMN_TEST_ID + " INTEGER, "
-                    + COLUMN_TEST_RESULT + " INTEGER DEFAULT 0);");
-        }
-
-        @Override
-        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-            db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
-            onCreate(db);
+            TestResult testResult = TestResult.fromActivityResult(resultCode, data);
+            mAdapter.setTestResult(testResult);
         }
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/InsecureClientActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/InsecureClientActivity.java
new file mode 100644
index 0000000..6dfbbea
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/InsecureClientActivity.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.bluetooth;
+
+public class InsecureClientActivity extends MessageTestActivity {
+    public InsecureClientActivity() {
+        super(false, false);
+    }
+}
\ No newline at end of file
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/InsecureServerActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/InsecureServerActivity.java
new file mode 100644
index 0000000..3526e04
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/InsecureServerActivity.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.bluetooth;
+
+public class InsecureServerActivity extends MessageTestActivity {
+    public InsecureServerActivity() {
+        super(false, true);
+    }
+}
\ No newline at end of file
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/MessageTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/MessageTestActivity.java
index 1d6706d..9405d71 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/MessageTestActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/MessageTestActivity.java
@@ -32,8 +32,8 @@
 import android.os.Handler;
 import android.os.Message;
 import android.view.View;
-import android.view.Window;
 import android.view.View.OnClickListener;
+import android.view.Window;
 import android.widget.ArrayAdapter;
 import android.widget.Button;
 import android.widget.ListView;
@@ -43,16 +43,14 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-public class MessageTestActivity extends PassFailButtons.Activity {
-
-    static final String EXTRA_DEVICE_ADDRESS = "deviceAddress";
-    static final String EXTRA_SECURE = "secure";
+class MessageTestActivity extends PassFailButtons.Activity {
 
     /** Broadcast action that should only be fired when pairing for a secure connection. */
     private static final String ACTION_PAIRING_REQUEST =
             "android.bluetooth.device.action.PAIRING_REQUEST";
 
     private static final int ENABLE_BLUETOOTH_REQUEST = 1;
+    private static final int PICK_SERVER_DEVICE_REQUEST = 2;
 
     private static final String MESSAGE_DELIMITER = "\n";
     private static final Pattern MESSAGE_PATTERN = Pattern.compile("Message (\\d+) to .*");
@@ -79,6 +77,11 @@
     private String mRemoteDeviceName = "";
     private StringBuilder mMessageBuffer = new StringBuilder();
 
+    MessageTestActivity(boolean secure, boolean server) {
+        mSecure = secure;
+        mServer = server;
+    }
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -86,9 +89,6 @@
         setContentView(R.layout.bt_messages);
         setPassFailButtonClickListeners();
 
-        mDeviceAddress = getIntent().getStringExtra(EXTRA_DEVICE_ADDRESS);
-        mSecure = getIntent().getBooleanExtra(EXTRA_SECURE, true);
-        mServer = mDeviceAddress == null || mDeviceAddress.isEmpty();
         if (mServer) {
             setTitle(mSecure ? R.string.bt_secure_server : R.string.bt_insecure_server);
         } else {
@@ -127,24 +127,41 @@
         registerReceiver(mPairingActionReceiver, intentFilter);
 
         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
-        if (mBluetoothAdapter.isEnabled()) {
-            startChatService();
+        if (!mServer) {
+            Intent intent = new Intent(this, DevicePickerActivity.class);
+            startActivityForResult(intent, PICK_SERVER_DEVICE_REQUEST);
         } else {
-            Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
-            startActivityForResult(intent, ENABLE_BLUETOOTH_REQUEST);
+            if (mBluetoothAdapter.isEnabled()) {
+                startChatService();
+            } else {
+                Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+                startActivityForResult(intent, ENABLE_BLUETOOTH_REQUEST);
+            }
         }
     }
 
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         super.onActivityResult(requestCode, resultCode, data);
-        if (requestCode == ENABLE_BLUETOOTH_REQUEST) {
-            if (resultCode == RESULT_OK) {
-                startChatService();
-            } else {
-                setResult(RESULT_CANCELED);
-                finish();
-            }
+        switch (requestCode) {
+            case ENABLE_BLUETOOTH_REQUEST:
+                if (resultCode == RESULT_OK) {
+                    startChatService();
+                } else {
+                    setResult(RESULT_CANCELED);
+                    finish();
+                }
+                break;
+
+            case PICK_SERVER_DEVICE_REQUEST:
+                if (resultCode == RESULT_OK) {
+                    mDeviceAddress = data.getStringExtra(DevicePickerActivity.EXTRA_DEVICE_ADDRESS);
+                    startChatService();
+                } else {
+                    setResult(RESULT_CANCELED);
+                    finish();
+                }
+                break;
         }
     }
 
@@ -332,7 +349,9 @@
     @Override
     protected void onDestroy() {
         super.onDestroy();
-        mChatService.stop();
+        if (mChatService != null) {
+            mChatService.stop();
+        }
         unregisterReceiver(mPairingActionReceiver);
     }
 }
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/SecureClientActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/SecureClientActivity.java
new file mode 100644
index 0000000..799f0b8
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/SecureClientActivity.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.bluetooth;
+
+public class SecureClientActivity extends MessageTestActivity {
+    public SecureClientActivity() {
+        super(true, false);
+    }
+}
\ No newline at end of file
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/SecureServerActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/SecureServerActivity.java
new file mode 100644
index 0000000..25e26e6
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/bluetooth/SecureServerActivity.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier.bluetooth;
+
+public class SecureServerActivity extends MessageTestActivity {
+    public SecureServerActivity() {
+        super(true, true);
+    }
+}
\ No newline at end of file