diff options
-rw-r--r-- | AndroidManifest.xml | 8 | ||||
-rw-r--r-- | res/values/strings.xml | 6 | ||||
-rw-r--r-- | src/com/android/providers/contacts/ContactsDatabaseHelper.java | 55 | ||||
-rw-r--r-- | src/com/android/providers/contacts/ContactsProvider2.java | 246 | ||||
-rw-r--r-- | src/com/android/providers/contacts/PackageUninstallReceiver.java | 46 | ||||
-rw-r--r-- | tests/src/com/android/providers/contacts/ContactsActor.java | 7 | ||||
-rw-r--r-- | tests/src/com/android/providers/contacts/DirectoryTest.java | 332 |
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; + } + } +} + |