summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml8
-rw-r--r--res/values/strings.xml6
-rw-r--r--src/com/android/providers/contacts/ContactsDatabaseHelper.java55
-rw-r--r--src/com/android/providers/contacts/ContactsProvider2.java246
-rw-r--r--src/com/android/providers/contacts/PackageUninstallReceiver.java46
-rw-r--r--tests/src/com/android/providers/contacts/ContactsActor.java7
-rw-r--r--tests/src/com/android/providers/contacts/DirectoryTest.java332
7 files changed, 698 insertions, 2 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 7ccfcce..3c2df6b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -58,5 +58,13 @@
<action android:name="android.intent.action.PRE_BOOT_COMPLETED"/>
</intent-filter>
</receiver>
+
+ <receiver android:name="PackageUninstallReceiver">
+ <!-- Clear out directory data for uninstalled packages -->
+ <intent-filter>
+ <action android:name="android.intent.action.PACKAGE_REMOVED" />
+ <data android:scheme="package" />
+ </intent-filter>
+ </receiver>
</application>
</manifest>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ba8a7de..0e39d61 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -31,5 +31,11 @@
<!-- Text for the notification shown when updating contacts fails because of memory shortage -->
<string name="upgrade_out_of_memory_notification_text">Select to complete the upgrade.</string>
+
+ <!-- The name of the default contact directory -->
+ <string name="default_directory">Contacts</string>
+
+ <!-- The name of the invisible local contact directory -->
+ <string name="local_invisible_directory">Other</string>
</resources>
diff --git a/src/com/android/providers/contacts/ContactsDatabaseHelper.java b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
index 85067cb..1455b2f 100644
--- a/src/com/android/providers/contacts/ContactsDatabaseHelper.java
+++ b/src/com/android/providers/contacts/ContactsDatabaseHelper.java
@@ -44,6 +44,7 @@ import android.provider.CallLog.Calls;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
import android.provider.ContactsContract.DisplayNameSources;
import android.provider.ContactsContract.FullNameStyle;
import android.provider.ContactsContract.Groups;
@@ -74,7 +75,7 @@ import java.util.Locale;
/* package */ class ContactsDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "ContactsDatabaseHelper";
- static final int DATABASE_VERSION = 401;
+ static final int DATABASE_VERSION = 402;
private static final String DATABASE_NAME = "contacts2.db";
private static final String DATABASE_PRESENCE = "presence_db";
@@ -100,6 +101,7 @@ import java.util.Locale;
public static final String PROPERTIES = "properties";
public static final String ACCOUNTS = "accounts";
public static final String VISIBLE_CONTACTS = "visible_contacts";
+ public static final String DIRECTORIES = "directories";
public static final String DATA_JOIN_MIMETYPES = "data "
+ "JOIN mimetypes ON (data.mimetype_id = mimetypes._id)";
@@ -920,6 +922,8 @@ import java.util.Locale;
// is added to the phone.
db.execSQL("INSERT INTO accounts VALUES(NULL, NULL)");
+ createDirectoriesTable(db);
+
createContactsViews(db);
createGroupsView(db);
createContactEntitiesView(db);
@@ -945,6 +949,43 @@ import java.util.Locale;
ContactsContract.AUTHORITY, new Bundle());
}
+ private void createDirectoriesTable(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + Tables.DIRECTORIES + "(" +
+ Directory._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ Directory.PACKAGE_NAME + " TEXT NOT NULL," +
+ Directory.DIRECTORY_AUTHORITY + " TEXT NOT NULL," +
+ Directory.TYPE_RESOURCE_ID + " INTEGER," +
+ Directory.ACCOUNT_TYPE + " TEXT," +
+ Directory.ACCOUNT_NAME + " TEXT," +
+ Directory.DISPLAY_NAME + " TEXT, " +
+ Directory.EXPORT_SUPPORT + " INTEGER NOT NULL" +
+ " DEFAULT " + Directory.EXPORT_SUPPORT_NONE +
+ ");");
+
+ insertDefaultDirectory(db);
+ insertLocalInvisibleDirectory(db);
+ }
+
+ private void insertDefaultDirectory(SQLiteDatabase db) {
+ ContentValues values = new ContentValues();
+ values.put(Directory._ID, Directory.DEFAULT);
+ values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName);
+ values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
+ values.put(Directory.TYPE_RESOURCE_ID, R.string.default_directory);
+ values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
+ db.insert(Tables.DIRECTORIES, null, values);
+ }
+
+ private void insertLocalInvisibleDirectory(SQLiteDatabase db) {
+ ContentValues values = new ContentValues();
+ values.put(Directory._ID, Directory.LOCAL_INVISIBLE);
+ values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName);
+ values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
+ values.put(Directory.TYPE_RESOURCE_ID, R.string.local_invisible_directory);
+ values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
+ db.insert(Tables.DIRECTORIES, null, values);
+ }
+
private static void createContactsTriggers(SQLiteDatabase db) {
/*
@@ -1484,6 +1525,11 @@ import java.util.Locale;
oldVersion = 401;
}
+ if (oldVersion == 401) {
+ upgradeToVersion402(db);
+ oldVersion = 402;
+ }
+
if (upgradeViewsAndTriggers) {
createContactsViews(db);
createGroupsView(db);
@@ -2413,6 +2459,13 @@ import java.util.Locale;
db.execSQL("DROP INDEX contacts_visible_index");
}
+ /**
+ * Introducing a new table: directories.
+ */
+ private void upgradeToVersion402(SQLiteDatabase db) {
+ createDirectoriesTable(db);
+ }
+
public String extractHandleFromEmailAddress(String email) {
Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
if (tokens.length == 0) {
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 40be58f..1e4a613 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -35,6 +35,7 @@ import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.content.SyncAdapterType;
import android.content.UriMatcher;
+import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.content.res.Configuration;
import android.database.CharArrayBuffer;
@@ -49,7 +50,9 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
+import android.net.Uri.Builder;
import android.os.AsyncTask;
+import android.os.Binder;
import android.os.Bundle;
import android.os.MemoryFile;
import android.os.RemoteException;
@@ -64,6 +67,7 @@ import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.ContactCounts;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
import android.provider.ContactsContract.DisplayNameSources;
import android.provider.ContactsContract.FullNameStyle;
import android.provider.ContactsContract.Groups;
@@ -245,6 +249,9 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
private static final int PROVIDER_STATUS = 16001;
+ private static final int DIRECTORIES = 17001;
+ private static final int DIRECTORIES_ID = 17002;
+
private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID =
RawContactsColumns.CONCRETE_ID + "=? AND "
+ GroupsColumns.CONCRETE_ACCOUNT_NAME
@@ -410,6 +417,8 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
private static final HashMap<String, String> sStatusUpdatesProjectionMap;
/** Contains Live Folders columns */
private static final HashMap<String, String> sLiveFoldersProjectionMap;
+ /** Contains {@link Directory} columns */
+ private static final HashMap<String, String> sDirectoryProjectionMap;
// where clause to update the status_updates table
private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE =
@@ -537,6 +546,9 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
LIVE_FOLDERS_CONTACTS_FAVORITES);
matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS);
+
+ matcher.addURI(ContactsContract.AUTHORITY, "directories", DIRECTORIES);
+ matcher.addURI(ContactsContract.AUTHORITY, "directories/#", DIRECTORIES_ID);
}
static {
@@ -987,12 +999,33 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
// for contacts without a photo
// sLiveFoldersProjectionMap.put(LiveFolders.ICON_BITMAP,
// Photos.DATA + " AS " + LiveFolders.ICON_BITMAP);
+
+ sDirectoryProjectionMap = new HashMap<String, String>();
+ sDirectoryProjectionMap.put(Directory._ID, Directory._ID);
+ sDirectoryProjectionMap.put(Directory.PACKAGE_NAME, Directory.PACKAGE_NAME);
+ sDirectoryProjectionMap.put(Directory.TYPE_RESOURCE_ID, Directory.TYPE_RESOURCE_ID);
+ sDirectoryProjectionMap.put(Directory.DISPLAY_NAME, Directory.DISPLAY_NAME);
+ sDirectoryProjectionMap.put(Directory.DIRECTORY_AUTHORITY, Directory.DIRECTORY_AUTHORITY);
+ sDirectoryProjectionMap.put(Directory.ACCOUNT_TYPE, Directory.ACCOUNT_TYPE);
+ sDirectoryProjectionMap.put(Directory.ACCOUNT_NAME, Directory.ACCOUNT_NAME);
+ sDirectoryProjectionMap.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT);
}
private static void addProjection(HashMap<String, String> map, String toField, String fromField) {
map.put(toField, fromField + " AS " + toField);
}
+ private static class DirectoryInfo {
+ String authority;
+ String accountName;
+ String accountType;
+ }
+
+ /**
+ * Cached information about contact directories.
+ */
+ private HashMap<String, DirectoryInfo> mDirectoryCache;
+
/**
* Handles inserts and update for a specific Data type.
*/
@@ -2509,6 +2542,11 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
break;
}
+ case DIRECTORIES: {
+ id = insertDirectory(uri, values);
+ break;
+ }
+
default:
mSyncToNetwork = true;
return mLegacyApiSupport.insert(uri, values);
@@ -2586,6 +2624,23 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
return mAccount;
}
+ private long insertDirectory(Uri uri, ContentValues values) {
+ String packageName = values.getAsString(Directory.PACKAGE_NAME);
+ if (packageName == null) {
+ throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+ "The Directory.PACKAGE_NAME field is required", uri));
+ }
+
+ if (!verifyCallingPackage(packageName)) {
+ throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+ "The supplied package name " + packageName
+ + " does not match the name of the calling package", uri));
+ }
+
+ mDirectoryCache = null;
+ return mDb.insert(Tables.DIRECTORIES, null, values);
+ }
+
/**
* Inserts an item in the contacts table
*
@@ -3508,6 +3563,10 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
return deleteStatusUpdates(selection, selectionArgs);
}
+ case DIRECTORIES_ID: {
+ return deleteDirectory(uri);
+ }
+
default: {
mSyncToNetwork = true;
return mLegacyApiSupport.delete(uri, selection, selectionArgs);
@@ -3515,6 +3574,13 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
}
}
+ private int deleteDirectory(Uri uri) {
+ mDirectoryCache = null;
+ verifyCallingPackageForDirectory(uri);
+ mSelectionArgs1[0] = String.valueOf(ContentUris.parseId(uri));
+ return mDb.delete(Tables.DIRECTORIES, Directory._ID + "=?", mSelectionArgs1);
+ }
+
public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
mGroupIdCache.clear();
final long groupMembershipMimetypeId = mDbHelper
@@ -3743,6 +3809,11 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
break;
}
+ case DIRECTORIES_ID: {
+ count = updateDirectory(uri, values, selection, selectionArgs);
+ break;
+ }
+
default: {
mSyncToNetwork = true;
return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
@@ -4159,6 +4230,21 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
return 1;
}
+ private int updateDirectory(Uri uri, ContentValues values, String selection,
+ String[] selectionArgs) {
+
+ verifyCallingPackageForDirectory(uri);
+
+ mDirectoryCache = null;
+
+ mValues.clear();
+ mValues.putAll(values);
+ mValues.remove(Directory.PACKAGE_NAME);
+
+ mSelectionArgs1[0] = String.valueOf(ContentUris.parseId(uri));
+ return mDb.update(Tables.DIRECTORIES, mValues, Directory._ID + "=?", mSelectionArgs1);
+ }
+
public void onAccountsUpdated(Account[] accounts) {
// TODO : Check the unit test.
boolean accountsChanged = false;
@@ -4212,6 +4298,11 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
"DELETE FROM " + Tables.ACCOUNTS +
" WHERE " + RawContacts.ACCOUNT_NAME + "=?" +
" AND " + RawContacts.ACCOUNT_TYPE + "=?", params);
+ mDb.execSQL(
+ "DELETE FROM " + Tables.DIRECTORIES +
+ " WHERE " + Directory.ACCOUNT_NAME + "=?" +
+ " AND " + Directory.ACCOUNT_TYPE + "=?", params);
+ mDirectoryCache = null;
}
// Find all aggregated contacts that used to contain the raw contacts
@@ -4252,6 +4343,22 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
mAccountWritability.clear();
}
+ public void onPackageUninstalled(String packageName) {
+ mDb.beginTransaction();
+ try {
+ mSelectionArgs1[0] = packageName;
+ int count =
+ mDb.delete(Tables.DIRECTORIES, Directory.PACKAGE_NAME + "=?", mSelectionArgs1);
+ if (count != 0) {
+ Log.i(TAG, "Removed contact directories for package " + packageName);
+ }
+ mDb.setTransactionSuccessful();
+ } finally {
+ mDirectoryCache = null;
+ mDb.endTransaction();
+ }
+ }
+
/**
* Finds all distinct accounts present in the specified table.
*/
@@ -4297,6 +4404,76 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
+ String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY);
+ if (directory == null || directory.equals("0")) {
+ return queryLocal(uri, projection, selection, selectionArgs, sortOrder, false);
+ } else if (directory.equals("1")) {
+ return queryLocal(uri, projection, selection, selectionArgs, sortOrder, true);
+ }
+
+ DirectoryInfo directoryInfo = getDirectoryAuthority(directory);
+ if (directoryInfo == null) {
+ throw new IllegalArgumentException(
+ mDbHelper.exceptionMessage("Invalid directory ID", uri));
+ }
+
+ Builder builder = new Uri.Builder();
+ builder.scheme(ContentResolver.SCHEME_CONTENT);
+ builder.authority(directoryInfo.authority);
+ builder.encodedPath(uri.getEncodedPath());
+ if (directoryInfo.accountName != null) {
+ builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, directoryInfo.accountName);
+ }
+ if (directoryInfo.accountType != null) {
+ builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, directoryInfo.accountType);
+ }
+ Uri directoryUri = builder.build();
+ return getContext().getContentResolver().query(directoryUri, projection, selection,
+ selectionArgs, sortOrder);
+ }
+
+ private static final class DirectoryQuery {
+ public static final String[] COLUMNS = new String[] {
+ Directory._ID,
+ Directory.DIRECTORY_AUTHORITY,
+ Directory.ACCOUNT_NAME,
+ Directory.ACCOUNT_TYPE
+ };
+
+ public static final int DIRECTORY_ID = 0;
+ public static final int AUTHORITY = 1;
+ public static final int ACCOUNT_NAME = 2;
+ public static final int ACCOUNT_TYPE = 3;
+ }
+
+ /**
+ * Reads and caches directory information for the database.
+ */
+ private DirectoryInfo getDirectoryAuthority(String directoryId) {
+ if (mDirectoryCache == null) {
+ mDirectoryCache = new HashMap<String, DirectoryInfo>();
+ Cursor cursor = mDb.query(Tables.DIRECTORIES,
+ DirectoryQuery.COLUMNS,
+ null, null, null, null, null);
+ try {
+ while (cursor.moveToNext()) {
+ DirectoryInfo info = new DirectoryInfo();
+ String id = cursor.getString(DirectoryQuery.DIRECTORY_ID);
+ info.authority = cursor.getString(DirectoryQuery.AUTHORITY);
+ info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
+ info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
+ mDirectoryCache.put(id, info);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ return mDirectoryCache.get(directoryId);
+ }
+
+ public Cursor queryLocal(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder, boolean hiddenOnly) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "query: " + uri);
}
@@ -4317,6 +4494,9 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
case CONTACTS: {
setTablesAndProjectionMapForContacts(qb, uri, projection);
+ if (hiddenOnly) {
+ qb.appendWhere(Contacts.IN_VISIBLE_GROUP + "=0");
+ }
break;
}
@@ -4437,6 +4617,9 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
filterParam = uri.getLastPathSegment();
}
setTablesAndProjectionMapForContactsWithSnippet(qb, uri, projection, filterParam);
+ if (hiddenOnly) {
+ qb.appendWhere(Contacts.IN_VISIBLE_GROUP + "=0");
+ }
break;
}
@@ -4856,6 +5039,21 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
return queryProviderStatus(uri, projection);
}
+ case DIRECTORIES : {
+ qb.setTables(Tables.DIRECTORIES);
+ qb.setProjectionMap(sDirectoryProjectionMap);
+ break;
+ }
+
+ case DIRECTORIES_ID : {
+ long directoryId = ContentUris.parseId(uri);
+ qb.setTables(Tables.DIRECTORIES);
+ qb.setProjectionMap(sDirectoryProjectionMap);
+ selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(directoryId));
+ qb.appendWhere(Directory._ID + "=?");
+ break;
+ }
+
default:
return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
sortOrder, limit);
@@ -5792,7 +5990,10 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
return SearchManager.SUGGEST_MIME_TYPE;
case SEARCH_SHORTCUT:
return SearchManager.SHORTCUT_MIME_TYPE;
-
+ case DIRECTORIES:
+ return Directory.CONTENT_TYPE;
+ case DIRECTORIES_ID:
+ return Directory.CONTENT_ITEM_TYPE;
default:
return mLegacyApiSupport.getType(uri);
}
@@ -6081,6 +6282,49 @@ public class ContactsProvider2 extends SQLiteContentProvider implements OnAccoun
return writable;
}
+ private void verifyCallingPackageForDirectory(Uri uri) {
+ String packageName = null;
+ mSelectionArgs1[0] = String.valueOf(ContentUris.parseId(uri));
+ Cursor cursor = mDb.query(Tables.DIRECTORIES,
+ new String[] { Directory.PACKAGE_NAME }, Directory._ID + "=?",
+ mSelectionArgs1, null, null, null);
+ try {
+ if (cursor.moveToFirst()) {
+ packageName = cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
+ }
+
+ if (packageName == null) {
+ throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+ "Directory not found", uri));
+ }
+
+ if (!verifyCallingPackage(packageName)) {
+ throw new IllegalArgumentException(mDbHelper.exceptionMessage(
+ "The supplied package name " + packageName
+ + " does not match the name of the calling package", uri));
+ }
+ }
+
+ /**
+ * Returns true iff the package is one of the packages owned by the caller.
+ */
+ private boolean verifyCallingPackage(String packageName) {
+ final PackageManager pm = getContext().getPackageManager();
+ final int callingUid = Binder.getCallingUid();
+ final String[] callerPackages = pm.getPackagesForUid(callingUid);
+ if (callerPackages != null) {
+ for (int i = 0; i < callerPackages.length; i++) {
+ if (packageName.equals(callerPackages[i])) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
boolean defaultValue) {
diff --git a/src/com/android/providers/contacts/PackageUninstallReceiver.java b/src/com/android/providers/contacts/PackageUninstallReceiver.java
new file mode 100644
index 0000000..b493574
--- /dev/null
+++ b/src/com/android/providers/contacts/PackageUninstallReceiver.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2009 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.contacts;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+/**
+ * Package uninstall receiver whose job is to remove orphaned contact directories.
+ */
+public class PackageUninstallReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
+ // TODO: do the work in an IntentService
+ Uri packageUri = intent.getData();
+ String packageName = packageUri.getSchemeSpecificPart();
+ IContentProvider iprovider =
+ context.getContentResolver().acquireProvider(ContactsContract.AUTHORITY);
+ ContentProvider provider = ContentProvider.coerceToLocalContentProvider(iprovider);
+ if (provider instanceof ContactsProvider2) {
+ ((ContactsProvider2)provider).onPackageUninstalled(packageName);
+ }
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index 6e0e47a..ec18b15 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -157,6 +157,13 @@ public class ContactsActor {
public ContentResolver getContentResolver() {
return mResolver;
}
+
+ @Override
+ public ApplicationInfo getApplicationInfo() {
+ ApplicationInfo ai = new ApplicationInfo();
+ ai.packageName = "contactsTestPackage";
+ return ai;
+ }
}
private static class RestrictionMockResources extends MockResources {
diff --git a/tests/src/com/android/providers/contacts/DirectoryTest.java b/tests/src/com/android/providers/contacts/DirectoryTest.java
new file mode 100644
index 0000000..7940c61
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/DirectoryTest.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2009 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.contacts;
+
+import com.google.android.collect.Lists;
+
+import android.accounts.Account;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.Settings;
+import android.test.mock.MockContentProvider;
+import android.test.suitebuilder.annotation.LargeTest;
+
+
+/**
+ * Unit tests for {@link ContactsProvider2}, directory functionality.
+ *
+ * Run the test like this:
+ * <code>
+ * adb shell am instrument -e class com.android.providers.contacts.DirectoryTest -w \
+ * com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
+ * </code>
+ */
+@LargeTest
+public class DirectoryTest extends BaseContactsProvider2Test {
+
+ public void testDefaultDirectory() {
+ ContentValues values = new ContentValues();
+ Uri defaultDirectoryUri =
+ ContentUris.withAppendedId(Directory.CONTENT_URI, Directory.DEFAULT);
+
+ values.put(Directory.PACKAGE_NAME, "contactsTestPackage");
+ values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
+ values.put(Directory.TYPE_RESOURCE_ID, R.string.default_directory);
+ values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
+ values.putNull(Directory.ACCOUNT_NAME);
+ values.putNull(Directory.ACCOUNT_TYPE);
+ values.putNull(Directory.DISPLAY_NAME);
+
+ assertStoredValues(defaultDirectoryUri, values);
+ }
+
+ public void testInvisibleLocalDirectory() {
+ ContentValues values = new ContentValues();
+ Uri defaultDirectoryUri =
+ ContentUris.withAppendedId(Directory.CONTENT_URI, Directory.LOCAL_INVISIBLE);
+
+ values.put(Directory.PACKAGE_NAME, "contactsTestPackage");
+ values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
+ values.put(Directory.TYPE_RESOURCE_ID, R.string.local_invisible_directory);
+ values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
+ values.putNull(Directory.ACCOUNT_NAME);
+ values.putNull(Directory.ACCOUNT_TYPE);
+ values.putNull(Directory.DISPLAY_NAME);
+
+ assertStoredValues(defaultDirectoryUri, values);
+ }
+
+ public void testDirectoryInsert() {
+ ContentValues values = new ContentValues();
+ values.put(Directory.PACKAGE_NAME, ContactsActor.PACKAGE_GREY);
+ values.put(Directory.DIRECTORY_AUTHORITY, "my_authority");
+ values.put(Directory.TYPE_RESOURCE_ID, 42);
+ values.put(Directory.DISPLAY_NAME, "Universe");
+ values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT);
+ values.put(Directory.ACCOUNT_NAME, "accountName");
+ values.put(Directory.ACCOUNT_TYPE, "accountType");
+
+ mActor.ensureCallingPackage();
+
+ Uri uri = mResolver.insert(Directory.CONTENT_URI, values);
+ assertStoredValues(uri, values);
+ }
+
+ public void testDirectoryInsertWrongPackage() {
+ ContentValues values = new ContentValues();
+ values.put(Directory.PACKAGE_NAME, "wrong.package");
+ values.put(Directory.DIRECTORY_AUTHORITY, "my_authority");
+
+ mActor.ensureCallingPackage();
+
+ try {
+ mResolver.insert(Directory.CONTENT_URI, values);
+ fail("Was expecting an IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ // Expected
+ }
+ }
+
+ public void testDirectoryUpdate() {
+ ContentValues values = new ContentValues();
+ values.put(Directory.PACKAGE_NAME, ContactsActor.PACKAGE_GREY);
+ values.put(Directory.DIRECTORY_AUTHORITY, "my_authority");
+ values.put(Directory.TYPE_RESOURCE_ID, 42);
+ values.put(Directory.DISPLAY_NAME, "Universe");
+ values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT);
+ values.put(Directory.ACCOUNT_NAME, "accountName");
+ values.put(Directory.ACCOUNT_TYPE, "accountType");
+
+ mActor.ensureCallingPackage();
+
+ Uri uri = mResolver.insert(Directory.CONTENT_URI, values);
+
+ values.put(Directory.DIRECTORY_AUTHORITY, "different_authority");
+ values.put(Directory.TYPE_RESOURCE_ID, 43);
+ values.put(Directory.DISPLAY_NAME, "Beyond Universe");
+ values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
+ values.put(Directory.ACCOUNT_NAME, "newName");
+ values.put(Directory.ACCOUNT_TYPE, "newType");
+
+ int count = mResolver.update(uri, values, null, null);
+ assertEquals(1, count);
+ assertStoredValues(uri, values);
+ }
+
+ public void testDirectoryUpdateWrongPackage() {
+ ContentValues values = new ContentValues();
+ values.put(Directory.PACKAGE_NAME, ContactsActor.PACKAGE_GREY);
+ values.put(Directory.DIRECTORY_AUTHORITY, "my_authority");
+
+ mActor.ensureCallingPackage();
+ Uri uri = mResolver.insert(Directory.CONTENT_URI, values);
+
+ values.put(Directory.DIRECTORY_AUTHORITY, "different_authority");
+
+ mActor.packageName = "different.package";
+ mActor.ensureCallingPackage();
+
+ try {
+ mResolver.update(uri, values, null, null);
+ fail("Was expecting an IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ // Expected
+ }
+ }
+
+ public void testDirectorDelete() {
+ ContentValues values = new ContentValues();
+ values.put(Directory.PACKAGE_NAME, ContactsActor.PACKAGE_GREY);
+ values.put(Directory.DIRECTORY_AUTHORITY, "my_authority");
+
+ mActor.ensureCallingPackage();
+
+ Uri uri = mResolver.insert(Directory.CONTENT_URI, values);
+
+ mResolver.delete(uri, null, null);
+
+ assertEquals(0, getCount(uri, null, null));
+ }
+
+ public void testDirectorDeleteWrongPackage() {
+ ContentValues values = new ContentValues();
+ values.put(Directory.PACKAGE_NAME, ContactsActor.PACKAGE_GREY);
+ values.put(Directory.DIRECTORY_AUTHORITY, "my_authority");
+
+ mActor.ensureCallingPackage();
+ Uri uri = mResolver.insert(Directory.CONTENT_URI, values);
+
+ values.put(Directory.DIRECTORY_AUTHORITY, "different_authority");
+
+ mActor.packageName = "different.package";
+ mActor.ensureCallingPackage();
+ try {
+ mResolver.delete(uri, null, null);
+ fail("Was expecting an IllegalArgumentException");
+ } catch (IllegalArgumentException ex) {
+ // Expected
+ }
+ }
+
+ public void testAccountRemoval() {
+ ((ContactsProvider2)getProvider()).onAccountsUpdated(
+ new Account[]{new Account("accountName", "accountType")});
+
+ ContentValues values = new ContentValues();
+ values.put(Directory.PACKAGE_NAME, ContactsActor.PACKAGE_GREY);
+ values.put(Directory.DIRECTORY_AUTHORITY, "my_authority");
+ values.put(Directory.ACCOUNT_NAME, "accountName");
+ values.put(Directory.ACCOUNT_TYPE, "accountType");
+
+ mActor.ensureCallingPackage();
+ Uri uri = mResolver.insert(Directory.CONTENT_URI, values);
+
+ ((ContactsProvider2)getProvider()).onAccountsUpdated(
+ new Account[]{new Account("name", "type")});
+
+ // Removing the account should trigger the removal of the directory
+ assertEquals(0, getCount(uri, null, null));
+ }
+
+ public void testPackageRemoval() {
+ ContentValues values = new ContentValues();
+ values.put(Directory.PACKAGE_NAME, ContactsActor.PACKAGE_GREY);
+ values.put(Directory.DIRECTORY_AUTHORITY, "my_authority");
+
+ mActor.ensureCallingPackage();
+ Uri uri = mResolver.insert(Directory.CONTENT_URI, values);
+
+ ((ContactsProvider2)getProvider()).onPackageUninstalled(ContactsActor.PACKAGE_GREY);
+
+ // Uninstalling the package should trigger the removal of the directory
+ assertEquals(0, getCount(uri, null, null));
+ }
+
+ public void testForwardingToLocalContacts() {
+ long contactId = queryContactId(createRawContactWithName());
+
+ Uri contentUri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)).build();
+
+ Cursor cursor = mResolver.query(contentUri,
+ new String[]{Contacts._ID, Contacts.DISPLAY_NAME}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ cursor.moveToFirst();
+ assertEquals(contactId, cursor.getLong(0));
+ assertEquals("John Doe", cursor.getString(1));
+ cursor.close();
+ }
+
+ public void testForwardingToLocalInvisibleContacts() {
+ long visibleContactId = queryContactId(createRawContactWithName("Bob", "Parr"));
+
+ assertStoredValue(ContentUris.withAppendedId(Contacts.CONTENT_URI, visibleContactId),
+ Contacts.IN_VISIBLE_GROUP, "1");
+
+ long hiddenContactId = queryContactId(createRawContactWithName("Helen", "Parr",
+ new Account("accountName", "accountType")));
+
+ assertStoredValue(ContentUris.withAppendedId(Contacts.CONTENT_URI, hiddenContactId),
+ Contacts.IN_VISIBLE_GROUP, "0");
+
+ Uri contentUri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.LOCAL_INVISIBLE))
+ .build();
+
+ Cursor cursor = mResolver.query(contentUri,
+ new String[]{Contacts._ID, Contacts.DISPLAY_NAME}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ cursor.moveToFirst();
+ assertEquals(hiddenContactId, cursor.getLong(0));
+ assertEquals("Helen Parr", cursor.getString(1));
+ cursor.close();
+
+ Uri filterUri = Contacts.CONTENT_FILTER_URI.buildUpon().appendEncodedPath("parr")
+ .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+ String.valueOf(Directory.LOCAL_INVISIBLE)).build();
+
+ cursor = mResolver.query(filterUri,
+ new String[]{Contacts._ID, Contacts.DISPLAY_NAME}, null, null, null);
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ cursor.moveToFirst();
+ assertEquals(hiddenContactId, cursor.getLong(0));
+ assertEquals("Helen Parr", cursor.getString(1));
+ cursor.close();
+ }
+
+ public void testForwardingToDirectoryProvider() throws Exception {
+ addProvider(TestProvider.class, "test_authority");
+
+ ContentValues values = new ContentValues();
+ values.put(Directory.PACKAGE_NAME, ContactsActor.PACKAGE_GREY);
+ values.put(Directory.DIRECTORY_AUTHORITY, "test_authority");
+
+ mActor.ensureCallingPackage();
+ Uri uri = mResolver.insert(Directory.CONTENT_URI, values);
+ long directoryId = ContentUris.parseId(uri);
+
+ Uri contentUri = Contacts.CONTENT_URI.buildUpon().appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
+
+ // The request should be forwarded to TestProvider, which will simply
+ // package arguments and return them to us for verification
+ Cursor cursor = mResolver.query(contentUri,
+ new String[]{"f1", "f2"}, "query", new String[]{"s1", "s2"}, "so");
+ assertNotNull(cursor);
+ assertEquals(1, cursor.getCount());
+ cursor.moveToFirst();
+ assertEquals("[f1, f2]", cursor.getString(cursor.getColumnIndex("projection")));
+ assertEquals("query", cursor.getString(cursor.getColumnIndex("selection")));
+ assertEquals("[s1, s2]", cursor.getString(cursor.getColumnIndex("selectionArgs")));
+ assertEquals("so", cursor.getString(cursor.getColumnIndex("sortOrder")));
+ cursor.close();
+ }
+
+ public static class TestProvider extends MockContentProvider {
+
+ @Override
+ public void attachInfo(Context context, ProviderInfo info) {
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ MatrixCursor cursor = new MatrixCursor(new String[] {
+ "projection", "selection", "selectionArgs", "sortOrder"
+ });
+ cursor.addRow(new Object[] {
+ Lists.newArrayList(projection).toString(),
+ selection,
+ Lists.newArrayList(selectionArgs).toString(),
+ sortOrder
+ });
+ return cursor;
+ }
+ }
+}
+