am 7fc8966f: (-s ours) Import revised translations.  DO NOT MERGE

* commit '7fc8966fd8a05e438f1fc053b6d2255688276c37':
  Import revised translations.  DO NOT MERGE
diff --git a/Android.mk b/Android.mk
index f436b87..c7e96db 100644
--- a/Android.mk
+++ b/Android.mk
@@ -3,11 +3,14 @@
 
 LOCAL_MODULE_TAGS := optional
 
-LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-LOCAL_JAVA_LIBRARIES := ext
+LOCAL_JAVA_LIBRARIES := ext guava
 
 LOCAL_PACKAGE_NAME := ApplicationsProvider
 LOCAL_CERTIFICATE := shared
 
 include $(BUILD_PACKAGE)
+
+# Also build our test apk
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ec3fd69..4e4bfa2 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2,6 +2,8 @@
         package="com.android.providers.applications"
         android:sharedUserId="android.uid.shared">
 
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+
     <application android:process="android.process.acore" android:label="@string/app_label">
 
         <provider android:name="ApplicationsProvider" android:authorities="applications"
@@ -19,7 +21,7 @@
 
             <meta-data android:name="android.app.searchable"
                     android:resource="@xml/searchable" />
-
         </activity>
+
     </application>
 </manifest>
diff --git a/src/com/android/providers/applications/ApplicationLauncher.java b/src/com/android/providers/applications/ApplicationLauncher.java
index a866091..0b38dfe 100644
--- a/src/com/android/providers/applications/ApplicationLauncher.java
+++ b/src/com/android/providers/applications/ApplicationLauncher.java
@@ -100,5 +100,4 @@
             }
         }
     }
-
 }
diff --git a/src/com/android/providers/applications/ApplicationsProvider.java b/src/com/android/providers/applications/ApplicationsProvider.java
index c6a4c64..e46aa52 100644
--- a/src/com/android/providers/applications/ApplicationsProvider.java
+++ b/src/com/android/providers/applications/ApplicationsProvider.java
@@ -18,6 +18,9 @@
 
 import com.android.internal.content.PackageMonitor;
 
+import android.app.ActivityManager;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
 import android.app.SearchManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -36,7 +39,9 @@
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
 import android.database.sqlite.SQLiteQueryBuilder;
+import android.database.sqlite.SQLiteStatement;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.HandlerThread;
@@ -46,10 +51,14 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import java.lang.Runnable;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import com.google.common.annotations.VisibleForTesting;
+
 /**
  * Fetches the list of applications installed on the phone to provide search suggestions.
  * If the functionality of this provider changes, the documentation at
@@ -75,6 +84,11 @@
 
     // Messages for mHandler
     private static final int MSG_UPDATE_ALL = 0;
+    private static final int MSG_UPDATE_APP_LAUNCH_COUNTS = 1;
+
+    // A request to update application launch counts.
+    private static final String INTENT_UPDATE_LAUNCH_COUNTS =
+            ApplicationsProvider.class.getName() + ".UPDATE_LAUNCH_COUNTS";
 
     public static final String _ID = "_id";
     public static final String NAME = "name";
@@ -82,6 +96,7 @@
     public static final String PACKAGE = "package";
     public static final String CLASS = "class";
     public static final String ICON = "icon";
+    public static final String LAUNCH_COUNT = "launch_count";
 
     private static final String APPLICATIONS_TABLE = "applications";
 
@@ -94,10 +109,16 @@
     private static final HashMap<String, String> sSearchProjectionMap =
             buildSearchProjectionMap();
 
+    /**
+     * An in-memory database storing the details of applications installed on
+     * the device. Populated when the ApplicationsProvider is launched.
+     */
     private SQLiteDatabase mDb;
+
     // Handler that runs DB updates.
     private Handler mHandler;
 
+    private Runnable onApplicationsListUpdated;
 
     /**
      * We delay application updates by this many millis to avoid doing more than one update to the
@@ -105,6 +126,11 @@
      */
     private static final long UPDATE_DELAY_MILLIS = 1000L;
 
+    /**
+     * Application launch counts will be updated every 6 hours.
+     */
+    private static final long LAUNCH_COUNT_UPDATE_INTERVAL = AlarmManager.INTERVAL_HOUR * 6;
+
     private static UriMatcher buildUriMatcher() {
         UriMatcher matcher =  new UriMatcher(UriMatcher.NO_MATCH);
         matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
@@ -146,6 +172,20 @@
         }
     };
 
+    // Broadcast receiver receiving "update application launch counts" requests
+    // fired by the AlarmManager at regular intervals.
+    private BroadcastReceiver mLaunchCountUpdateReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (INTENT_UPDATE_LAUNCH_COUNTS.equals(action)) {
+                if (DBG) Log.d(TAG, "Launch count update requested");
+                mHandler.removeMessages(MSG_UPDATE_APP_LAUNCH_COUNTS);
+                Message.obtain(mHandler, MSG_UPDATE_APP_LAUNCH_COUNTS).sendToTarget();
+            }
+        }
+    };
+
     @Override
     public boolean onCreate() {
         createDatabase();
@@ -160,6 +200,7 @@
         mHandler = new UpdateHandler(thread.getLooper());
         // Kick off first apps update
         postUpdateAll();
+        scheduleRegularLaunchCountUpdates();
         return true;
     }
 
@@ -175,6 +216,9 @@
                 case MSG_UPDATE_ALL:
                     updateApplicationsList(null);
                     break;
+                case MSG_UPDATE_APP_LAUNCH_COUNTS:
+                    updateLaunchCounts();
+                    break;
                 default:
                     Log.e(TAG, "Unknown message: " + msg.what);
                     break;
@@ -194,6 +238,27 @@
         mHandler.sendMessageDelayed(msg, UPDATE_DELAY_MILLIS);
     }
 
+    @VisibleForTesting
+    protected void scheduleRegularLaunchCountUpdates() {
+        // Set up a recurring event that sends an intent caught by the
+        // mLaunchCountUpdateReceiver event handler. This event handler
+        // will update application launch counts in the ApplicationsProvider's
+        // database.
+        getContext().registerReceiver(
+                mLaunchCountUpdateReceiver,
+                new IntentFilter(INTENT_UPDATE_LAUNCH_COUNTS));
+
+        PendingIntent updateLaunchCountsIntent = PendingIntent.getBroadcast(
+                getContext(), 0, new Intent(INTENT_UPDATE_LAUNCH_COUNTS),
+                PendingIntent.FLAG_CANCEL_CURRENT);
+
+        // Schedule the recurring event.
+        AlarmManager alarmManager =
+                (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
+        alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME, LAUNCH_COUNT_UPDATE_INTERVAL,
+                LAUNCH_COUNT_UPDATE_INTERVAL, updateLaunchCountsIntent);
+    }
+
     // ----------
     // END ASYC UPDATE CODE
     // ----------
@@ -210,7 +275,8 @@
                 DESCRIPTION + " description TEXT," +
                 PACKAGE + " TEXT," +
                 CLASS + " TEXT," +
-                ICON + " TEXT" +
+                ICON + " TEXT," +
+                LAUNCH_COUNT + " INTEGER DEFAULT 0" +
                 ");");
         // Needed for efficient update and remove
         mDb.execSQL("CREATE INDEX applicationsComponentIndex ON " + APPLICATIONS_TABLE + " (" 
@@ -258,7 +324,7 @@
             case SEARCH:
                 return Applications.APPLICATION_DIR_TYPE;
             default:
-                throw new IllegalArgumentException("Unknown URL " + uri);
+                throw new IllegalArgumentException("URL " + uri + " doesn't support querying.");
         }
     }
 
@@ -304,7 +370,7 @@
                 return getSearchResults(query, projectionIn);
             }
             default:
-                throw new IllegalArgumentException("Unknown URL " + uri);
+                throw new IllegalArgumentException("URL " + uri + " doesn't support querying.");
         }
     }
 
@@ -351,14 +417,28 @@
         }
         // don't return duplicates when there are two matching tokens for an app
         String groupBy = APPLICATIONS_TABLE + "." + _ID;
-        // order first by whether it a full prefix match, then by name
+        String orderBy = getOrderBy();
+        Cursor cursor = qb.query(mDb, projectionIn, null, null, groupBy, null, orderBy);
+        if (DBG) Log.d(TAG, "Returning " + cursor.getCount() + " results for " + query);
+        return cursor;
+    }
+
+    private String getOrderBy() {
+        // order first by whether it a full prefix match, then by launch
+        // count (if allowed, frequently used apps rank higher), then name
         // MIN(token_index) != 0 is true for non-full prefix matches,
         // and since false (0) < true(1), this expression makes sure
         // that full prefix matches come first.
-        String order = "MIN(token_index) != 0, " + NAME;
-        Cursor cursor = qb.query(mDb, projectionIn, null, null, groupBy, null, order);
-        if (DBG) Log.d(TAG, "Returning " + cursor.getCount() + " results for " + query);
-        return cursor;
+        StringBuilder orderBy = new StringBuilder();
+        orderBy.append("MIN(token_index) != 0");
+
+        if (canRankByLaunchCount()) {
+            orderBy.append(", " + LAUNCH_COUNT + " DESC");
+        }
+
+        orderBy.append(", " + NAME);
+
+        return orderBy.toString();
     }
 
     @SuppressWarnings("deprecation")
@@ -421,6 +501,9 @@
         int packageCol = inserter.getColumnIndex(PACKAGE);
         int classCol = inserter.getColumnIndex(CLASS);
         int iconCol = inserter.getColumnIndex(ICON);
+        int launchCountCol = inserter.getColumnIndex(LAUNCH_COUNT);
+
+        Map<String, Integer> launchCounts = fetchLaunchCounts();
 
         mDb.beginTransaction();
         try {
@@ -433,22 +516,31 @@
                 // Limit to activities in the package, if given
                 mainIntent.setPackage(packageName);
             }
-            final PackageManager manager = getContext().getPackageManager();
+            final PackageManager manager = getPackageManager();
             List<ResolveInfo> activities = manager.queryIntentActivities(mainIntent, 0);
             int activityCount = activities == null ? 0 : activities.size();
             for (int i = 0; i < activityCount; i++) {
                 ResolveInfo info = activities.get(i);
                 String title = info.loadLabel(manager).toString();
+                String activityClassName = info.activityInfo.name;
                 if (TextUtils.isEmpty(title)) {
-                    title = info.activityInfo.name;
+                    title = activityClassName;
                 }
+
+                String activityPackageName = info.activityInfo.applicationInfo.packageName;
+                Integer launchCount = launchCounts.get(activityPackageName);
+                if (launchCount == null) {
+                    launchCount = 0;
+                }
+
                 String icon = getActivityIconUri(info.activityInfo);
                 inserter.prepareForInsert();
                 inserter.bind(nameCol, title);
                 inserter.bind(descriptionCol, description);
-                inserter.bind(packageCol, info.activityInfo.applicationInfo.packageName);
-                inserter.bind(classCol, info.activityInfo.name);
+                inserter.bind(packageCol, activityPackageName);
+                inserter.bind(classCol, activityClassName);
                 inserter.bind(iconCol, icon);
+                inserter.bind(launchCountCol, launchCount);
                 inserter.execute();
             }
             mDb.setTransactionSuccessful();
@@ -456,13 +548,39 @@
             mDb.endTransaction();
             inserter.close();
         }
+
+        if (onApplicationsListUpdated != null) {
+            onApplicationsListUpdated.run();
+        }
+
         if (DBG) Log.d(TAG, "Finished updating database.");
     }
 
+    @VisibleForTesting
+    protected void updateLaunchCounts() {
+        Map<String, Integer> launchCounts = fetchLaunchCounts();
+
+        mDb.beginTransaction();
+        try {
+            for (String packageName : launchCounts.keySet()) {
+                ContentValues updatedValues = new ContentValues();
+                updatedValues.put(LAUNCH_COUNT, launchCounts.get(packageName));
+
+                mDb.update(APPLICATIONS_TABLE, updatedValues,
+                        PACKAGE + " = ?", new String[] { packageName });
+            }
+            mDb.setTransactionSuccessful();
+        } finally {
+            mDb.endTransaction();
+        }
+
+        if (DBG) Log.d(TAG, "Finished updating application launch counts in database.");
+    }
+
     private String getActivityIconUri(ActivityInfo activityInfo) {
         int icon = activityInfo.getIconResource();
         if (icon == 0) return null;
-        Uri uri = getResourceUri(getContext(), activityInfo.applicationInfo, icon);
+        Uri uri = getResourceUri(activityInfo.applicationInfo, icon);
         return uri == null ? null : uri.toString();
     }
 
@@ -491,9 +609,9 @@
         throw new UnsupportedOperationException();
     }
 
-    private static Uri getResourceUri(Context context, ApplicationInfo appInfo, int res) {
+    private Uri getResourceUri(ApplicationInfo appInfo, int res) {
         try {
-            Resources resources = context.getPackageManager().getResourcesForApplication(appInfo);
+            Resources resources = getPackageManager().getResourcesForApplication(appInfo);
             return getResourceUri(resources, appInfo.packageName, res);
         } catch (PackageManager.NameNotFoundException e) {
             return null;
@@ -524,4 +642,37 @@
         return uriBuilder.build();
     }
 
+    @VisibleForTesting
+    protected Map<String, Integer> fetchLaunchCounts() {
+        try {
+            ActivityManager activityManager = (ActivityManager)
+                    getContext().getSystemService(Context.ACTIVITY_SERVICE);
+
+            Map<String, Integer> allPackageLaunchCounts = activityManager.getAllPackageLaunchCounts();
+            return allPackageLaunchCounts;
+        } catch (Exception e) {
+            Log.w(TAG, "Could not fetch launch counts", e);
+            return new HashMap<String, Integer>();
+        }
+    }
+
+    @VisibleForTesting
+    protected PackageManager getPackageManager() {
+        return getContext().getPackageManager();
+    }
+
+    @VisibleForTesting
+    protected boolean canRankByLaunchCount() {
+        // Only the global search system is allowed to rank apps by launch count.
+        // Without this restriction the ApplicationsProvider could leak
+        // information about the user's behavior to applications.
+        return (PackageManager.PERMISSION_GRANTED ==
+                getContext().checkCallingPermission(android.Manifest.permission.GLOBAL_SEARCH));
+    }
+
+    @VisibleForTesting
+    protected void setOnApplicationsListUpdated(Runnable onApplicationsListUpdated) {
+        this.onApplicationsListUpdated = onApplicationsListUpdated;
+    }
+
 }
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..7d1fe34
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,18 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_INSTRUMENTATION_FOR := ApplicationsProvider
+
+# framework is required to access android.provider.Applications
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_PACKAGE_NAME := ApplicationsProviderTests
+LOCAL_CERTIFICATE := shared
+
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..c614afc
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.providers.applications.tests">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+        android:targetPackage="com.android.providers.applications"
+        android:label="Tests for the ApplicationsProvider">
+    </instrumentation>
+</manifest>
diff --git a/tests/runtests b/tests/runtests
new file mode 100644
index 0000000..be1c779
--- /dev/null
+++ b/tests/runtests
@@ -0,0 +1,6 @@
+mmm packages/providers/ApplicationsProvider || return
+mmm packages/providers/ApplicationsProvider/tests || return
+adb install -r ${OUT}/system/app/ApplicationsProvider.apk || return
+adb install -r ${OUT}/data/app/ApplicationsProviderTests.apk || return
+adb shell am instrument -w -e class com.android.providers.applications.ApplicationsProviderTest com.android.providers.applications.tests/android.test.InstrumentationTestRunner || return
+
diff --git a/tests/src/com/android/providers/applications/ApplicationsProviderForTesting.java b/tests/src/com/android/providers/applications/ApplicationsProviderForTesting.java
new file mode 100644
index 0000000..fa8645c
--- /dev/null
+++ b/tests/src/com/android/providers/applications/ApplicationsProviderForTesting.java
@@ -0,0 +1,64 @@
+/*
+ * 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.providers.applications;
+
+import android.content.pm.PackageManager;
+
+import java.util.Map;
+
+/**
+ * An extension of {@link ApplicationsProvider} that makes its testing easier.
+ */
+public class ApplicationsProviderForTesting extends ApplicationsProvider {
+
+    private PackageManager mMockPackageManager;
+
+    private MockActivityManager mMockActivityManager;
+
+    private boolean mCanRankByLaunchCount;
+
+    @Override
+    protected PackageManager getPackageManager() {
+        return mMockPackageManager;
+    }
+
+    protected void setMockPackageManager(PackageManager mockPackageManager) {
+        mMockPackageManager = mockPackageManager;
+    }
+
+    @Override
+    protected Map<String, Integer> fetchLaunchCounts() {
+        return mMockActivityManager.getAllPackageLaunchCounts();
+    }
+
+    protected void setMockActivityManager(MockActivityManager mockActivityManager) {
+        mMockActivityManager = mockActivityManager;
+    }
+
+    protected void setCanRankByLaunchCount(boolean canRankByLaunchCount) {
+        mCanRankByLaunchCount = canRankByLaunchCount;
+    }
+
+    @Override
+    protected boolean canRankByLaunchCount() {
+        return mCanRankByLaunchCount;
+    }
+
+    @Override
+    protected void scheduleRegularLaunchCountUpdates() {
+    }
+}
diff --git a/tests/src/com/android/providers/applications/ApplicationsProviderTest.java b/tests/src/com/android/providers/applications/ApplicationsProviderTest.java
new file mode 100644
index 0000000..77bc064
--- /dev/null
+++ b/tests/src/com/android/providers/applications/ApplicationsProviderTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.providers.applications;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.Applications;
+import android.test.ProviderTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.util.concurrent.FutureTask;
+
+
+/**
+ * Instrumentation test for the ApplicationsProvider.
+ *
+ * The tests use an IsolatedContext, and are not affected by the real list of
+ * applications on the device. The ApplicationsProvider's persistent database
+ * is also created in an isolated context so it doesn't interfere with the
+ * database of the actual ApplicationsProvider installed on the device.
+ */
+@LargeTest
+public class ApplicationsProviderTest extends ProviderTestCase2<ApplicationsProviderForTesting> {
+
+    private ApplicationsProviderForTesting mProvider;
+
+    private MockActivityManager mMockActivityManager;
+
+    public ApplicationsProviderTest() {
+        super(ApplicationsProviderForTesting.class, Applications.AUTHORITY);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mProvider = getProvider();
+        mMockActivityManager = new MockActivityManager();
+        initProvider(mProvider);
+    }
+
+    /**
+     * Ensures that the ApplicationsProvider is in a ready-to-test state.
+     */
+    private void initProvider(ApplicationsProviderForTesting provider) throws Exception {
+        // Decouple the provider from Android's real list of applications.
+        MockPackageManager mockPackageManager = new MockPackageManager();
+        addDefaultTestPackages(mockPackageManager);
+        provider.setMockPackageManager(mockPackageManager);
+        provider.setMockActivityManager(mMockActivityManager);
+
+        // We need to wait for the applications database to be updated (it's
+        // updated with a slight delay by a separate thread) before we can use
+        // the ApplicationsProvider.
+        Runnable markerRunnable = new Runnable() {
+            @Override
+            public void run() {
+            }
+        };
+        FutureTask<Void> onApplicationsListUpdated = new FutureTask<Void>(markerRunnable, null);
+
+        provider.setOnApplicationsListUpdated(onApplicationsListUpdated);
+        onApplicationsListUpdated.get();
+    }
+
+    /**
+     * Register a few default applications with the ApplicationsProvider that
+     * tests can query.
+     */
+    private void addDefaultTestPackages(MockPackageManager mockPackageManager) {
+        mockPackageManager.addPackage(
+                "Email", new ComponentName("com.android.email", "com.android.email.MainView"));
+        mockPackageManager.addPackage(
+                "Ebay", new ComponentName("com.android.ebay", "com.android.ebay.Shopping"));
+        mockPackageManager.addPackage(
+                "Fakeapp", new ComponentName("com.android.fakeapp", "com.android.fakeapp.FakeView"));
+
+        // Apps that can be used to test ordering.
+        mockPackageManager.addPackage("AlphabeticA", new ComponentName("a", "a.AView"));
+        mockPackageManager.addPackage("AlphabeticB", new ComponentName("b", "b.BView"));
+        mockPackageManager.addPackage("AlphabeticC", new ComponentName("c", "c.CView"));
+        mockPackageManager.addPackage("AlphabeticD", new ComponentName("d", "d.DView"));
+    }
+
+    public void testSearch_singleResult() {
+        testSearch("ema", "Email");
+    }
+
+    public void testSearch_multipleResults() {
+        testSearch("e", "Ebay", "Email");
+    }
+
+    public void testSearch_noResults() {
+        testSearch("nosuchapp");
+    }
+
+    public void testSearch_orderingIsAlphabeticByDefault() {
+        testSearch("alphabetic", "AlphabeticA", "AlphabeticB", "AlphabeticC", "AlphabeticD");
+    }
+
+    public void testSearch_emptySearchQueryReturnsEverything() {
+        testSearch("",
+                "AlphabeticA", "AlphabeticB", "AlphabeticC", "AlphabeticD",
+                "Ebay", "Email", "Fakeapp");
+    }
+
+    public void testSearch_appsAreRankedByLaunchCountOnStartup() throws Exception {
+        mMockActivityManager.addLaunchCount("d", 3);
+        mMockActivityManager.addLaunchCount("b", 1);
+        // Missing launch count for "a".
+        mMockActivityManager.addLaunchCount("c", 0);
+
+        // Launch count database is populated on startup.
+        mProvider = createNewProvider(getMockContext());
+        mProvider.setCanRankByLaunchCount(true);
+        initProvider(mProvider);
+
+        // Override the previous provider with the new instance in the
+        // ContentResolver.
+        getMockContentResolver().addProvider(Applications.AUTHORITY, mProvider);
+
+        // New ranking: D, B, A, C (first by launch count, then
+        // - if the launch counts of two apps are equal - alphabetically)
+        testSearch("alphabetic", "AlphabeticD", "AlphabeticB", "AlphabeticA", "AlphabeticC");
+    }
+
+    public void testSearch_appsAreRankedByLaunchCountAfterScheduledUpdate() {
+        mProvider.setCanRankByLaunchCount(true);
+
+        mMockActivityManager.addLaunchCount("d", 3);
+        mMockActivityManager.addLaunchCount("b", 1);
+        // Missing launch count for "a".
+        mMockActivityManager.addLaunchCount("c", 0);
+
+        // Fetch new data from launch count provider (in the real instance this
+        // is scheduled).
+        mProvider.updateLaunchCounts();
+
+        // New ranking: D, B, A, C (first by launch count, then
+        // - if the launch counts of two apps are equal - alphabetically)
+        testSearch("alphabetic", "AlphabeticD", "AlphabeticB", "AlphabeticA", "AlphabeticC");
+    }
+
+    /**
+     * The ApplicationsProvider must only rank by launch count if the caller
+     * is a privileged application - ordering apps by launch count when asked
+     * by a regular application would leak information about user behavior.
+     */
+    public void testSearch_notAllowedToRankByLaunchCount() {
+        // Simulate non-privileged calling application.
+        mProvider.setCanRankByLaunchCount(false);
+
+        mMockActivityManager.addLaunchCount("d", 3);
+        mMockActivityManager.addLaunchCount("b", 1);
+        mMockActivityManager.addLaunchCount("a", 0);
+        mMockActivityManager.addLaunchCount("c", 0);
+
+        // Fetch new data from launch count provider.
+        mProvider.updateLaunchCounts();
+
+        // Launch count information mustn't be leaked - ranking is still
+        // alphabetic.
+        testSearch("alphabetic", "AlphabeticA", "AlphabeticB", "AlphabeticC", "AlphabeticD");
+    }
+
+    private void testSearch(String searchQuery, String... expectedResultsInOrder) {
+        Cursor cursor = Applications.search(getMockContentResolver(), searchQuery);
+
+        assertNotNull(cursor);
+        assertFalse(cursor.isClosed());
+
+        verifySearchResults(cursor, expectedResultsInOrder);
+
+        cursor.close();
+    }
+
+    private void verifySearchResults(Cursor cursor, String... expectedResultsInOrder) {
+        int expectedResultCount = expectedResultsInOrder.length;
+        assertEquals("Wrong number of app search results.",
+                expectedResultCount, cursor.getCount());
+
+        if (expectedResultCount > 0) {
+            cursor.moveToFirst();
+            int nameIndex = cursor.getColumnIndex(ApplicationsProvider.NAME);
+            // Verify that the actual results match the expected ones.
+            for (int i = 0; i < cursor.getCount(); i++) {
+                assertEquals("Wrong search result at position " + i,
+                        expectedResultsInOrder[i], cursor.getString(nameIndex));
+                cursor.moveToNext();
+            }
+        }
+    }
+
+    private ApplicationsProviderForTesting createNewProvider(Context context) throws Exception {
+        ApplicationsProviderForTesting newProviderInstance =
+                ApplicationsProviderForTesting.class.newInstance();
+        newProviderInstance.attachInfo(context, null);
+        return newProviderInstance;
+    }
+}
diff --git a/tests/src/com/android/providers/applications/MockActivityManager.java b/tests/src/com/android/providers/applications/MockActivityManager.java
new file mode 100644
index 0000000..63deacf
--- /dev/null
+++ b/tests/src/com/android/providers/applications/MockActivityManager.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.providers.applications;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The real ActivityManager is difficult to mock out (has a package visibility
+ * constructor), so this doesn't extend it.
+ */
+public class MockActivityManager {
+
+    private Map<String, Integer> mPackageLaunchCounts = new HashMap<String, Integer>();
+
+    public void addLaunchCount(String packageName, int launchCount) {
+        mPackageLaunchCounts.put(packageName, launchCount);
+    }
+
+    public Map<String, Integer> getAllPackageLaunchCounts() {
+        return mPackageLaunchCounts;
+    }
+}
diff --git a/tests/src/com/android/providers/applications/MockPackageManager.java b/tests/src/com/android/providers/applications/MockPackageManager.java
new file mode 100644
index 0000000..3d272c4
--- /dev/null
+++ b/tests/src/com/android/providers/applications/MockPackageManager.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package com.android.providers.applications;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MockPackageManager extends android.test.mock.MockPackageManager {
+
+    private List<ResolveInfo> mPackages = new ArrayList<ResolveInfo>();
+
+    /**
+     * Returns all packages registered with the mock package manager.
+     * ApplicationsProvider uses this method to query the list of applications.
+     */
+    @Override
+    public List<ResolveInfo> queryIntentActivities(Intent intent, int flags) {
+        return mPackages;
+    }
+
+    /**
+     * Adds a new package to the mock package manager.
+     *
+     * Example:
+     * addPackage("Email", new ComponentName("com.android.email", "com.android.email.MainView"));
+     *
+     * @param title the user-friendly title of the application (this is what
+     *         users will search for)
+     */
+    public void addPackage(final String title, ComponentName componentName) {
+        // Set the application's title.
+        ResolveInfo packageInfo = new ResolveInfo() {
+            @Override
+            public CharSequence loadLabel(PackageManager pm) {
+                return title;
+            }
+        };
+
+        // Set the application's ComponentName.
+        packageInfo.activityInfo = new ActivityInfo();
+        packageInfo.activityInfo.name = componentName.getClassName();
+        packageInfo.activityInfo.applicationInfo = new ApplicationInfo();
+        packageInfo.activityInfo.applicationInfo.packageName = componentName.getPackageName();
+
+        mPackages.add(packageInfo);
+    }
+}