diff options
Diffstat (limited to 'src')
5 files changed, 297 insertions, 65 deletions
diff --git a/src/com/android/providers/contacts/CallLogProvider.java b/src/com/android/providers/contacts/CallLogProvider.java index e7a6351..b07a56d 100644 --- a/src/com/android/providers/contacts/CallLogProvider.java +++ b/src/com/android/providers/contacts/CallLogProvider.java @@ -17,9 +17,12 @@ package com.android.providers.contacts; import static com.android.providers.contacts.util.DbQueryUtils.checkForSupportedColumns; +import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; +import static com.android.providers.contacts.util.DbQueryUtils.getInequalityClause; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; import com.android.providers.contacts.util.DbQueryUtils; +import com.android.providers.contacts.util.SelectionBuilder; import android.content.ContentProvider; import android.content.ContentUris; @@ -33,6 +36,8 @@ import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.provider.CallLog; import android.provider.CallLog.Calls; +import android.provider.ContactsContract; +import android.util.Log; import java.util.HashMap; import java.util.Set; @@ -42,6 +47,31 @@ import java.util.Set; */ public class CallLogProvider extends ContentProvider { + /** + * An optional URI parameter for call_log operations which instructs the + * provider to allow the operation to be applied to voicemail records as well. + * <p> TYPE: Boolean + * + * <p> Using this parameter with a value true will result in a security + * error if the calling application does not have appropriate permissions + * to access voicemails. + */ + // TODO: Move this to ContactsContract.Calls once we are happy with the changes. + public static final String ALLOW_VOICEMAILS_PARAM_KEY = "allow_voicemails"; + + /** + * Content uri with {@link #ALLOW_VOICEMAILS_PARAM_KEY} set. This can directly + * be used to access call log entries that includes voicemail records. + */ + // TODO: Move this to ContactsContract.Calls once we are happy with the changes. + public static Uri CONTENT_URI_WITH_VOICEMAIL = Calls.CONTENT_URI.buildUpon() + .appendQueryParameter(CallLogProvider.ALLOW_VOICEMAILS_PARAM_KEY, "true") + .build(); + + /** Selection clause to use to exclude voicemail records. */ + private static final String EXCLUDE_VOICEMAIL_SELECTION = getInequalityClause( + Calls.TYPE, Integer.toString(Calls.VOICEMAIL_TYPE)); + private static final int CALLS = 1; private static final int CALLS_ID = 2; @@ -77,6 +107,7 @@ public class CallLogProvider extends ContentProvider { private DatabaseUtils.InsertHelper mCallsInserter; private boolean mUseStrictPhoneNumberComparation; private CountryMonitor mCountryMonitor; + private VoicemailPermissions mVoicemailPermissions; @Override public boolean onCreate() { @@ -86,6 +117,7 @@ public class CallLogProvider extends ContentProvider { context.getResources().getBoolean( com.android.internal.R.bool.config_use_strict_phone_number_comparation); mCountryMonitor = new CountryMonitor(context); + mVoicemailPermissions = new VoicemailPermissions(context); return true; } @@ -102,18 +134,17 @@ public class CallLogProvider extends ContentProvider { qb.setProjectionMap(sCallsProjectionMap); qb.setStrict(true); + SelectionBuilder selectionBuilder = new SelectionBuilder(selection); + checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder); + int match = sURIMatcher.match(uri); switch (match) { case CALLS: break; case CALLS_ID: { - try { - Long id = Long.valueOf(uri.getPathSegments().get(1)); - qb.appendWhere(Calls._ID + "=" + id.toString()); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Invalid call id in uri: " + uri, e); - } + selectionBuilder.addClause(getEqualityClause(Calls._ID, + parseCallIdFromUri(uri))); break; } @@ -130,7 +161,8 @@ public class CallLogProvider extends ContentProvider { } final SQLiteDatabase db = mDbHelper.getReadableDatabase(); - Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, null); + Cursor c = qb.query(db, projection, selectionBuilder.build(), selectionArgs, null, null, + sortOrder, null); if (c != null) { c.setNotificationUri(getContext().getContentResolver(), CallLog.CONTENT_URI); } @@ -155,6 +187,13 @@ public class CallLogProvider extends ContentProvider { @Override public Uri insert(Uri uri, ContentValues values) { checkForSupportedColumns(sCallsProjectionMap, values); + // Inserting a voicemail record through call_log requires the voicemail + // permission and also requires the additional voicemail param set. + if (hasVoicemailValue(values)) { + checkIsAllowVoicemailRequest(uri); + mVoicemailPermissions.checkCallerHasFullAccess(); + } + // Inserted the current country code, so we know the country // the number belongs to. values.put(Calls.COUNTRY_ISO, getCurrentCountryIso()); @@ -172,26 +211,32 @@ public class CallLogProvider extends ContentProvider { } @Override - public int update(Uri url, ContentValues values, String selection, String[] selectionArgs) { + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { checkForSupportedColumns(sCallsProjectionMap, values); + // Request that involves changing record type to voicemail requires the + // voicemail param set in the uri. + if (hasVoicemailValue(values)) { + checkIsAllowVoicemailRequest(uri); + } + + SelectionBuilder selectionBuilder = new SelectionBuilder(selection); + checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder); + final SQLiteDatabase db = mDbHelper.getWritableDatabase(); - String where; - final int matchedUriId = sURIMatcher.match(url); + final int matchedUriId = sURIMatcher.match(uri); switch (matchedUriId) { case CALLS: - where = selection; break; case CALLS_ID: - where = DatabaseUtils.concatenateWhere(selection, Calls._ID + "=" - + url.getPathSegments().get(1)); + selectionBuilder.addClause(getEqualityClause(Calls._ID, parseCallIdFromUri(uri))); break; default: - throw new UnsupportedOperationException("Cannot update URL: " + url); + throw new UnsupportedOperationException("Cannot update URL: " + uri); } - int count = db.update(Tables.CALLS, values, where, selectionArgs); + int count = db.update(Tables.CALLS, values, selectionBuilder.build(), selectionArgs); if (count > 0) { notifyChange(); } @@ -200,12 +245,14 @@ public class CallLogProvider extends ContentProvider { @Override public int delete(Uri uri, String selection, String[] selectionArgs) { - final SQLiteDatabase db = mDbHelper.getWritableDatabase(); + SelectionBuilder selectionBuilder = new SelectionBuilder(selection); + checkVoicemailPermissionAndAddRestriction(uri, selectionBuilder); + final SQLiteDatabase db = mDbHelper.getWritableDatabase(); final int matchedUriId = sURIMatcher.match(uri); switch (matchedUriId) { case CALLS: - int count = db.delete(Tables.CALLS, selection, selectionArgs); + int count = db.delete(Tables.CALLS, selectionBuilder.build(), selectionArgs); if (count > 0) { notifyChange(); } @@ -224,4 +271,60 @@ public class CallLogProvider extends ContentProvider { protected String getCurrentCountryIso() { return mCountryMonitor.getCountryIso(); } + + private boolean hasVoicemailValue(ContentValues values) { + return values.containsKey(Calls.TYPE) && + values.getAsInteger(Calls.TYPE).equals(Calls.VOICEMAIL_TYPE); + } + + /** + * Checks if the supplied uri requests to include voicemails and take appropriate + * action. + * <p> If voicemail is requested, then check for voicemail permissions. Otherwise + * modify the selection to restrict to non-voicemail entries only. + */ + private void checkVoicemailPermissionAndAddRestriction(Uri uri, + SelectionBuilder selectionBuilder) { + if (isAllowVoicemailRequest(uri)) { + mVoicemailPermissions.checkCallerHasFullAccess(); + } else { + selectionBuilder.addClause(EXCLUDE_VOICEMAIL_SELECTION); + } + } + + /** + * Determines if the supplied uri has the request to allow voicemails to be + * included. + */ + private boolean isAllowVoicemailRequest(Uri uri) { + return uri.getBooleanQueryParameter(ALLOW_VOICEMAILS_PARAM_KEY, false); + } + + /** + * Checks to ensure that the given uri has allow_voicemail set. Used by + * insert and update operations to check that ContentValues with voicemail + * call type must use the voicemail uri. + * @throws IllegalArgumentException if allow_voicemail is not set. + */ + private void checkIsAllowVoicemailRequest(Uri uri) { + if (!isAllowVoicemailRequest(uri)) { + throw new IllegalArgumentException( + String.format("Uri %s cannot be used for voicemail record." + + " Please set '%s=true' in the uri.", uri, ALLOW_VOICEMAILS_PARAM_KEY)); + } + } + + /** + * Parses the call Id from the given uri, assuming that this is a uri that + * matches CALLS_ID. For other uri types the behaviour is undefined. + * @throws IllegalArgumentException if the id included in the Uri is not a valid long value. + */ + private String parseCallIdFromUri(Uri uri) { + try { + Long id = Long.valueOf(uri.getPathSegments().get(1)); + return id.toString(); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid call id in uri: " + uri, e); + } + } } diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java index 52903d1..07a9103 100644 --- a/src/com/android/providers/contacts/VoicemailContentProvider.java +++ b/src/com/android/providers/contacts/VoicemailContentProvider.java @@ -82,14 +82,14 @@ public class VoicemailContentProvider extends ContentProvider { .build(); private ContentResolver mContentResolver; private ContactsDatabaseHelper mDbHelper; + private VoicemailPermissions mVoicemailPermissions; @Override public boolean onCreate() { Context context = context(); - mContentResolver = context.getContentResolver(); mDbHelper = getDatabaseHelper(context); - + mVoicemailPermissions = new VoicemailPermissions(context); return true; } @@ -141,7 +141,7 @@ public class VoicemailContentProvider extends ContentProvider { @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { - checkCallerHasOwnPermission(); + mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); UriData uriData = createUriData(uri); checkPackagePermission(uriData); return queryInternal(uriData, projection, @@ -184,7 +184,7 @@ public class VoicemailContentProvider extends ContentProvider { @Override public int bulkInsert(Uri uri, ContentValues[] valuesArray) { - checkCallerHasOwnPermission(); + mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); // 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); @@ -202,7 +202,7 @@ public class VoicemailContentProvider extends ContentProvider { @Override public Uri insert(Uri uri, ContentValues values) { - checkCallerHasOwnPermission(); + mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); return insertInternal(createUriData(uri), values, true); } @@ -266,7 +266,7 @@ public class VoicemailContentProvider extends ContentProvider { } } // You must have access to the provider given in values. - if (!callerHasFullPermission()) { + if (!mVoicemailPermissions.callerHasFullAccess()) { checkPackagesMatch(getCallingPackage(), values.getAsString(Voicemails.SOURCE_PACKAGE), uriData.getUri()); } @@ -297,7 +297,7 @@ public class VoicemailContentProvider extends ContentProvider { @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - checkCallerHasOwnPermission(); + mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); UriData uriData = createUriData(uri); checkPackagePermission(uriData); checkForSupportedColumns(sVoicemailProjectionMap, values); @@ -323,7 +323,7 @@ public class VoicemailContentProvider extends ContentProvider { @Override public int delete(Uri uri, String selection, String[] selectionArgs) { - checkCallerHasOwnPermission(); + mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); UriData uriData = createUriData(uri); checkPackagePermission(uriData); final SQLiteDatabase db = mDbHelper.getWritableDatabase(); @@ -359,7 +359,7 @@ public class VoicemailContentProvider extends ContentProvider { @Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { - checkCallerHasOwnPermission(); + mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); UriData uriData = createUriData(uri); checkPackagePermission(uriData); @@ -463,7 +463,7 @@ public class VoicemailContentProvider extends ContentProvider { * @throws SecurityException if the check fails. */ private void checkPackagePermission(UriData uriData) { - if (!callerHasFullPermission()) { + if (!mVoicemailPermissions.callerHasFullAccess()) { if (!uriData.hasSourcePackage()) { // You cannot have a match if this is not a provider uri. throw new SecurityException(String.format( @@ -525,11 +525,11 @@ public class VoicemailContentProvider extends ContentProvider { // which one we return. String bestSoFar = callerPackages[0]; for (String callerPackage : callerPackages) { - if (hasPermission(callerPackage, Manifest.permission.READ_WRITE_ALL_VOICEMAIL)) { + if (mVoicemailPermissions.packageHasFullAccess(callerPackage)) { // Full always wins, we can return early. return callerPackage; } - if (hasPermission(callerPackage, Manifest.permission.READ_WRITE_OWN_VOICEMAIL)) { + if (mVoicemailPermissions.packageHasOwnVoicemailAccess(callerPackage)) { bestSoFar = callerPackage; } } @@ -537,45 +537,11 @@ public class VoicemailContentProvider extends ContentProvider { } /** - * 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 checkCallerHasOwnPermission() { - if (!callerHasOwnPermission()) { - throw new SecurityException("The caller must have permission: " + - Manifest.permission.READ_WRITE_OWN_VOICEMAIL); - } - } - - /** Determines if the calling process has own permission. */ - private boolean callerHasOwnPermission() { - return callerHasPermission(Manifest.permission.READ_WRITE_OWN_VOICEMAIL); - } - - /** Determines if the calling process has full permission. */ - private boolean callerHasFullPermission() { - return callerHasOwnPermission() && - callerHasPermission(Manifest.permission.READ_WRITE_ALL_VOICEMAIL); - } - - /** Determines if the calling process has the given permission. */ - boolean callerHasPermission(String permission) { - return context().checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED; - } - - /** Determines if the given package has the given permission. */ - 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 (callerHasFullPermission()) { + if (mVoicemailPermissions.callerHasFullAccess()) { return null; } return getEqualityClause(Voicemails.SOURCE_PACKAGE, getCallingPackage()); diff --git a/src/com/android/providers/contacts/VoicemailPermissions.java b/src/com/android/providers/contacts/VoicemailPermissions.java new file mode 100644 index 0000000..34dbced --- /dev/null +++ b/src/com/android/providers/contacts/VoicemailPermissions.java @@ -0,0 +1,91 @@ +/* + * 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 android.content.Context; +import android.content.pm.PackageManager; + +/** + * Provides method related to check various voicemail permissions under the + * specified context. + * <p> This is an immutable object. + */ +public class VoicemailPermissions { + private final Context mContext; + + public VoicemailPermissions(Context context) { + mContext = context; + } + + /** Determines if the calling process has access to its own voicemails. */ + public boolean callerHasOwnVoicemailAccess() { + return callerHasPermission(Manifest.permission.READ_WRITE_OWN_VOICEMAIL); + } + + /** Determines if the calling process has access to all voicemails. */ + public boolean callerHasFullAccess() { + return callerHasPermission(Manifest.permission.READ_WRITE_OWN_VOICEMAIL) && + callerHasPermission(Manifest.permission.READ_WRITE_ALL_VOICEMAIL); + } + + /** + * Checks that the caller has permissions to access its own voicemails. + * + * @throws SecurityException if the caller does not have the voicemail source permission. + */ + public void checkCallerHasOwnVoicemailAccess() { + if (!callerHasOwnVoicemailAccess()) { + throw new SecurityException("The caller must have permission: " + + Manifest.permission.READ_WRITE_OWN_VOICEMAIL); + } + } + + /** + * Checks that the caller has permissions to access ALL voicemails. + * + * @throws SecurityException if the caller does not have the voicemail source permission. + */ + public void checkCallerHasFullAccess() { + if (!callerHasFullAccess()) { + throw new SecurityException(String.format("The caller must have permissions %s AND %s", + Manifest.permission.READ_WRITE_OWN_VOICEMAIL, + Manifest.permission.READ_WRITE_ALL_VOICEMAIL)); + } + } + + /** Determines if the given package has access to its own voicemails. */ + public boolean packageHasOwnVoicemailAccess(String packageName) { + return packageHasPermission(packageName, Manifest.permission.READ_WRITE_OWN_VOICEMAIL); + } + + /** Determines if the given package has full access. */ + public boolean packageHasFullAccess(String packageName) { + return packageHasPermission(packageName, Manifest.permission.READ_WRITE_OWN_VOICEMAIL) && + packageHasPermission(packageName, Manifest.permission.READ_WRITE_ALL_VOICEMAIL); + } + + /** Determines if the given package has the given permission. */ + private boolean packageHasPermission(String packageName, String permission) { + return mContext.getPackageManager().checkPermission(permission, packageName) + == PackageManager.PERMISSION_GRANTED; + } + + /** Determines if the calling process has the given permission. */ + private boolean callerHasPermission(String permission) { + return mContext.checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED; + } +} diff --git a/src/com/android/providers/contacts/util/DbQueryUtils.java b/src/com/android/providers/contacts/util/DbQueryUtils.java index 0045c59..58c8bb1 100644 --- a/src/com/android/providers/contacts/util/DbQueryUtils.java +++ b/src/com/android/providers/contacts/util/DbQueryUtils.java @@ -31,10 +31,19 @@ public class DbQueryUtils { /** Returns a WHERE clause asserting equality of a field to a value. */ public static String getEqualityClause(String field, String value) { + return getClauseWithOperator(field, "=", value); + } + + /** Returns a WHERE clause asserting in-equality of a field to a value. */ + public static String getInequalityClause(String field, String value) { + return getClauseWithOperator(field, "!=", value); + } + + private static String getClauseWithOperator(String field, String operator, String value) { StringBuilder clause = new StringBuilder(); clause.append("("); clause.append(field); - clause.append(" = "); + clause.append(" ").append(operator).append(" "); DatabaseUtils.appendEscapedSQLString(clause, value); clause.append(")"); return clause.toString(); diff --git a/src/com/android/providers/contacts/util/SelectionBuilder.java b/src/com/android/providers/contacts/util/SelectionBuilder.java new file mode 100644 index 0000000..14499e8 --- /dev/null +++ b/src/com/android/providers/contacts/util/SelectionBuilder.java @@ -0,0 +1,63 @@ +/* + * 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.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Builds a selection clause by concatenating several clauses with AND. + */ +public class SelectionBuilder { + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + private final List<String> mWhereClauses; + + /** + * @param baseSelection The base selection to start with. This is typically + * the user supplied selection arg. Pass null if no base selection is + * required. + */ + public SelectionBuilder(String baseSelection) { + mWhereClauses = new ArrayList<String>(); + addClause(baseSelection); + } + + /** + * Adds a new clause to the selection. Nothing is added if the supplied clause + * is null or empty. + */ + public SelectionBuilder addClause(String clause) { + if (!TextUtils.isEmpty(clause)) { + mWhereClauses.add(clause); + } + return this; + } + + /** + * Returns a combined selection clause with AND of all clauses added using + * {@link #addClause(String)}. Returns null if no clause has been added or + * only null/empty clauses have been added till now. + */ + public String build() { + if (mWhereClauses.size() == 0) { + return null; + } + return DbQueryUtils.concatenateClauses(mWhereClauses.toArray(EMPTY_STRING_ARRAY)); + } +} |