Merge "Add features and data_usage columns to calls table."
diff --git a/src/com/android/providers/contacts/ContactsProvider2.java b/src/com/android/providers/contacts/ContactsProvider2.java
index 6a15e59..50c1d49 100644
--- a/src/com/android/providers/contacts/ContactsProvider2.java
+++ b/src/com/android/providers/contacts/ContactsProvider2.java
@@ -148,9 +148,11 @@
import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
import com.android.providers.contacts.database.ContactsTableUtil;
import com.android.providers.contacts.database.DeletedContactsTableUtil;
+import com.android.providers.contacts.database.MoreDatabaseUtils;
import com.android.providers.contacts.util.Clock;
import com.android.providers.contacts.util.DbQueryUtils;
import com.android.providers.contacts.util.NeededForTesting;
+import com.android.providers.contacts.util.UserUtils;
import com.android.vcard.VCardComposer;
import com.android.vcard.VCardConfig;
@@ -307,6 +309,8 @@
private static final int CONTACTS_LOOKUP_ID_STREAM_ITEMS = 1024;
private static final int CONTACTS_FREQUENT = 1025;
private static final int CONTACTS_DELETE_USAGE = 1026;
+ private static final int CONTACTS_ID_PHOTO_CORP = 1027;
+ private static final int CONTACTS_ID_DISPLAY_PHOTO_CORP = 1028;
private static final int RAW_CONTACTS = 2002;
private static final int RAW_CONTACTS_ID = 2003;
@@ -334,6 +338,7 @@
private static final int CONTACTABLES_FILTER = 3015;
private static final int PHONE_LOOKUP = 4000;
+ private static final int PHONE_LOOKUP_ENTERPRISE = 4001;
private static final int AGGREGATION_EXCEPTIONS = 6000;
private static final int AGGREGATION_EXCEPTION_ID = 6001;
@@ -1137,6 +1142,12 @@
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_ID_PHOTO);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/display_photo",
CONTACTS_ID_DISPLAY_PHOTO);
+
+ // Special URIs that refer to contact pictures in the corp CP2.
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts_corp/#/photo", CONTACTS_ID_PHOTO_CORP);
+ matcher.addURI(ContactsContract.AUTHORITY, "contacts_corp/#/display_photo",
+ CONTACTS_ID_DISPLAY_PHOTO_CORP);
+
matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/stream_items",
CONTACTS_ID_STREAM_ITEMS);
matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER);
@@ -1225,6 +1236,8 @@
PROFILE_SYNCSTATE_ID);
matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
+ matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup_enterprise/*",
+ PHONE_LOOKUP_ENTERPRISE);
matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
AGGREGATION_EXCEPTIONS);
matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
@@ -6122,6 +6135,15 @@
break;
}
+ case PHONE_LOOKUP_ENTERPRISE: {
+ if (uri.getPathSegments().size() != 2) {
+ throw new IllegalArgumentException("Phone number missing in URI: " + uri);
+ }
+ final String phoneNumber = Uri.decode(uri.getLastPathSegment());
+ final boolean isSipAddress = uri.getBooleanQueryParameter(
+ PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false);
+ return queryPhoneLookupEnterprise(phoneNumber, projection, isSipAddress);
+ }
case PHONE_LOOKUP: {
// Phone lookup cannot be combined with a selection
selection = null;
@@ -6450,6 +6472,165 @@
}
/**
+ * Handles {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
+ */
+ // TODO Test
+ private Cursor queryPhoneLookupEnterprise(String phoneNumber, String[] projection,
+ boolean isSipAddress) {
+
+ final int corpUserId = UserUtils.getCorpUserId(getContext());
+
+ // Step 1. Look at the database on the current profile.
+ final Uri localUri = PhoneLookup.CONTENT_FILTER_URI.buildUpon().appendPath(phoneNumber)
+ .appendQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
+ String.valueOf(isSipAddress))
+ .build();
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "queryPhoneLookupEnterprise: local query URI=" + localUri);
+ }
+ final Cursor local = queryLocal(localUri, projection,
+ /* selection */ null, /* args */ null, /* order */ null, /* directory */ 0,
+ /* cancellationsignal*/ null);
+ try {
+ if (VERBOSE_LOGGING) {
+ MoreDatabaseUtils.dumpCursor(TAG, "local", local);
+ }
+
+ // If we found a result or there's no corp profile, just return it as-is.
+ if (local.getCount() > 0 || corpUserId < 0) {
+ return local;
+ }
+ } catch (Throwable th) { // If something throws, close the cursor.
+ local.close();
+ throw th;
+ }
+
+ // Step 2. No rows found in the local db, and there is a corp profile. Look at the corp
+ // DB.
+
+ // Add the user-id to the URI, like "content://USER@com.android.contacts/...".
+ final Uri remoteUri = maybeAddUserId(localUri, corpUserId);
+
+ if (VERBOSE_LOGGING) {
+ Log.v(TAG, "queryPhoneLookupEnterprise: corp query URI=" + localUri);
+ }
+ final Cursor corp = getContext().getContentResolver().query(remoteUri, projection,
+ /* selection */ null, /* args */ null, /* order */ null,
+ /* cancellationsignal*/ null);
+ try {
+ if (VERBOSE_LOGGING) {
+ MoreDatabaseUtils.dumpCursor(TAG, "corp raw", corp);
+ }
+ final Cursor rewritten = rewriteCorpPhoneLookup(corp);
+ if (VERBOSE_LOGGING) {
+ MoreDatabaseUtils.dumpCursor(TAG, "corp rewritten", rewritten);
+ }
+ return rewritten;
+ } finally {
+ // Always close the corp cursor; as we just return the rewritten one.
+ corp.close();
+ }
+ }
+
+ /**
+ * Rewrite a cursor from the corp profile for {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
+ */
+ // TODO Test
+ private static Cursor rewriteCorpPhoneLookup(Cursor original) {
+ final String[] columns = original.getColumnNames();
+ final MatrixCursor ret = new MatrixCursor(columns);
+
+ original.moveToPosition(-1);
+ while (original.moveToNext()) {
+ final int contactId = original.getInt(original.getColumnIndex(PhoneLookup._ID));
+
+ final MatrixCursor.RowBuilder builder = ret.newRow();
+
+ for (int i = 0; i < columns.length; i++) {
+ switch (columns[i]) {
+ // Set artificial photo URLs using Contacts.CORP_CONTENT_URI.
+ case PhoneLookup.PHOTO_THUMBNAIL_URI:
+ builder.add(getCorpThumbnailUri(contactId, original));
+ break;
+ case PhoneLookup.PHOTO_URI:
+ builder.add(getCorpDisplayPhotoUri(contactId, original));
+ break;
+
+ // These columns are set to null.
+ // See the javadoc on PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI for the reasons.
+ case PhoneLookup._ID:
+ case PhoneLookup.PHOTO_FILE_ID:
+ case PhoneLookup.PHOTO_ID:
+ case PhoneLookup.LOOKUP_KEY:
+ case PhoneLookup.CUSTOM_RINGTONE:
+ case PhoneLookup.IN_VISIBLE_GROUP:
+ case PhoneLookup.IN_DEFAULT_DIRECTORY:
+ builder.add(null);
+ break;
+ default:
+ // Copy the original value.
+ switch (original.getType(i)) {
+ case Cursor.FIELD_TYPE_NULL:
+ builder.add(null);
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ builder.add(original.getInt(i));
+ break;
+ case Cursor.FIELD_TYPE_FLOAT:
+ builder.add(original.getFloat(i));
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ builder.add(original.getString(i));
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ builder.add(original.getBlob(i));
+ break;
+ }
+ }
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Generate a photo URI for {@link PhoneLookup#PHOTO_THUMBNAIL_URI}.
+ *
+ * Example: "content://USER@com.android.contacts/contacts/ID/photo"
+ *
+ * {@link #openAssetFile} knows how to fetch from this URI.
+ */
+ // TODO Test
+ private static String getCorpThumbnailUri(long contactId, Cursor originalCursor) {
+ // First, check if the contact has a thumbnail.
+ if (originalCursor.isNull(
+ originalCursor.getColumnIndex(PhoneLookup.PHOTO_THUMBNAIL_URI))) {
+ // No thumbnail. Just return null.
+ return null;
+ }
+ return ContentUris.appendId(Contacts.CORP_CONTENT_URI.buildUpon(), contactId)
+ .appendPath(Contacts.Photo.CONTENT_DIRECTORY).build().toString();
+ }
+
+ /**
+ * Generate a photo URI for {@link PhoneLookup#PHOTO_URI}.
+ *
+ * Example: "content://USER@com.android.contacts/contacts/ID/display_photo"
+ *
+ * {@link #openAssetFile} knows how to fetch from this URI.
+ */
+ // TODO Test
+ private static String getCorpDisplayPhotoUri(long contactId, Cursor originalCursor) {
+ // First, check if the contact has a display photo.
+ if (originalCursor.isNull(
+ originalCursor.getColumnIndex(PhoneLookup.PHOTO_FILE_ID))) {
+ // No display photo, fall-back to thumbnail.
+ return getCorpThumbnailUri(contactId, originalCursor);
+ }
+ return ContentUris.appendId(Contacts.CORP_CONTENT_URI.buildUpon(), contactId)
+ .appendPath(Contacts.Photo.DISPLAY_PHOTO).build().toString();
+ }
+
+ /**
* Runs the query with the supplied contact ID and lookup ID. If the query succeeds,
* it returns the resulting cursor, otherwise it returns null and the calling
* method needs to resolve the lookup key and rerun the query.
@@ -7752,7 +7933,7 @@
long photoMimetypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
return openPhotoAssetFile(db, uri, mode,
Data._ID + "=? AND " + DataColumns.MIMETYPE_ID + "=" + photoMimetypeId,
- new String[] {String.valueOf(dataId)});
+ new String[]{String.valueOf(dataId)});
}
case PROFILE_AS_VCARD: {
@@ -7802,12 +7983,49 @@
return buildAssetFileDescriptor(localStream);
}
+ case CONTACTS_ID_PHOTO_CORP: {
+ final long contactId = Long.parseLong(uri.getPathSegments().get(1));
+ return openCorpContactPicture(contactId, uri, mode, /* displayPhoto =*/ false);
+ }
+
+ case CONTACTS_ID_DISPLAY_PHOTO_CORP: {
+ final long contactId = Long.parseLong(uri.getPathSegments().get(1));
+ return openCorpContactPicture(contactId, uri, mode, /* displayPhoto =*/ true);
+ }
+
default:
throw new FileNotFoundException(
mDbHelper.get().exceptionMessage("File does not exist", uri));
}
}
+ /**
+ * Handles "/contacts_corp/ID/{photo,display_photo}", which refer to contact picures in the corp
+ * CP2.
+ */
+ private AssetFileDescriptor openCorpContactPicture(long contactId, Uri uri, String mode,
+ boolean displayPhoto) throws FileNotFoundException {
+ if (!mode.equals("r")) {
+ throw new IllegalArgumentException(
+ "Photos retrieved by contact ID can only be read.");
+ }
+ final int corpUserId = UserUtils.getCorpUserId(getContext());
+ if (corpUserId < 0) {
+ // No corp profile or the currrent profile is not the personal.
+ throw new FileNotFoundException(uri.toString());
+ }
+ // Convert the URI into:
+ // content://USER@com.android.contacts/contacts_corp/ID/{photo,display_photo}
+ final Uri corpUri = maybeAddUserId(
+ ContentUris.appendId(Contacts.CONTENT_URI.buildUpon(), contactId)
+ .appendPath(displayPhoto ?
+ Contacts.Photo.DISPLAY_PHOTO : Contacts.Photo.CONTENT_DIRECTORY)
+ .build(), corpUserId);
+
+ // TODO Make sure it doesn't leak any FDs.
+ return getContext().getContentResolver().openAssetFileDescriptor(corpUri, mode);
+ }
+
private AssetFileDescriptor openPhotoAssetFile(
SQLiteDatabase db, Uri uri, String mode, String selection, String[] selectionArgs)
throws FileNotFoundException {
@@ -8079,6 +8297,7 @@
case PHONES_ID:
return Phone.CONTENT_ITEM_TYPE;
case PHONE_LOOKUP:
+ case PHONE_LOOKUP_ENTERPRISE:
return PhoneLookup.CONTENT_TYPE;
case EMAILS:
return Email.CONTENT_TYPE;
@@ -8157,6 +8376,7 @@
return sDataProjectionMap.getColumnNames();
case PHONE_LOOKUP:
+ case PHONE_LOOKUP_ENTERPRISE:
return sPhoneLookupProjectionMap.getColumnNames();
case AGGREGATION_EXCEPTIONS:
diff --git a/src/com/android/providers/contacts/database/MoreDatabaseUtils.java b/src/com/android/providers/contacts/database/MoreDatabaseUtils.java
index fa186bb..3dadb3f 100644
--- a/src/com/android/providers/contacts/database/MoreDatabaseUtils.java
+++ b/src/com/android/providers/contacts/database/MoreDatabaseUtils.java
@@ -18,6 +18,9 @@
import com.android.providers.contacts.util.NeededForTesting;
+import android.database.Cursor;
+import android.util.Log;
+
/**
* Static methods for database operations.
*/
@@ -76,4 +79,33 @@
}
return sb.toString();
}
+
+ /** Debug utility that dumps a cursor on logcat. */
+ public static final void dumpCursor(String logTag, String name, Cursor c) {
+ Log.d(logTag, "Dumping cursor " + name + " containing " + c.getCount() + " rows");
+
+ // Dump the column names.
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < c.getColumnCount(); i++) {
+ if (sb.length() > 0) sb.append(" ");
+ sb.append(c.getColumnName(i));
+ }
+ Log.d(logTag, sb.toString());
+
+ // Dump the values.
+ c.moveToPosition(-1);
+ while (c.moveToNext()) {
+ sb.setLength(0);
+ sb.append("row#");
+ sb.append(c.getPosition());
+
+ for (int i = 0; i < c.getColumnCount(); i++) {
+ sb.append(" ");
+
+ String s = c.getString(i);
+ sb.append(s == null ? "{null}" : s.replaceAll("\\s", "{space}"));
+ }
+ Log.d(logTag, sb.toString());
+ }
+ }
}
diff --git a/src/com/android/providers/contacts/UserUtils.java b/src/com/android/providers/contacts/util/UserUtils.java
similarity index 81%
rename from src/com/android/providers/contacts/UserUtils.java
rename to src/com/android/providers/contacts/util/UserUtils.java
index b42ab8d..9576727 100644
--- a/src/com/android/providers/contacts/UserUtils.java
+++ b/src/com/android/providers/contacts/util/UserUtils.java
@@ -13,7 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License
*/
-package com.android.providers.contacts;
+package com.android.providers.contacts.util;
+
+import com.android.providers.contacts.ContactsProvider2;
import android.content.Context;
import android.content.pm.UserInfo;
@@ -37,17 +39,19 @@
}
/**
- * @return the user ID of the enterprise user that is linked to the current user, if any.
- * If there's no such user, returns -1.
+ * @return the user ID of the corp user that is linked to the current user, if any.
+ * If there's no such user or cross-user contacts access is disallowed by policy, returns -1.
*
* STOPSHIP: Have amith look at it.
*/
- public static int getEnterpriseUserId(Context context) {
+ public static int getCorpUserId(Context context) {
final UserManager um = getUserManager(context);
final int currentUser = um.getUserHandle();
+ // STOPSHIP Check the policy and make sure cross-user contacts lookup is allowed.
+
if (VERBOSE_LOGGING) {
- Log.v(TAG, "getEnterpriseUserId: current=" + currentUser);
+ Log.v(TAG, "getCorpUserId: current=" + currentUser);
}
// TODO: Skip if the current is not the primary user?