am cf0c3aa8: Merge "CtsVerifier Test Result Infrastructure" into froyo

Merge commit 'cf0c3aa8313c4202d5af2c879824a57f906cd96f' into gingerbread

* commit 'cf0c3aa8313c4202d5af2c879824a57f906cd96f':
  CtsVerifier Test Result Infrastructure
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 6d4d52b..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();
+    }
+}