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);
+ }
+}