CtsVerifier Test Result Infrastructure

Add a TestResult class that allows test activities to set their
finishing results as either pass or fail. Reflect the pass or
fail status in the TestListActivity's test list. This is done
via a ContentProvider that reads and writes to a database with
the TestListAdapter listening for changes.

Use the new API in the SUID Files test as an example. The other
activities should be able to be easily adjusted to use the API.
Just call TestResult.setPassedResult or .setFailedResult during
some time in the activity before you finish.

Change-Id: Ia6a1f75e2ac9d03cb22ed233bedf392e3fb28f0e
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index 625dfe5..190c519 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -32,6 +32,9 @@
         </activity>
 
         <activity android:name=".TestListActivity" android:label="@string/test_list_title" />
+        
+        <provider android:name=".TestResultsProvider" 
+                android:authorities="com.android.cts.verifier.testresultsprovider" />
 
         <activity android:name=".suid.SuidFilesActivity" 
                 android:label="@string/suid_files"
diff --git a/apps/CtsVerifier/res/drawable/test_fail_gradient.xml b/apps/CtsVerifier/res/drawable/test_fail_gradient.xml
new file mode 100644
index 0000000..defc0dd
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable/test_fail_gradient.xml
@@ -0,0 +1,6 @@
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+    <gradient android:startColor="#33F3614B"
+              android:centerColor="#66F3614B"
+              android:endColor="#FFF3614B"
+              android:angle="315"/>
+</shape>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/drawable/test_pass_gradient.xml b/apps/CtsVerifier/res/drawable/test_pass_gradient.xml
new file mode 100644
index 0000000..49a938c
--- /dev/null
+++ b/apps/CtsVerifier/res/drawable/test_pass_gradient.xml
@@ -0,0 +1,6 @@
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+    <gradient android:startColor="#3399D200"
+              android:centerColor="#6699D200"
+              android:endColor="#FF99D200"
+              android:angle="315"/>
+</shape>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/layout/main.xml b/apps/CtsVerifier/res/layout/main.xml
index 52da8b8..479cc1b 100644
--- a/apps/CtsVerifier/res/layout/main.xml
+++ b/apps/CtsVerifier/res/layout/main.xml
@@ -13,7 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
@@ -28,7 +28,8 @@
         android:id="@+id/continue_button"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
         android:onClick="continueButtonClickHandler"
         android:text="@string/continue_button_text"
         />
-</LinearLayout>
+</RelativeLayout>
diff --git a/apps/CtsVerifier/res/menu/test_list_menu.xml b/apps/CtsVerifier/res/menu/test_list_menu.xml
new file mode 100644
index 0000000..3f97a16
--- /dev/null
+++ b/apps/CtsVerifier/res/menu/test_list_menu.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/clear"
+          android:icon="@android:drawable/ic_menu_delete" 
+          android:title="@string/clear" />
+</menu>
\ No newline at end of file
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index 6d4d52b29..9904336 100644
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -24,6 +24,8 @@
     <string name="test_category_security">Security</string>
     <string name="test_category_features">Features</string>
     <string name="test_category_other">Other</string>
+    <string name="clear">Clear</string>
+    <string name="test_results_cleared">Test results cleared.</string>
 
     <!-- Strings for FeatureSummaryActivity -->
     <string name="feature_summary">Hardware/Software Feature Summary</string>
@@ -38,7 +40,7 @@
 
     <!-- Strings for SuidFilesActivity -->
     <string name="suid_files">SUID Files</string>
-    <string name="starting_scan">Starting scan...</string>
+    <string name="scanning_directory">Scanning directory...</string>
     <string name="file_status">User: %1$s\nGroup: %2$s\nPermissions: %3$s\nPath: %4$s</string>
     <string name="no_file_status">Could not stat file...</string>
     <string name="congratulations">Congratulations!</string>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
index f0bba52..b7801a6 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestListActivity.java
@@ -16,251 +16,132 @@
 
 package com.android.cts.verifier;
 
-import android.app.ListActivity;
-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.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.ListAdapter;
-import android.widget.ListView;
-import android.widget.TextView;
+import com.android.cts.verifier.TestListAdapter.TestListItem;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+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.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ListView;
+import android.widget.Toast;
 
 /** {@link ListActivity} that displays a  list of manual tests. */
 public class TestListActivity extends ListActivity {
 
-    /** Activities implementing {@link Intent#ACTION_MAIN} and this will appear in the list. */
-    static final String CATEGORY_MANUAL_TEST = "android.cts.intent.category.MANUAL_TEST";
-
-    static final String TEST_CATEGORY_META_DATA = "test_category";
+    private static final int LAUNCH_TEST_REQUEST_CODE = 1;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setListAdapter(new TestListAdapter(this));
-    }
 
+        TestListAdapter adapter = new TestListAdapter(this);
+        setListAdapter(adapter);
+
+        TestResultContentObserver observer = new TestResultContentObserver(adapter);
+        ContentResolver resolver = getContentResolver();
+        resolver.registerContentObserver(TestResultsProvider.CONTENT_URI, true, observer);
+    }
 
     /** Launch the activity when its {@link ListView} item is clicked. */
     @Override
     protected void onListItemClick(ListView listView, View view, int position, long id) {
         super.onListItemClick(listView, view, position, id);
         Intent intent = getIntent(position);
-        startActivity(intent);
+        startActivityForResult(intent, LAUNCH_TEST_REQUEST_CODE);
     }
 
-    @SuppressWarnings("unchecked")
     private Intent getIntent(int position) {
-        ListAdapter adapter = getListAdapter();
-        Map<String, ?> data = (Map<String, ?>) adapter.getItem(position);
-        return (Intent) data.get(TestListAdapter.INTENT);
+        TestListAdapter adapter = getListAdapter();
+        TestListItem item = adapter.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) {
+            case LAUNCH_TEST_REQUEST_CODE:
+                handleLaunchTestResult(resultCode, data);
+                break;
+
+            default:
+                throw new IllegalArgumentException("Unknown request code: " + requestCode);
+        }
+    }
+
+    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.CONTENT_URI, values,
+                    TestResultsProvider.COLUMN_TEST_NAME + " = ?",
+                    new String[] {testResult.getName()});
+
+            if (numUpdated == 0) {
+                resolver.insert(TestResultsProvider.CONTENT_URI, values);
+            }
+        }
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        MenuInflater inflater = getMenuInflater();
+        inflater.inflate(R.menu.test_list_menu, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.clear:
+                handleClearItemSelected();
+                return true;
+            default:
+                return super.onOptionsItemSelected(item);
+        }
+    }
+
+    private void handleClearItemSelected() {
+        ContentResolver resolver = getContentResolver();
+        resolver.delete(TestResultsProvider.CONTENT_URI, "1", null);
+        Toast.makeText(this, R.string.test_results_cleared, Toast.LENGTH_SHORT).show();
     }
 
     /**
-     * Each {@link ListView} item will have a map associated it with containing the title to
-     * display and the intent used to launch it. If there is no intent, then it is a test category
-     * header.
+     * {@link ContentResolver} that refreshes the {@link TestListAdapter} and thus
+     * the {@link ListView} when the test results change.
      */
-    static class TestListAdapter extends BaseAdapter {
+    private static class TestResultContentObserver extends ContentObserver {
 
-        static final String TITLE = "title";
+        private final TestListAdapter mAdapter;
 
-        static final String INTENT = "intent";
-
-        /** View type for a category of tests like "Sensors" or "Features" */
-        static final int TEST_CATEGORY_HEADER_VIEW_TYPE = 0;
-
-        /** View type for an actual test like the Accelerometer test. */
-        static final int TEST_VIEW_TYPE = 1;
-
-        private final List<Map<String, ?>> mData;
-
-        private final LayoutInflater mLayoutInflater;
-
-        TestListAdapter(Context context) {
-            this.mData = getData(context);
-            this.mLayoutInflater =
-                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        }
-
-        static List<Map<String, ?>> getData(Context context) {
-            /*
-             * 1. Get all the tests keyed by their category.
-             * 2. Flatten the tests and categories into one giant list for the list view.
-             */
-
-            Map<String, List<Map<String, ?>>> testsByCategory = getTestsByCategory(context);
-
-            List<String> testCategories = new ArrayList<String>(testsByCategory.keySet());
-            Collections.sort(testCategories);
-
-            List<Map<String, ?>> data = new ArrayList<Map<String, ?>>();
-            for (String testCategory : testCategories) {
-                addItem(data, testCategory, null);
-
-                List<Map<String, ?>> tests = testsByCategory.get(testCategory);
-                Collections.sort(tests, new Comparator<Map<String, ?>>() {
-                    public int compare(Map<String, ?> item, Map<String, ?> otherItem) {
-                        String title = (String) item.get(TITLE);
-                        String otherTitle = (String) otherItem.get(TITLE);
-                        return title.compareTo(otherTitle);
-                    }
-                });
-                data.addAll(tests);
-            }
-
-            return data;
-        }
-
-        static Map<String, List<Map<String, ?>>> getTestsByCategory(Context context) {
-            Map<String, List<Map<String, ?>>> testsByCategory =
-                new HashMap<String, List<Map<String, ?>>>();
-
-            Intent mainIntent = new Intent(Intent.ACTION_MAIN);
-            mainIntent.addCategory(CATEGORY_MANUAL_TEST);
-
-            PackageManager packageManager = context.getPackageManager();
-            List<ResolveInfo> list = packageManager.queryIntentActivities(mainIntent,
-                    PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
-
-            for (int i = 0; i < list.size(); i++) {
-                ResolveInfo info = list.get(i);
-                String testCategory = getTestCategory(context, info.activityInfo.metaData);
-                String title = getTitle(context, info.activityInfo);
-                Intent intent = getActivityIntent(info.activityInfo);
-                addItemToCategory(testsByCategory, testCategory, title, intent);
-            }
-
-            return testsByCategory;
-        }
-
-        static String getTestCategory(Context context, Bundle metaData) {
-            String testCategory = null;
-            if (metaData != null) {
-                testCategory = metaData.getString(TEST_CATEGORY_META_DATA);
-            }
-            if (testCategory != null) {
-                return testCategory;
-            } else {
-                return context.getString(R.string.test_category_other);
-            }
-        }
-
-        static String getTitle(Context context, ActivityInfo activityInfo) {
-            if (activityInfo.labelRes != 0) {
-                return context.getString(activityInfo.labelRes);
-            } else {
-                return activityInfo.name;
-            }
-        }
-
-        static Intent getActivityIntent(ActivityInfo activityInfo) {
-            Intent intent = new Intent();
-            intent.setClassName(activityInfo.packageName, activityInfo.name);
-            return intent;
-        }
-
-        static void addItemToCategory(Map<String, List<Map<String, ?>>> data, String testCategory,
-                String title, Intent intent) {
-            List<Map<String, ?>> tests;
-            if (data.containsKey(testCategory)) {
-                tests = data.get(testCategory);
-            } else {
-                tests = new ArrayList<Map<String, ?>>();
-            }
-            data.put(testCategory, tests);
-            addItem(tests, title, intent);
-        }
-
-        /**
-         * @param tests to add this new item to
-         * @param title to show in the list view
-         * @param intent for a test to launch or null for a test category header
-         */
-        @SuppressWarnings("unchecked")
-        static void addItem(List<Map<String, ?>> tests, String title, Intent intent) {
-            HashMap item = new HashMap(2);
-            item.put(TITLE, title);
-            item.put(INTENT, intent);
-            tests.add(item);
+        public TestResultContentObserver(TestListAdapter adapter) {
+            super(new Handler());
+            this.mAdapter = adapter;
         }
 
         @Override
-        public boolean areAllItemsEnabled() {
-            // Section headers for test categories are not clickable.
-            return false;
-        }
+        public void onChange(boolean selfChange) {
+            super.onChange(selfChange);
 
-        @Override
-        public boolean isEnabled(int position) {
-            return isTestActivity(position);
-        }
-
-        @Override
-        public int getItemViewType(int position) {
-            return isTestActivity(position) ? TEST_VIEW_TYPE : TEST_CATEGORY_HEADER_VIEW_TYPE;
-        }
-
-        private boolean isTestActivity(int position) {
-            Map<String, ?> item = getItem(position);
-            return item.get(INTENT) != null;
-        }
-
-        @Override
-        public int getViewTypeCount() {
-            return 2;
-        }
-
-        public int getCount() {
-            return mData.size();
-        }
-
-        public Map<String, ?> getItem(int position) {
-            return mData.get(position);
-        }
-
-        public long getItemId(int position) {
-            return position;
-        }
-
-        public View getView(int position, View convertView, ViewGroup parent) {
-            TextView textView;
-            if (convertView == null) {
-                int layout = getLayout(position);
-                textView = (TextView) mLayoutInflater.inflate(layout, parent, false);
-            } else {
-                textView = (TextView) convertView;
-            }
-
-            Map<String, ?> data = getItem(position);
-            String title = (String) data.get(TITLE);
-            textView.setText(title);
-            return textView;
-        }
-
-        private int getLayout(int position) {
-            int viewType = getItemViewType(position);
-            switch (viewType) {
-                case TEST_CATEGORY_HEADER_VIEW_TYPE:
-                    return R.layout.test_category_row;
-                case TEST_VIEW_TYPE:
-                    return android.R.layout.simple_list_item_1;
-                default:
-                    throw new IllegalArgumentException("Illegal view type: " + viewType);
-
-            }
+            // 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
new file mode 100644
index 0000000..ddf4fe8
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestListAdapter.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier;
+
+import android.content.ContentResolver;
+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.Cursor;
+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.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * {@link BaseAdapter} that populates the {@link TestListActivity}'s {@link ListView}.
+ * Making a new test activity to appear in the list requires the following steps:
+ *
+ * <ol>
+ *     <li>REQUIRED: Add an activity to the AndroidManifest.xml with an intent filter with a
+ *         main action and the MANUAL_TEST category.
+ *         <pre>
+ *             <intent-filter>
+ *                <action android:name="android.intent.action.MAIN" />
+ *                <category android:name="android.cts.intent.category.MANUAL_TEST" />
+ *             </intent-filter>
+ *         </pre>
+ *     </li>
+ *     <li>OPTIONAL: Add a meta data attribute to indicate what category of tests the activity
+ *         should belong to. If you don't add this attribute, your test will show up in the
+ *         "Other" tests category.
+ *         <pre>
+ *             <meta-data android:name="test_category" android:value="@string/test_category_security" />
+ *         </pre>
+ *     </li>
+ * </ol>
+ */
+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";
+
+    /** View type for a category of tests like "Sensors" or "Features" */
+    private static final int CATEGORY_HEADER_VIEW_TYPE = 0;
+
+    /** View type for an actual test like the Accelerometer test. */
+    private static final int TEST_VIEW_TYPE = 1;
+
+    /** Padding around the text views and icons. */
+    private static final int PADDING = 10;
+
+    private final Context mContext;
+
+    /** Immutable data of tests like the test's title and launch intent. */
+    private final List<TestListItem> mRows;
+
+    /** Mutable test results that will change as each test activity finishes. */
+    private final Map<String, Integer> mTestResults = new HashMap<String, Integer>();
+
+    private final LayoutInflater mLayoutInflater;
+
+    /** {@link ListView} row that is either a test category header or a test. */
+    static class TestListItem {
+
+        /** Title shown in the {@link ListView}. */
+        final String title;
+
+        /** Class name with package to uniquely identify the test. Null for categories. */
+        final String className;
+
+        /** Intent used to launch the activity from the list. Null for categories. */
+        final Intent intent;
+
+        static TestListItem newTest(String title, String className, Intent intent) {
+            return new TestListItem(title, className, intent);
+        }
+
+        static TestListItem newCategory(String title) {
+            return new TestListItem(title, null, null);
+        }
+
+        private TestListItem(String title, String className, Intent intent) {
+            this.title = title;
+            this.className = className;
+            this.intent = intent;
+        }
+
+        boolean isTest() {
+            return intent != null;
+        }
+    }
+
+    TestListAdapter(Context context) {
+        this.mContext = context;
+        this.mRows = getRows(context);
+        this.mLayoutInflater =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        updateTestResults(mContext, mTestResults);
+    }
+
+    static List<TestListItem> getRows(Context context) {
+        /*
+         * 1. Get all the tests keyed by their category.
+         * 2. Flatten the tests and categories into one giant list for the list view.
+         */
+
+        Map<String, List<TestListItem>> testsByCategory = getTestsByCategory(context);
+
+        List<String> testCategories = new ArrayList<String>(testsByCategory.keySet());
+        Collections.sort(testCategories);
+
+        List<TestListItem> allRows = new ArrayList<TestListItem>();
+        for (String testCategory : testCategories) {
+            allRows.add(TestListItem.newCategory(testCategory));
+
+            List<TestListItem> tests = testsByCategory.get(testCategory);
+            Collections.sort(tests, new Comparator<TestListItem>() {
+                public int compare(TestListItem item, TestListItem otherItem) {
+                    return item.title.compareTo(otherItem.title);
+                }
+            });
+            allRows.addAll(tests);
+        }
+        return allRows;
+    }
+
+    static Map<String, List<TestListItem>> getTestsByCategory(Context context) {
+        Map<String, List<TestListItem>> testsByCategory =
+                new HashMap<String, List<TestListItem>>();
+
+        Intent mainIntent = new Intent(Intent.ACTION_MAIN);
+        mainIntent.addCategory(CATEGORY_MANUAL_TEST);
+
+        PackageManager packageManager = context.getPackageManager();
+        List<ResolveInfo> list = packageManager.queryIntentActivities(mainIntent,
+                PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
+
+        for (int i = 0; i < list.size(); i++) {
+            ResolveInfo info = list.get(i);
+            String testCategory = getTestCategory(context, info.activityInfo.metaData);
+            String title = getTitle(context, info.activityInfo);
+            String className = info.activityInfo.name;
+            Intent intent = getActivityIntent(info.activityInfo);
+
+            addTestToCategory(testsByCategory, testCategory, title, className, intent);
+        }
+
+        return testsByCategory;
+    }
+
+    static String getTestCategory(Context context, Bundle metaData) {
+        String testCategory = null;
+        if (metaData != null) {
+            testCategory = metaData.getString(TEST_CATEGORY_META_DATA);
+        }
+        if (testCategory != null) {
+            return testCategory;
+        } else {
+            return context.getString(R.string.test_category_other);
+        }
+    }
+
+    static String getTitle(Context context, ActivityInfo activityInfo) {
+        if (activityInfo.labelRes != 0) {
+            return context.getString(activityInfo.labelRes);
+        } else {
+            return activityInfo.name;
+        }
+    }
+
+    static Intent getActivityIntent(ActivityInfo activityInfo) {
+        Intent intent = new Intent();
+        intent.setClassName(activityInfo.packageName, activityInfo.name);
+        return intent;
+    }
+
+    static void addTestToCategory(Map<String, List<TestListItem>> testsByCategory,
+            String testCategory, String title, String className, Intent intent) {
+        List<TestListItem> tests;
+        if (testsByCategory.containsKey(testCategory)) {
+            tests = testsByCategory.get(testCategory);
+        } else {
+            tests = new ArrayList<TestListItem>();
+        }
+        testsByCategory.put(testCategory, tests);
+        tests.add(TestListItem.newTest(title, className, intent));
+    }
+
+    static void updateTestResults(Context context, Map<String, Integer> testResults) {
+        testResults.clear();
+        ContentResolver resolver = context.getContentResolver();
+        Cursor cursor = null;
+        try {
+            cursor = resolver.query(TestResultsProvider.RESULTS_ALL_CONTENT_URI,
+                    TestResultsProvider.ALL_COLUMNS, null, null, null);
+            if (cursor.moveToFirst()) {
+                do {
+                    String className = cursor.getString(1);
+                    int testResult = cursor.getInt(2);
+                    testResults.put(className, testResult);
+                } while (cursor.moveToNext());
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    public void refreshTestResults() {
+        updateTestResults(mContext, mTestResults);
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public boolean areAllItemsEnabled() {
+        // Section headers for test categories are not clickable.
+        return false;
+    }
+
+    @Override
+    public boolean isEnabled(int position) {
+        return getItem(position).isTest();
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        return getItem(position).isTest() ? TEST_VIEW_TYPE : CATEGORY_HEADER_VIEW_TYPE;
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        return 2;
+    }
+
+    public int getCount() {
+        return mRows.size();
+    }
+
+    public TestListItem getItem(int position) {
+        return mRows.get(position);
+    }
+
+    public long getItemId(int position) {
+        return position;
+    }
+
+    public View getView(int position, View convertView, ViewGroup parent) {
+        TextView textView;
+        if (convertView == null) {
+            int layout = getLayout(position);
+            textView = (TextView) mLayoutInflater.inflate(layout, parent, false);
+        } else {
+            textView = (TextView) convertView;
+        }
+
+        TestListItem item = getItem(position);
+        textView.setText(item.title);
+        textView.setPadding(PADDING, 0, PADDING, 0);
+        textView.setCompoundDrawablePadding(PADDING);
+
+        if (item.isTest()) {
+            int testResult = mTestResults.containsKey(item.className)
+                    ? mTestResults.get(item.className)
+                    : TestResult.TEST_RESULT_NOT_EXECUTED;
+
+
+            int backgroundResource = 0;
+            int iconResource = 0;
+
+            /** TODO: Remove fs_ prefix from feature icons since they are used here too. */
+            switch (testResult) {
+                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: " + testResult);
+            }
+
+            textView.setBackgroundResource(backgroundResource);
+            textView.setCompoundDrawablesWithIntrinsicBounds(0, 0, iconResource, 0);
+        }
+
+        return textView;
+    }
+
+    private int getLayout(int position) {
+        int viewType = getItemViewType(position);
+        switch (viewType) {
+            case CATEGORY_HEADER_VIEW_TYPE:
+                return R.layout.test_category_row;
+            case TEST_VIEW_TYPE:
+                return android.R.layout.simple_list_item_1;
+            default:
+                throw new IllegalArgumentException("Illegal view type: " + viewType);
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestResult.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestResult.java
new file mode 100644
index 0000000..6269f97
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestResult.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier;
+
+import android.app.Activity;
+import android.content.Intent;
+
+/**
+ * Object representing the result of a test activity like whether it succeeded or failed.
+ * Use {@link #setPassedResult(Activity)} or {@link #setFailedResult(Activity)} from a test
+ * activity like you would {@link Activity#setResult(int)} so that {@link TestListActivity} will
+ * persist the test result and update its adapter and thus the list view.
+ */
+public class TestResult {
+
+    public static final int TEST_RESULT_NOT_EXECUTED = 0;
+    public static final int TEST_RESULT_PASSED = 1;
+    public static final int TEST_RESULT_FAILED = 2;
+
+    private static final String TEST_NAME = "name";
+    private static final String TEST_RESULT = "result";
+
+    private final String mName;
+
+    private final int mResult;
+
+    /** Sets the test activity's result to pass. */
+    public static void setPassedResult(Activity activity) {
+        activity.setResult(Activity.RESULT_OK, createResult(activity, TEST_RESULT_PASSED));
+    }
+
+    /** Sets the test activity's result to failed. */
+    public static void setFailedResult(Activity activity) {
+        activity.setResult(Activity.RESULT_OK, createResult(activity, TEST_RESULT_FAILED));
+    }
+
+    private static Intent createResult(Activity activity, int testResult) {
+        Intent data = new Intent(activity, activity.getClass());
+        data.putExtra(TEST_NAME, activity.getClass().getName());
+        data.putExtra(TEST_RESULT, testResult);
+        return data;
+    }
+
+    /**
+     * Convert the test activity's result into a {@link TestResult}. Only meant to be used by
+     * {@link TestListActivity}.
+     */
+    public static TestResult fromActivityResult(int resultCode, Intent data) {
+        String name = data.getStringExtra(TEST_NAME);
+        int result = data.getIntExtra(TEST_RESULT, TEST_RESULT_NOT_EXECUTED);
+        return new TestResult(name, result);
+    }
+
+    private TestResult(String name, int result) {
+        this.mName = name;
+        this.mResult = result;
+    }
+
+    /** Return the name of the test like "com.android.cts.verifier.foo.FooTest" */
+    public String getName() {
+        return mName;
+    }
+
+    /** Return integer test result. See test result constants. */
+    public int getResult() {
+        return mResult;
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsProvider.java b/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsProvider.java
new file mode 100644
index 0000000..8d07b13
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/TestResultsProvider.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.verifier;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+
+/** {@link ContentProvider} that provides read and write access to the test results. */
+public class TestResultsProvider extends ContentProvider {
+
+    private static final String RESULTS_PATH = "results";
+
+    public static final String AUTHORITY = "com.android.cts.verifier.testresultsprovider";
+    public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
+    public static final Uri RESULTS_ALL_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI, RESULTS_PATH);
+
+    public static final String _ID = "_id";
+
+    /** String name of the test like "com.android.cts.verifier.foo.FooTestActivity" */
+    public static final String COLUMN_TEST_NAME = "testname";
+
+    /** Integer test result corresponding to constants in {@link TestResult}. */
+    public static final String COLUMN_TEST_RESULT = "testresult";
+
+    public static final String[] ALL_COLUMNS = {
+        _ID,
+        COLUMN_TEST_NAME,
+        COLUMN_TEST_RESULT
+    };
+
+    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+    private static final int RESULTS_ALL = 1;
+    private static final int RESULTS_ID = 2;
+    private static final int RESULTS_TEST_NAME = 3;
+    static {
+        URI_MATCHER.addURI(AUTHORITY, RESULTS_PATH, RESULTS_ALL);
+        URI_MATCHER.addURI(AUTHORITY, RESULTS_PATH + "/#", RESULTS_ID);
+        URI_MATCHER.addURI(AUTHORITY, RESULTS_PATH + "/*", RESULTS_TEST_NAME);
+    }
+
+    private static final String TABLE_NAME = "results";
+
+    private SQLiteOpenHelper mOpenHelper;
+
+    @Override
+    public boolean onCreate() {
+        mOpenHelper = new TestResultsOpenHelper(getContext());
+        return false;
+    }
+
+    private static class TestResultsOpenHelper extends SQLiteOpenHelper {
+
+        private static final String DATABASE_NAME = "results.db";
+
+        private static final int DATABASE_VERSION = 4;
+
+        TestResultsOpenHelper(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_NAME + " TEXT, "
+                    + COLUMN_TEST_RESULT + " INTEGER);");
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
+            onCreate(db);
+        }
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        SQLiteQueryBuilder query = new SQLiteQueryBuilder();
+        query.setTables(TABLE_NAME);
+
+        int match = URI_MATCHER.match(uri);
+        switch (match) {
+            case RESULTS_ALL:
+                break;
+
+            case RESULTS_ID:
+                query.appendWhere(_ID);
+                query.appendWhere(uri.getPathSegments().get(0));
+                break;
+
+            case RESULTS_TEST_NAME:
+                query.appendWhere(COLUMN_TEST_NAME);
+                query.appendWhere(uri.getPathSegments().get(0));
+                break;
+
+            default:
+                throw new IllegalArgumentException("Unknown URI: " + uri);
+        }
+
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        return query.query(db, projection, selection, selectionArgs, null, null, sortOrder);
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        long id = db.insert(TABLE_NAME, null, values);
+        getContext().getContentResolver().notifyChange(uri, null);
+        return Uri.withAppendedPath(CONTENT_URI, "" + id);
+
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int numUpdated = db.update(TABLE_NAME, values, selection, selectionArgs);
+        if (numUpdated > 0) {
+            getContext().getContentResolver().notifyChange(uri, null);
+        }
+        return numUpdated;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int numDeleted = db.delete(TABLE_NAME, selection, selectionArgs);
+        if (numDeleted > 0) {
+            getContext().getContentResolver().notifyChange(uri, null);
+        }
+        return numDeleted;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/suid/SuidFilesActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/suid/SuidFilesActivity.java
index c89b8b4..4188b58 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/suid/SuidFilesActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/suid/SuidFilesActivity.java
@@ -17,6 +17,7 @@
 package com.android.cts.verifier.suid;
 
 import com.android.cts.verifier.R;
+import com.android.cts.verifier.TestResult;
 import com.android.cts.verifier.os.FileUtils;
 import com.android.cts.verifier.os.FileUtils.FileStatus;
 
@@ -61,12 +62,13 @@
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        setResult(RESULT_CANCELED);
 
         mAdapter = new SuidFilesAdapter();
         setListAdapter(mAdapter);
 
         mProgressDialog = new ProgressDialog(this);
-        mProgressDialog.setMessage(getString(R.string.starting_scan));
+        mProgressDialog.setTitle(getString(R.string.scanning_directory));
         mProgressDialog.setOnCancelListener(new OnCancelListener() {
             public void onCancel(DialogInterface dialog) {
                 // If the scanning dialog is cancelled, then stop the task and finish the activity
@@ -109,6 +111,7 @@
 
     @Override
     protected void onDestroy() {
+        Log.e("Suid", "onDestroy");
         super.onDestroy();
         if (mFindSuidFilesTask != null) {
             mFindSuidFilesTask.cancel(true);
@@ -223,10 +226,13 @@
                             .setOnCancelListener(new OnCancelListener() {
                                 public void onCancel(DialogInterface dialog) {
                                     // No reason to hang around if there were no offending files.
+                                    TestResult.setPassedResult(SuidFilesActivity.this);
                                     finish();
                                 }
                             })
                             .show();
+                } else {
+                    TestResult.setFailedResult(SuidFilesActivity.this);
                 }
             }
         }
diff --git a/apps/CtsVerifier/tests/src/com/android/cts/verifier/TestListActivityTest.java b/apps/CtsVerifier/tests/src/com/android/cts/verifier/TestListActivityTest.java
index 5175f33..5d0f918 100644
--- a/apps/CtsVerifier/tests/src/com/android/cts/verifier/TestListActivityTest.java
+++ b/apps/CtsVerifier/tests/src/com/android/cts/verifier/TestListActivityTest.java
@@ -19,8 +19,10 @@
 import android.app.Activity;
 import android.app.Instrumentation;
 import android.app.Instrumentation.ActivityMonitor;
+import android.content.ContentResolver;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.database.Cursor;
 import android.test.ActivityInstrumentationTestCase2;
 import android.view.KeyEvent;
 
@@ -50,9 +52,29 @@
     }
 
     /** Test that clicking on an item launches a test. */
-    public void testListItem() throws Throwable {
+    public void testLaunchAndFinishTestActivity() throws Throwable {
+        clearAllTestResults();
+        Activity testActivity = launchTestActivity();
+        finishTestActivity(testActivity);
+    }
+
+    private void clearAllTestResults() throws Throwable {
+        runTestOnUiThread(new Runnable() {
+            public void run() {
+                ContentResolver resolver = mActivity.getContentResolver();
+                resolver.delete(TestResultsProvider.CONTENT_URI, "1", null);
+
+                Cursor cursor = resolver.query(TestResultsProvider.RESULTS_ALL_CONTENT_URI,
+                        TestResultsProvider.ALL_COLUMNS, null, null, null);
+                assertEquals(0, cursor.getCount());
+                cursor.close();
+            }
+        });
+    }
+
+    private Activity launchTestActivity() {
         IntentFilter filter = new IntentFilter(Intent.ACTION_MAIN);
-        filter.addCategory(TestListActivity.CATEGORY_MANUAL_TEST);
+        filter.addCategory(TestListAdapter.CATEGORY_MANUAL_TEST);
 
         ActivityMonitor monitor = new ActivityMonitor(filter, null, false);
         mInstrumentation.addMonitor(monitor);
@@ -60,8 +82,20 @@
         sendKeys(KeyEvent.KEYCODE_ENTER);
 
         Activity activity = mInstrumentation.waitForMonitorWithTimeout(monitor,
-                TimeUnit.SECONDS.toMillis(10));
+                TimeUnit.SECONDS.toMillis(1));
         assertNotNull(activity);
+        return activity;
+    }
+
+    private void finishTestActivity(Activity activity) throws Throwable {
+        TestResult.setPassedResult(activity);
         activity.finish();
+        mInstrumentation.waitForIdleSync();
+
+        ContentResolver resolver = mActivity.getContentResolver();
+        Cursor cursor = resolver.query(TestResultsProvider.RESULTS_ALL_CONTENT_URI,
+                TestResultsProvider.ALL_COLUMNS, null, null, null);
+        assertEquals(1, cursor.getCount());
+        cursor.close();
     }
 }
diff --git a/apps/CtsVerifier/tests/src/com/android/cts/verifier/TestResultsProviderTest.java b/apps/CtsVerifier/tests/src/com/android/cts/verifier/TestResultsProviderTest.java
new file mode 100644
index 0000000..0b90061
--- /dev/null
+++ b/apps/CtsVerifier/tests/src/com/android/cts/verifier/TestResultsProviderTest.java
@@ -0,0 +1,81 @@
+package com.android.cts.verifier;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.test.ProviderTestCase2;
+
+public class TestResultsProviderTest extends ProviderTestCase2<TestResultsProvider> {
+
+    private static final String FOO_TEST_NAME = "com.android.cts.verifier.foo.FooActivity";
+
+    private static final String BAR_TEST_NAME = "com.android.cts.verifier.foo.BarActivity";
+
+    private TestResultsProvider mProvider;
+
+    public TestResultsProviderTest() {
+        super(TestResultsProvider.class, TestResultsProvider.AUTHORITY);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mProvider = getProvider();
+    }
+
+    public void testInsertUpdateDeleteByTestName() {
+        Cursor cursor = mProvider.query(TestResultsProvider.RESULTS_ALL_CONTENT_URI,
+                TestResultsProvider.ALL_COLUMNS, null, null, null);
+        assertEquals(0, cursor.getCount());
+
+        ContentValues values = new ContentValues(2);
+        values.put(TestResultsProvider.COLUMN_TEST_NAME, FOO_TEST_NAME);
+        values.put(TestResultsProvider.COLUMN_TEST_RESULT, TestResult.TEST_RESULT_FAILED);
+        assertNotNull(mProvider.insert(TestResultsProvider.CONTENT_URI, values));
+
+        cursor = mProvider.query(TestResultsProvider.RESULTS_ALL_CONTENT_URI,
+                TestResultsProvider.ALL_COLUMNS, null, null, null);
+        assertEquals(1, cursor.getCount());
+        assertTrue(cursor.moveToFirst());
+        assertEquals(FOO_TEST_NAME, cursor.getString(1));
+        assertEquals(TestResult.TEST_RESULT_FAILED, cursor.getInt(2));
+        cursor.close();
+
+        values = new ContentValues();
+        values.put(TestResultsProvider.COLUMN_TEST_NAME, BAR_TEST_NAME);
+        values.put(TestResultsProvider.COLUMN_TEST_RESULT, TestResult.TEST_RESULT_PASSED);
+        int numUpdated = mProvider.update(TestResultsProvider.CONTENT_URI, values,
+                TestResultsProvider.COLUMN_TEST_NAME + " = ?", new String[] {BAR_TEST_NAME});
+        assertEquals(0, numUpdated);
+
+        cursor = mProvider.query(Uri.withAppendedPath(TestResultsProvider.CONTENT_URI, "results"),
+                TestResultsProvider.ALL_COLUMNS, null, null, null);
+        assertEquals(1, cursor.getCount());
+        assertTrue(cursor.moveToFirst());
+        assertEquals(FOO_TEST_NAME, cursor.getString(1));
+        assertEquals(TestResult.TEST_RESULT_FAILED, cursor.getInt(2));
+        cursor.close();
+
+        values = new ContentValues(1);
+        values.put(TestResultsProvider.COLUMN_TEST_RESULT, TestResult.TEST_RESULT_PASSED);
+        numUpdated = mProvider.update(TestResultsProvider.CONTENT_URI, values,
+                TestResultsProvider.COLUMN_TEST_NAME + " = ?", new String[] {FOO_TEST_NAME});
+        assertEquals(1, numUpdated);
+
+        cursor = mProvider.query(Uri.withAppendedPath(TestResultsProvider.CONTENT_URI, "results"),
+                TestResultsProvider.ALL_COLUMNS, null, null, null);
+        assertEquals(1, cursor.getCount());
+        assertTrue(cursor.moveToFirst());
+        assertEquals(FOO_TEST_NAME, cursor.getString(1));
+        assertEquals(TestResult.TEST_RESULT_PASSED, cursor.getInt(2));
+        cursor.close();
+
+        int numDeleted = mProvider.delete(TestResultsProvider.CONTENT_URI, "1", null);
+        assertEquals(1, numDeleted);
+
+        cursor = mProvider.query(TestResultsProvider.RESULTS_ALL_CONTENT_URI,
+                TestResultsProvider.ALL_COLUMNS, null, null, null);
+        assertEquals(0, cursor.getCount());
+        cursor.close();
+    }
+}