summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml24
-rw-r--r--res/values/strings.xml13
-rw-r--r--src/com/android/providers/contacts/ProjectionMap.java2
-rw-r--r--src/com/android/providers/contacts/VoicemailContentProvider.java576
-rw-r--r--src/com/android/providers/contacts/VoicemailUriType.java40
-rw-r--r--src/com/android/providers/contacts/util/CloseUtils.java33
-rw-r--r--src/com/android/providers/contacts/util/DbQueryUtils.java55
-rw-r--r--src/com/android/providers/contacts/util/TypedUriMatcher.java27
-rw-r--r--src/com/android/providers/contacts/util/TypedUriMatcherImpl.java60
-rw-r--r--src/com/android/providers/contacts/util/UriType.java27
-rw-r--r--tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java48
-rw-r--r--tests/src/com/android/providers/contacts/util/TypedUriMatcherImplTest.java97
12 files changed, 997 insertions, 5 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d36dd0c..e9eacc7 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -2,6 +2,24 @@
package="com.android.providers.contacts"
android:sharedUserId="android.uid.shared">
+ <!-- TODO: These permissions should be moved to framework/base once voicemail
+ API is approved. -->
+ <permission
+ android:name="com.android.voicemail.permission.READ_WRITE_OWN_VOICEMAIL"
+ android:label="@string/read_write_own_voicemail_label"
+ android:description="@string/read_write_own_voicemail_description"
+ android:permissionGroup="android.permission-group.PERSONAL_INFO"
+ android:protectionLevel="dangerous"
+ />
+
+ <permission
+ android:name="com.android.voicemail.permission.READ_WRITE_ALL_VOICEMAIL"
+ android:label="@string/read_write_all_voicemail_label"
+ android:description="@string/read_write_all_voicemail_description"
+ android:permissionGroup="android.permission-group.PERSONAL_INFO"
+ android:protectionLevel="dangerous"
+ />
+
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
@@ -44,6 +62,12 @@
android:writePermission="android.permission.WRITE_CONTACTS">
</provider>
+ <provider android:name="VoicemailContentProvider"
+ android:authorities="com.android.voicemail"
+ android:syncable="false" android:multiprocess="false"
+ android:permission="com.android.voicemail.permission.READ_WRITE_OWN_VOICEMAIL">
+ </provider>
+
<!-- TODO: create permissions for social data -->
<provider android:name="SocialProvider"
android:authorities="com.android.social"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0e39d61..78b3620 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -4,9 +4,9 @@
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.
@@ -22,7 +22,7 @@
<!-- What to show in messaging that refers to this provider, e.g. AccountSyncSettings -->
<string name="provider_label">Contacts</string>
-
+
<!-- Ticker for the notification shown when updating contacts fails because of memory shortage -->
<string name="upgrade_out_of_memory_notification_ticker">Contact upgrade needs more memory</string>
@@ -31,11 +31,16 @@
<!-- 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>
+ <string name="read_write_own_voicemail_label">read and write own voicemails</string>
+ <string name="read_write_own_voicemail_description">The application is allowed to store and access only voicemails it owns on the device.</string>
+ <string name="read_write_all_voicemail_label">read and write all voicemails</string>
+ <string name="read_write_all_voicemail_description">The application is allowed to store and access all voicemails on the device.</string>
+
</resources>
diff --git a/src/com/android/providers/contacts/ProjectionMap.java b/src/com/android/providers/contacts/ProjectionMap.java
index 56198b8..f4c76d6 100644
--- a/src/com/android/providers/contacts/ProjectionMap.java
+++ b/src/com/android/providers/contacts/ProjectionMap.java
@@ -56,7 +56,7 @@ public class ProjectionMap extends HashMap<String, String> {
}
- public String[] mColumns;
+ private String[] mColumns;
public static Builder builder() {
return new Builder();
diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java
new file mode 100644
index 0000000..d559758
--- /dev/null
+++ b/src/com/android/providers/contacts/VoicemailContentProvider.java
@@ -0,0 +1,576 @@
+/*
+ * 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.contacts;
+
+import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
+import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.provider.CallLog.Calls;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.util.Log;
+
+import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
+import com.android.providers.contacts.ContactsDatabaseHelper.Views;
+import com.android.providers.contacts.util.CloseUtils;
+import com.android.providers.contacts.util.TypedUriMatcherImpl;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+// TODO: Restrict access to only voicemail columns (i.e no access to call_log
+// specific fields)
+// TODO: Port unit tests from perforce.
+/**
+ * An implementation of the Voicemail content provider.
+ */
+public class VoicemailContentProvider extends ContentProvider {
+ private static final String TAG = "VoicemailContentProvider";
+
+ /** The private directory in which to store the data associated with the voicemail. */
+ private static final String DATA_DIRECTORY = "voicemail-data";
+
+ private static final String[] MIME_TYPE_ONLY_PROJECTION = new String[] { Voicemails.MIME_TYPE };
+ private static final String[] FILENAME_ONLY_PROJECTION = new String[] { Voicemails._DATA };
+ private static final String VOICEMAILS_TABLE_NAME = Tables.CALLS;
+
+ // Voicemail projection map
+ private static final ProjectionMap sVoicemailProjectionMap = new ProjectionMap.Builder()
+ .add(Voicemails._ID)
+ .add(Voicemails.NUMBER)
+ .add(Voicemails.DATE)
+ .add(Voicemails.DURATION)
+ .add(Voicemails.NEW)
+ .add(Voicemails.STATE)
+ .add(Voicemails.SOURCE_DATA)
+ .add(Voicemails.SOURCE_PACKAGE)
+ .add(Voicemails.HAS_CONTENT)
+ .add(Voicemails.MIME_TYPE)
+ .add(Voicemails._DATA)
+ .build();
+ private ContentResolver mContentResolver;
+ private ContactsDatabaseHelper mDbHelper;
+
+ @Override
+ public boolean onCreate() {
+ Context context = context();
+
+ mContentResolver = context.getContentResolver();
+ mDbHelper = ContactsDatabaseHelper.getInstance(context);
+
+ return true;
+ }
+
+ /*package for testing*/ Context context() {
+ return getContext();
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ UriData uriData = null;
+ try {
+ uriData = createUriData(uri);
+ } catch (IllegalArgumentException ignored) {
+ // Special case: for illegal URIs, we return null rather than thrown an exception.
+ return null;
+ }
+ // TODO: DB lookup for the mime type may cause strict mode exception for the callers of
+ // getType(). See if this could be avoided.
+ if (uriData.hasId()) {
+ // An individual voicemail - so lookup the MIME type in the db.
+ return lookupMimeType(uriData);
+ }
+ // Not an individual voicemail - must be a directory listing type.
+ return VoicemailContract.DIR_TYPE;
+ }
+
+ /** Query the db for the MIME type of the given URI, called only from {@link #getType(Uri)}. */
+ private String lookupMimeType(UriData uriData) {
+ Cursor cursor = null;
+ try {
+ // Use queryInternal, bypassing provider permission check. This is needed because
+ // getType() can be called from any application context (even without voicemail
+ // permissions) to know the MIME type of the URI. There is no security issue here as we
+ // do not expose any sensitive data through this interface.
+ cursor = queryInternal(uriData, MIME_TYPE_ONLY_PROJECTION, null, null, null);
+ if (cursor.moveToFirst()) {
+ return cursor.getString(cursor.getColumnIndex(Voicemails.MIME_TYPE));
+ }
+ } finally {
+ CloseUtils.closeQuietly(cursor);
+ }
+ return null;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ checkHasOwnPermission();
+ UriData uriData = createUriData(uri);
+ checkPackagePermission(uriData);
+ return queryInternal(uriData, projection,
+ concatenateClauses(selection, getPackageRestrictionClause()), selectionArgs,
+ sortOrder);
+ }
+
+ /**
+ * Internal version of query(), that does not apply any provider restriction and lets the query
+ * flow through without such checks.
+ * <p>
+ * This is useful for internal queries when we do not worry about access permissions.
+ */
+ private Cursor queryInternal(UriData uriData, String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(Tables.CALLS);
+ qb.setProjectionMap(sVoicemailProjectionMap);
+ qb.setStrict(true);
+
+ String combinedClause = concatenateClauses(selection, getWhereClause(uriData),
+ getCallTypeClause());
+ SQLiteDatabase db = mDbHelper.getReadableDatabase();
+ Cursor c = qb.query(db, projection, combinedClause, selectionArgs, null, null, sortOrder);
+ if (c != null) {
+ c.setNotificationUri(mContentResolver, VoicemailContract.CONTENT_URI);
+ }
+ return c;
+ }
+
+ private String getWhereClause(UriData uriData) {
+ return concatenateClauses(
+ (uriData.hasId() ?
+ getEqualityClause(Voicemails._ID, uriData.getId())
+ : null),
+ (uriData.hasSourcePackage() ?
+ getEqualityClause(Voicemails.SOURCE_PACKAGE, uriData.getSourcePackage())
+ : null));
+ }
+
+ @Override
+ public int bulkInsert(Uri uri, ContentValues[] valuesArray) {
+ checkHasOwnPermission();
+ // TODO: There is scope to optimize this method further. At the least we can avoid doing the
+ // extra work related to the calling provider and checking permissions.
+ UriData uriData = createUriData(uri);
+ int numInserted = 0;
+ for (ContentValues values : valuesArray) {
+ if (insertInternal(uriData, values, false) != null) {
+ numInserted++;
+ }
+ }
+ if (numInserted > 0) {
+ notifyChange(uri, Intent.ACTION_PROVIDER_CHANGED);
+ }
+ return numInserted;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ checkHasOwnPermission();
+ return insertInternal(createUriData(uri), values, true);
+ }
+
+ private Uri insertInternal(UriData uriData, ContentValues values,
+ boolean sendProviderChangedNotification) {
+ checkInsertSupported(uriData);
+ checkAndAddSourcePackageIntoValues(uriData, values);
+
+ // "_data" column is used by base ContentProvider's openFileHelper() to determine filename
+ // when Input/Output stream is requested to be opened.
+ values.put(Voicemails._DATA, generateDataFile());
+
+ // call type is always voicemail.
+ values.put(Calls.TYPE, Calls.VOICEMAIL_TYPE);
+
+ SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ long rowId = db.insert(VOICEMAILS_TABLE_NAME, null, values);
+ if (rowId > 0) {
+ Uri newUri = ContentUris.withAppendedId(
+ Uri.withAppendedPath(VoicemailContract.CONTENT_URI_SOURCE,
+ values.getAsString(Voicemails.SOURCE_PACKAGE)), rowId);
+
+ if (sendProviderChangedNotification) {
+ notifyChange(newUri, VoicemailContract.ACTION_NEW_VOICEMAIL,
+ Intent.ACTION_PROVIDER_CHANGED);
+ } else {
+ notifyChange(newUri, VoicemailContract.ACTION_NEW_VOICEMAIL);
+ }
+ // Populate the 'voicemail_uri' field to be used by the call_log provider.
+ updateVoicemailUri(newUri);
+ return newUri;
+ }
+ return null;
+ }
+
+ private void updateVoicemailUri(Uri newUri) {
+ ContentValues values = new ContentValues();
+ values.put(Calls.VOICEMAIL_URI, newUri.toString());
+ update(newUri, values, null, null);
+ }
+
+ private void checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values) {
+ // If content values don't contain the provider, calculate the right provider to use.
+ if (!values.containsKey(Voicemails.SOURCE_PACKAGE)) {
+ String provider = uriData.hasSourcePackage() ?
+ uriData.getSourcePackage() : getCallingPackage();
+ values.put(Voicemails.SOURCE_PACKAGE, provider);
+ }
+ // If you put a provider in the URI and in the values, they must match.
+ if (uriData.hasSourcePackage() && values.containsKey(Voicemails.SOURCE_PACKAGE)) {
+ if (!uriData.getSourcePackage().equals(values.get(Voicemails.SOURCE_PACKAGE))) {
+ throw new IllegalArgumentException(
+ "Provider in URI was " + uriData.getSourcePackage() +
+ " but doesn't match provider in ContentValues which was "
+ + values.get(Voicemails.SOURCE_PACKAGE));
+ }
+ }
+ // You must have access to the provider given in values.
+ if (!hasFullPermission(getCallingPackage())) {
+ checkPackagesMatch(getCallingPackage(), values.getAsString(Voicemails.SOURCE_PACKAGE),
+ uriData.getUri());
+ }
+ }
+
+ /**
+ * Checks that the callingProvider is same as voicemailProvider. Throws {@link
+ * SecurityException} if they don't match.
+ */
+ private final void checkPackagesMatch(String callingProvider, String voicemailProvider,
+ Uri uri) {
+ if (!voicemailProvider.equals(callingProvider)) {
+ String errorMsg = String.format("Permission denied for URI: %s\n. " +
+ "Provider %s cannot perform this operation for %s. Requires %s permission.",
+ uri, callingProvider, voicemailProvider,
+ Manifest.permission.READ_WRITE_ALL_VOICEMAIL);
+ throw new SecurityException(errorMsg);
+ }
+ }
+
+ private void checkInsertSupported(UriData uriData) {
+ if (uriData.hasId()) {
+ throw new UnsupportedOperationException(String.format(
+ "Cannot insert URI: %s. Inserted URIs should not contain an id.",
+ uriData.getUri()));
+ }
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ checkHasOwnPermission();
+ UriData uriData = createUriData(uri);
+ checkUpdateSupported(uriData);
+ checkPackagePermission(uriData);
+ final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ // TODO: This implementation does not allow bulk update because it only accepts
+ // URI that include message Id. I think we do want to support bulk update.
+ String combinedClause = concatenateClauses(selection, getPackageRestrictionClause(),
+ getWhereClause(uriData), getCallTypeClause());
+ int count = db.update(VOICEMAILS_TABLE_NAME, values, combinedClause, selectionArgs);
+ if (count > 0) {
+ notifyChange(uri, Intent.ACTION_PROVIDER_CHANGED);
+ }
+ return count;
+ }
+
+ private void checkUpdateSupported(UriData uriData) {
+ if (!uriData.hasId()) {
+ throw new UnsupportedOperationException(String.format(
+ "Cannot update URI: %s. Bulk update not supported", uriData.getUri()));
+ }
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ checkHasOwnPermission();
+ UriData uriData = createUriData(uri);
+ checkPackagePermission(uriData);
+ final SQLiteDatabase db = mDbHelper.getWritableDatabase();
+ String combinedClause = concatenateClauses(selection, getPackageRestrictionClause(),
+ getWhereClause(uriData), getCallTypeClause());
+
+ // Delete all the files associated with this query. Once we've deleted the rows, there will
+ // be no way left to get hold of the files.
+ Cursor cursor = null;
+ try {
+ cursor = queryInternal(uriData, FILENAME_ONLY_PROJECTION, selection, selectionArgs,
+ null);
+ while (cursor.moveToNext()) {
+ File file = new File(cursor.getString(0));
+ if (file.exists()) {
+ boolean success = file.delete();
+ if (!success) {
+ Log.e(TAG, "Failed to delete file: " + file.getAbsolutePath());
+ }
+ }
+ }
+ } finally {
+ CloseUtils.closeQuietly(cursor);
+ }
+
+ // Now delete the rows themselves.
+ int count = db.delete(VOICEMAILS_TABLE_NAME, combinedClause, selectionArgs);
+ if (count > 0) {
+ notifyChange(uri, Intent.ACTION_PROVIDER_CHANGED);
+ }
+ return count;
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+ checkHasOwnPermission();
+ UriData uriData = createUriData(uri);
+ checkPackagePermission(uriData);
+
+ // This relies on "_data" column to be populated with the file path.
+ ParcelFileDescriptor openFileHelper = openFileHelper(uri, mode);
+
+ // If the open succeeded, then update the file exists bit in the table.
+ if (mode.contains("w")) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Voicemails.HAS_CONTENT, 1);
+ update(uri, contentValues, null, null);
+ }
+
+ return openFileHelper;
+ }
+
+ /**
+ * Notifies the content resolver and fires required broadcast intent(s) to notify about the
+ * change.
+ *
+ * @param notificationUri The URI that got impacted due to the change. This is the URI that is
+ * included in content resolver and broadcast intent notification.
+ * @param intentActions List of intent actions that needs to be fired. A separate intent is
+ * fired for each intent action.
+ */
+ private void notifyChange(Uri notificationUri, String... intentActions) {
+ // Notify the observers.
+ mContentResolver.notifyChange(notificationUri, null, true);
+ // Fire notification intents.
+ for (String intentAction : intentActions) {
+ // TODO: We can possibly be more intelligent here and send targeted intents based on
+ // what voicemail permission the package has. If possible, here is what we would like to
+ // do for a given broadcast intent -
+ // 1) Send it to all packages that have READ_WRITE_ALL_VOICEMAIL permission.
+ // 2) Send it to only the owner package that has just READ_WRITE_OWN_VOICEMAIL, if not
+ // already sent in (1).
+ Intent intent = new Intent(intentAction, notificationUri);
+ intent.putExtra(VoicemailContract.EXTRA_CHANGED_BY, getCallingPackage());
+ context().sendOrderedBroadcast(intent, Manifest.permission.READ_WRITE_OWN_VOICEMAIL);
+ }
+ }
+
+ /** Generates a random file for storing audio data. */
+ private String generateDataFile() {
+ try {
+ File dataDirectory = context().getDir(DATA_DIRECTORY, Context.MODE_PRIVATE);
+ File voicemailFile = File.createTempFile("voicemail", "", dataDirectory);
+ return voicemailFile.getAbsolutePath();
+ } catch (IOException e) {
+ // If we are unable to create a temporary file, something went horribly wrong.
+ throw new RuntimeException("unable to create temp file", e);
+ }
+ }
+
+ /**
+ * Decorates a URI by providing methods to get various properties from the URI.
+ */
+ private static class UriData {
+ private final Uri mUri;
+ private final String mId;
+ private final String mSourcePackage;
+
+ public UriData(Uri uri, String id, String sourcePackage) {
+ mUri = uri;
+ mId = id;
+ mSourcePackage = sourcePackage;
+ }
+
+ /** Gets the original URI to which this {@link UriData} corresponds. */
+ public final Uri getUri() {
+ return mUri;
+ }
+
+ /** Tells us if our URI has an individual voicemail id. */
+ public final boolean hasId() {
+ return mId != null;
+ }
+
+ /** Gets the ID for the voicemail. */
+ public final String getId() {
+ return mId;
+ }
+
+ /** Tells us if our URI has a source package string. */
+ public final boolean hasSourcePackage() {
+ return mSourcePackage != null;
+ }
+
+ /** Gets the source package. */
+ public final String getSourcePackage() {
+ return mSourcePackage;
+ }
+ }
+
+ /**
+ * Checks that either the caller has READ_WRITE_ALL_VOICEMAIL permission, or has the
+ * READ_WRITE_OWN_VOICEMAIL permission and is using a URI that matches
+ * /voicemail/source/[source-package] where [source-package] is the same as the calling
+ * package.
+ *
+ * @throws SecurityException if the check fails.
+ */
+ private void checkPackagePermission(UriData uriData) {
+ if (!hasFullPermission(getCallingPackage())) {
+ if (!uriData.hasSourcePackage()) {
+ // You cannot have a match if this is not a provider uri.
+ throw new SecurityException(String.format(
+ "Provider %s does not have %s permission." +
+ "\nPlease use /voicemail/provider/ query path instead.\nURI: %s",
+ getCallingPackage(), Manifest.permission.READ_WRITE_ALL_VOICEMAIL,
+ uriData.getUri()));
+ }
+ checkPackagesMatch(getCallingPackage(), uriData.getSourcePackage(), uriData.getUri());
+ }
+ }
+
+ private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() {
+ return new TypedUriMatcherImpl<VoicemailUriType>(
+ VoicemailContract.AUTHORITY, VoicemailUriType.values());
+ }
+
+ /** Get a {@link UriData} corresponding to a given uri. */
+ private UriData createUriData(Uri uri) {
+ List<String> segments = uri.getPathSegments();
+ switch (createUriMatcher().match(uri)) {
+ case VOICEMAILS:
+ return new UriData(uri, null, null);
+ case VOICEMAILS_ID:
+ return new UriData(uri, segments.get(1), null);
+ case VOICEMAILS_SOURCE:
+ return new UriData(uri, null, segments.get(2));
+ case VOICEMAILS_SOURCE_ID:
+ return new UriData(uri, segments.get(3), segments.get(2));
+ case NO_MATCH:
+ throw new IllegalArgumentException("Invalid URI: " + uri);
+ default:
+ throw new IllegalStateException("Impossible, all cases are covered");
+ }
+ }
+
+ /**
+ * Gets the name of the calling package.
+ * <p>
+ * It's possible (though unlikely) for there to be more than one calling package (requires that
+ * your manifest say you want to share process ids) in which case we will return an arbitrary
+ * package name. It's also possible (though very unlikely) for us to be unable to work out what
+ * your calling package is, in which case we will return null.
+ */
+ /* package for test */String getCallingPackage() {
+ int caller = Binder.getCallingUid();
+ if (caller == 0) {
+ return null;
+ }
+ String[] callerPackages = context().getPackageManager().getPackagesForUid(caller);
+ if (callerPackages == null || callerPackages.length == 0) {
+ return null;
+ }
+ if (callerPackages.length == 1) {
+ return callerPackages[0];
+ }
+ // If we have more than one caller package, which is very unlikely, let's return the one
+ // with the highest permissions. If more than one has the same permission, we don't care
+ // which one we return.
+ String bestSoFar = callerPackages[0];
+ for (String callerPackage : callerPackages) {
+ if (hasFullPermission(callerPackage)) {
+ // Full always wins, we can return early.
+ return callerPackage;
+ }
+ if (hasOwnPermission(callerPackage)) {
+ bestSoFar = callerPackage;
+ }
+ }
+ return bestSoFar;
+ }
+
+ /**
+ * This check is made once only at every entry-point into this class from outside.
+ *
+ * @throws SecurityException if the caller does not have the voicemail source permission.
+ */
+ private void checkHasOwnPermission() {
+ if (!hasOwnPermission(getCallingPackage())) {
+ throw new SecurityException("The caller must have permission: " +
+ Manifest.permission.READ_WRITE_OWN_VOICEMAIL);
+ }
+ }
+
+ /** Tells us if the given package has the source permission. */
+ private boolean hasOwnPermission(String packageName) {
+ return hasPermission(packageName, Manifest.permission.READ_WRITE_OWN_VOICEMAIL);
+ }
+
+ /**
+ * Tells us if the given package has the full permission and the source
+ * permission.
+ */
+ private boolean hasFullPermission(String packageName) {
+ return hasOwnPermission(packageName) &&
+ hasPermission(packageName, Manifest.permission.READ_WRITE_ALL_VOICEMAIL);
+ }
+
+ /** Tells us if the given package has the given permission. */
+ /* package for test */boolean hasPermission(String packageName, String permission) {
+ return context().getPackageManager().checkPermission(permission, packageName)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * Creates a clause to restrict the selection to the calling provider or null if the caller has
+ * access to all data.
+ */
+ private String getPackageRestrictionClause() {
+ if (hasFullPermission(getCallingPackage())) {
+ return null;
+ }
+ return getEqualityClause(Voicemails.SOURCE_PACKAGE, getCallingPackage());
+ }
+
+
+ /** Creates a clause to restrict the selection to only voicemail call type.*/
+ private String getCallTypeClause() {
+ return getEqualityClause(Calls.TYPE, String.valueOf(Calls.VOICEMAIL_TYPE));
+ }
+
+}
diff --git a/src/com/android/providers/contacts/VoicemailUriType.java b/src/com/android/providers/contacts/VoicemailUriType.java
new file mode 100644
index 0000000..9e728a2
--- /dev/null
+++ b/src/com/android/providers/contacts/VoicemailUriType.java
@@ -0,0 +1,40 @@
+/*
+ * 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.contacts;
+
+import com.android.providers.contacts.util.UriType;
+
+/**
+ * Defines the different URIs handled by the voicemail content provider.
+ */
+enum VoicemailUriType implements UriType {
+ NO_MATCH(null),
+ VOICEMAILS("voicemail"),
+ VOICEMAILS_ID("voicemail/#"),
+ VOICEMAILS_SOURCE("voicemail/source/*"),
+ VOICEMAILS_SOURCE_ID("voicemail/source/*/#");
+
+ private final String path;
+
+ private VoicemailUriType(String path) {
+ this.path = path;
+ }
+
+ @Override
+ public String path() {
+ return path;
+ }
+}
diff --git a/src/com/android/providers/contacts/util/CloseUtils.java b/src/com/android/providers/contacts/util/CloseUtils.java
new file mode 100644
index 0000000..c5753a7
--- /dev/null
+++ b/src/com/android/providers/contacts/util/CloseUtils.java
@@ -0,0 +1,33 @@
+/*
+ * 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.contacts.util;
+
+import android.database.Cursor;
+
+/**
+ * Utility methods for closing database cursors.
+ */
+public class CloseUtils {
+ private CloseUtils() {
+ }
+
+ /** If the argument is non-null, close the cursor. */
+ public static void closeQuietly(Cursor cursor) {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+}
diff --git a/src/com/android/providers/contacts/util/DbQueryUtils.java b/src/com/android/providers/contacts/util/DbQueryUtils.java
new file mode 100644
index 0000000..6db077f
--- /dev/null
+++ b/src/com/android/providers/contacts/util/DbQueryUtils.java
@@ -0,0 +1,55 @@
+/*
+ * 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.contacts.util;
+
+import android.database.DatabaseUtils;
+import android.text.TextUtils;
+
+/**
+ * Static methods for helping us build database query selection strings.
+ */
+public class DbQueryUtils {
+ // Static class with helper methods, so private constructor.
+ private DbQueryUtils() {
+ }
+
+ /** Returns a WHERE clause asserting equality of a field to a value. */
+ public static String getEqualityClause(String field, String value) {
+ StringBuilder clause = new StringBuilder();
+ clause.append("(");
+ clause.append(field);
+ clause.append(" = ");
+ DatabaseUtils.appendEscapedSQLString(clause, value);
+ clause.append(")");
+ return clause.toString();
+ }
+
+ /** Concatenates any number of clauses using "AND". */
+ public static String concatenateClauses(String... clauses) {
+ StringBuilder builder = new StringBuilder();
+ for (String clause : clauses) {
+ if (!TextUtils.isEmpty(clause)) {
+ if (builder.length() > 0) {
+ builder.append(" AND ");
+ }
+ builder.append("(");
+ builder.append(clause);
+ builder.append(")");
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/src/com/android/providers/contacts/util/TypedUriMatcher.java b/src/com/android/providers/contacts/util/TypedUriMatcher.java
new file mode 100644
index 0000000..e9a5f4b
--- /dev/null
+++ b/src/com/android/providers/contacts/util/TypedUriMatcher.java
@@ -0,0 +1,27 @@
+/*
+ * 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.contacts.util;
+
+import android.net.Uri;
+
+/**
+ * Maps a {@link Uri} into an enum type.
+ *
+ * @param <T> the type of the URI
+ */
+public interface TypedUriMatcher<T extends UriType> {
+ public T match(Uri uri);
+}
diff --git a/src/com/android/providers/contacts/util/TypedUriMatcherImpl.java b/src/com/android/providers/contacts/util/TypedUriMatcherImpl.java
new file mode 100644
index 0000000..6378adf
--- /dev/null
+++ b/src/com/android/providers/contacts/util/TypedUriMatcherImpl.java
@@ -0,0 +1,60 @@
+/*
+ * 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.contacts.util;
+
+import android.content.UriMatcher;
+import android.net.Uri;
+
+/**
+ * Implementation of {@link TypedUriMatcher}.
+ *
+ * @param <T> the type of the URI
+ */
+public class TypedUriMatcherImpl<T extends UriType> implements TypedUriMatcher<T> {
+ private final String mAuthority;
+ private final T[] mValues;
+ private final T mNoMatchUriType;
+ private final UriMatcher mUriMatcher;
+
+ public TypedUriMatcherImpl(String authority, T[] values) {
+ mAuthority = authority;
+ mValues = values;
+ mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ T candidateNoMatchUriType = null;
+ for (T value : values) {
+ String path = value.path();
+ if (path != null) {
+ addUriType(path, value);
+ } else {
+ candidateNoMatchUriType = value;
+ }
+ }
+ this.mNoMatchUriType = candidateNoMatchUriType;
+ }
+
+ private void addUriType(String path, T value) {
+ mUriMatcher.addURI(mAuthority, path, value.ordinal());
+ }
+
+ @Override
+ public T match(Uri uri) {
+ int match = mUriMatcher.match(uri);
+ if (match == UriMatcher.NO_MATCH) {
+ return mNoMatchUriType;
+ }
+ return mValues[match];
+ }
+}
diff --git a/src/com/android/providers/contacts/util/UriType.java b/src/com/android/providers/contacts/util/UriType.java
new file mode 100644
index 0000000..e0e0b21
--- /dev/null
+++ b/src/com/android/providers/contacts/util/UriType.java
@@ -0,0 +1,27 @@
+/*
+ * 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.contacts.util;
+
+/**
+ * URI type used by {@link TypedUriMatcher}.
+ */
+public interface UriType {
+ /** Returns the path associated with this URI type. */
+ public String path();
+
+ /** Returns the ordinal associated with this URI type. */
+ public int ordinal();
+}
diff --git a/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java b/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java
new file mode 100644
index 0000000..35341fe
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/util/DBQueryUtilsTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.contacts.util;
+
+import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.providers.contacts.util.DbQueryUtils;
+
+/**
+ * Unit tests for the {@link DbQueryUtils} class.
+ * Run the test like this:
+ * <code>
+ * runtest -c com.android.providers.contacts.util.DBQueryUtilsTest contactsprov
+ * </code>
+ */
+@SmallTest
+public class DBQueryUtilsTest extends AndroidTestCase {
+ public void testGetEqualityClause() {
+ assertEquals("(foo = 'bar')", DbQueryUtils.getEqualityClause("foo", "bar"));
+ }
+
+ public void testConcatenateClauses() {
+ assertEquals("(first)", concatenateClauses("first"));
+ assertEquals("(first) AND (second)", concatenateClauses("first", "second"));
+ assertEquals("(second)", concatenateClauses("second", null));
+ assertEquals("(second)", concatenateClauses(null, "second"));
+ assertEquals("(second)", concatenateClauses(null, "second", null));
+ assertEquals("(a) AND (b) AND (c)", concatenateClauses(null, "a", "b", null, "c"));
+ assertEquals("(WHERE \"a\" = \"b\")", concatenateClauses(null, "WHERE \"a\" = \"b\""));
+ }
+}
diff --git a/tests/src/com/android/providers/contacts/util/TypedUriMatcherImplTest.java b/tests/src/com/android/providers/contacts/util/TypedUriMatcherImplTest.java
new file mode 100644
index 0000000..48bd608
--- /dev/null
+++ b/tests/src/com/android/providers/contacts/util/TypedUriMatcherImplTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.contacts.util;
+
+import android.net.Uri;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.providers.contacts.util.TypedUriMatcherImpl;
+import com.android.providers.contacts.util.UriType;
+
+/**
+ * Unit tests for {@link TypedUriMatcherImpl}.
+ * Run the test like this:
+ * <code>
+ * runtest -c com.android.providers.contacts.util.TypedUriMatcherImplTest contactsprov
+ * </code>
+ */
+@SmallTest
+public class TypedUriMatcherImplTest extends AndroidTestCase {
+ /** URI type used for testing. */
+ private static enum TestUriType implements UriType {
+ NO_MATCH(null),
+ SIMPLE_URI("build"),
+ URI_WITH_ID("build/#"),
+ URI_WITH_TWO_IDS("project/*/build/#");
+
+ private String path;
+
+ private TestUriType(String path) {
+ this.path = path;
+ }
+
+ @Override
+ public String path() {
+ return path;
+ }
+ }
+
+ private final static String AUTHORITY = "authority";
+ private final static String BASE_URI = "scheme://" + AUTHORITY + "/";
+
+ /** The object under test. */
+ TypedUriMatcherImpl<TestUriType> mTypedUriMatcherImpl;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mTypedUriMatcherImpl =
+ new TypedUriMatcherImpl<TestUriType>(AUTHORITY, TestUriType.values());
+ }
+
+ public void testMatch_NoMatch() {
+ // Incorrect authority.
+ assertUriTypeMatch(TestUriType.NO_MATCH, "scheme://authority1/build");
+ // Incorrect path.
+ assertUriTypeMatch(TestUriType.NO_MATCH, BASE_URI + "test");
+ }
+
+ public void testMatch_SimpleUri() {
+ assertUriTypeMatch(TestUriType.SIMPLE_URI, BASE_URI + "build");
+ }
+
+ public void testMatch_UriWithId() {
+ assertUriTypeMatch(TestUriType.URI_WITH_ID, BASE_URI + "build/2");
+ // Argument must be a number.
+ assertUriTypeMatch(TestUriType.NO_MATCH, BASE_URI + "build/a");
+ // Additional arguments not allowed.
+ assertUriTypeMatch(TestUriType.NO_MATCH, BASE_URI + "build/2/more");
+ }
+
+ public void testMatch_UriWithTwoIds() {
+ assertUriTypeMatch(TestUriType.URI_WITH_TWO_IDS, BASE_URI + "project/vm/build/3");
+ // Missing argument.
+ assertUriTypeMatch(TestUriType.NO_MATCH, BASE_URI + "project/vm/build/");
+ // Argument cannot contain / itself
+ assertUriTypeMatch(TestUriType.NO_MATCH, BASE_URI + "project/vm/x/build/3");
+ }
+
+ private void assertUriTypeMatch(UriType expectedType, String uri) {
+ assertEquals(expectedType, mTypedUriMatcherImpl.match(Uri.parse(uri)));
+ }
+}