blob: b2f6b1e95e01e0897151f1f814bd062e9b26931d [file] [log] [blame]
/*
* 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 android.provider.VoicemailContract.SOURCE_PACKAGE_FIELD;
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.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
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.util.SelectionBuilder;
import com.android.providers.contacts.util.TypedUriMatcherImpl;
import com.google.common.annotations.VisibleForTesting;
import java.io.FileNotFoundException;
import java.util.List;
/**
* An implementation of the Voicemail content provider. This class in the entry point for both
* voicemail content ('calls') table and 'voicemail_status' table. This class performs all common
* permission checks and then delegates database level operations to respective table delegate
* objects.
*/
public class VoicemailContentProvider extends ContentProvider
implements VoicemailTable.DelegateHelper {
private VoicemailPermissions mVoicemailPermissions;
private VoicemailTable.Delegate mVoicemailContentTable;
private VoicemailTable.Delegate mVoicemailStatusTable;
@Override
public boolean onCreate() {
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate start");
}
Context context = context();
mVoicemailPermissions = new VoicemailPermissions(context);
mVoicemailContentTable = new VoicemailContentTable(Tables.CALLS, context,
getDatabaseHelper(context), this, createCallLogInsertionHelper(context));
mVoicemailStatusTable = new VoicemailStatusTable(Tables.VOICEMAIL_STATUS, context,
getDatabaseHelper(context), this);
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
Log.d(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate finish");
}
return true;
}
@VisibleForTesting
/*package*/ CallLogInsertionHelper createCallLogInsertionHelper(Context context) {
return DefaultCallLogInsertionHelper.getInstance(context);
}
@VisibleForTesting
/*package*/ ContactsDatabaseHelper getDatabaseHelper(Context context) {
return ContactsDatabaseHelper.getInstance(context);
}
@VisibleForTesting
/*package*/ Context context() {
return getContext();
}
@Override
public String getType(Uri uri) {
UriData uriData = null;
try {
uriData = UriData.createUriData(uri);
} catch (IllegalArgumentException ignored) {
// Special case: for illegal URIs, we return null rather than thrown an exception.
return null;
}
return getTableDelegate(uriData).getType(uriData);
}
@Override
public Uri insert(Uri uri, ContentValues values) {
UriData uriData = checkPermissionsAndCreateUriData(uri, values);
return getTableDelegate(uriData).insert(uriData, values);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
UriData uriData = checkPermissionsAndCreateUriDataForReadOperation(uri);
SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
selectionBuilder.addClause(getPackageRestrictionClause());
return getTableDelegate(uriData).query(uriData, projection, selectionBuilder.build(),
selectionArgs, sortOrder);
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
UriData uriData = checkPermissionsAndCreateUriData(uri, values);
SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
selectionBuilder.addClause(getPackageRestrictionClause());
return getTableDelegate(uriData).update(uriData, values, selectionBuilder.build(),
selectionArgs);
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
UriData uriData = checkPermissionsAndCreateUriData(uri);
SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
selectionBuilder.addClause(getPackageRestrictionClause());
return getTableDelegate(uriData).delete(uriData, selectionBuilder.build(), selectionArgs);
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
UriData uriData = null;
if (mode.equals("r")) {
uriData = checkPermissionsAndCreateUriDataForReadOperation(uri);
} else {
uriData = checkPermissionsAndCreateUriData(uri);
}
// openFileHelper() relies on "_data" column to be populated with the file path.
return getTableDelegate(uriData).openFile(uriData, mode);
}
/** Returns the correct table delegate object that can handle this URI. */
private VoicemailTable.Delegate getTableDelegate(UriData uriData) {
switch (uriData.getUriType()) {
case STATUS:
case STATUS_ID:
return mVoicemailStatusTable;
case VOICEMAILS:
case VOICEMAILS_ID:
return mVoicemailContentTable;
case NO_MATCH:
throw new IllegalStateException("Invalid uri type for uri: " + uriData.getUri());
default:
throw new IllegalStateException("Impossible, all cases are covered.");
}
}
/**
* Decorates a URI by providing methods to get various properties from the URI.
*/
public static class UriData {
private final Uri mUri;
private final String mId;
private final String mSourcePackage;
private final VoicemailUriType mUriType;
public UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage) {
mUriType = uriType;
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;
}
/** Gets the Voicemail URI type. */
public final VoicemailUriType getUriType() {
return mUriType;
}
/** Builds a where clause from the URI data. */
public final String getWhereClause() {
return concatenateClauses(
(hasId() ? getEqualityClause(BaseColumns._ID, getId()) : null),
(hasSourcePackage() ? getEqualityClause(SOURCE_PACKAGE_FIELD,
getSourcePackage()) : null));
}
/** Create a {@link UriData} corresponding to a given uri. */
public static UriData createUriData(Uri uri) {
String sourcePackage = uri.getQueryParameter(
VoicemailContract.PARAM_KEY_SOURCE_PACKAGE);
List<String> segments = uri.getPathSegments();
VoicemailUriType uriType = createUriMatcher().match(uri);
switch (uriType) {
case VOICEMAILS:
case STATUS:
return new UriData(uri, uriType, null, sourcePackage);
case VOICEMAILS_ID:
case STATUS_ID:
return new UriData(uri, uriType, segments.get(1), sourcePackage);
case NO_MATCH:
throw new IllegalArgumentException("Invalid URI: " + uri);
default:
throw new IllegalStateException("Impossible, all cases are covered");
}
}
private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() {
return new TypedUriMatcherImpl<VoicemailUriType>(
VoicemailContract.AUTHORITY, VoicemailUriType.values());
}
}
@Override
// VoicemailTable.DelegateHelper interface.
public void checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values) {
// If content values don't contain the provider, calculate the right provider to use.
if (!values.containsKey(SOURCE_PACKAGE_FIELD)) {
String provider = uriData.hasSourcePackage() ?
uriData.getSourcePackage() : getCallingPackage();
values.put(SOURCE_PACKAGE_FIELD, provider);
}
// You must have access to the provider given in values.
if (!mVoicemailPermissions.callerHasFullAccess()) {
checkPackagesMatch(getCallingPackage(),
values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD),
uriData.getUri());
}
}
/**
* Checks that the source_package field is same in uriData and ContentValues, if it happens
* to be set in both.
*/
private void checkSourcePackageSameIfSet(UriData uriData, ContentValues values) {
if (uriData.hasSourcePackage() && values.containsKey(SOURCE_PACKAGE_FIELD)) {
if (!uriData.getSourcePackage().equals(values.get(SOURCE_PACKAGE_FIELD))) {
throw new SecurityException(
"source_package in URI was " + uriData.getSourcePackage() +
" but doesn't match source_package in ContentValues which was "
+ values.get(SOURCE_PACKAGE_FIELD));
}
}
}
@Override
/** Implementation of {@link VoicemailTable.DelegateHelper#openDataFile(UriData, String)} */
public ParcelFileDescriptor openDataFile(UriData uriData, String mode)
throws FileNotFoundException {
return openFileHelper(uriData.getUri(), mode);
}
/**
* Performs necessary voicemail permission checks common to all operations and returns
* the structured representation, {@link UriData}, of the supplied uri.
*/
private UriData checkPermissionsAndCreateUriDataForReadOperation(Uri uri) {
// If the caller has been explicitly granted read permission to this URI then no need to
// check further.
if (context().checkCallingUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
== PackageManager.PERMISSION_GRANTED) {
return UriData.createUriData(uri);
}
return checkPermissionsAndCreateUriData(uri);
}
/**
* Performs necessary voicemail permission checks common to all operations and returns
* the structured representation, {@link UriData}, of the supplied uri.
*/
private UriData checkPermissionsAndCreateUriData(Uri uri) {
mVoicemailPermissions.checkCallerHasOwnVoicemailAccess();
UriData uriData = UriData.createUriData(uri);
checkPackagePermission(uriData);
return uriData;
}
/**
* Same as {@link #checkPackagePermission(UriData)}. In addition does permission check
* on the ContentValues.
*/
private UriData checkPermissionsAndCreateUriData(Uri uri, ContentValues... valuesArray) {
UriData uriData = checkPermissionsAndCreateUriData(uri);
for (ContentValues values : valuesArray) {
checkSourcePackageSameIfSet(uriData, values);
}
return uriData;
}
/**
* Checks that the callingPackage is same as voicemailSourcePackage. Throws {@link
* SecurityException} if they don't match.
*/
private final void checkPackagesMatch(String callingPackage, String voicemailSourcePackage,
Uri uri) {
if (!voicemailSourcePackage.equals(callingPackage)) {
String errorMsg = String.format("Permission denied for URI: %s\n. " +
"Package %s cannot perform this operation for %s. Requires %s permission.",
uri, callingPackage, voicemailSourcePackage,
Manifest.permission.READ_WRITE_ALL_VOICEMAIL);
throw new SecurityException(errorMsg);
}
}
/**
* Checks that either the caller has READ_WRITE_ALL_VOICEMAIL permission, or has the
* ADD_VOICEMAIL permission and is using a URI that matches
* /voicemail/?source_package=[source-package] where [source-package] is the same as the calling
* package.
*
* @throws SecurityException if the check fails.
*/
private void checkPackagePermission(UriData uriData) {
if (!mVoicemailPermissions.callerHasFullAccess()) {
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 set query parameter '%s' in the URI.\nURI: %s",
getCallingPackage(), Manifest.permission.READ_WRITE_ALL_VOICEMAIL,
VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, uriData.getUri()));
}
checkPackagesMatch(getCallingPackage(), uriData.getSourcePackage(), uriData.getUri());
}
}
/**
* 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 (mVoicemailPermissions.packageHasFullAccess(callerPackage)) {
// Full always wins, we can return early.
return callerPackage;
}
if (mVoicemailPermissions.packageHasOwnVoicemailAccess(callerPackage)) {
bestSoFar = callerPackage;
}
}
return bestSoFar;
}
/**
* 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 (mVoicemailPermissions.callerHasFullAccess()) {
return null;
}
return getEqualityClause(Voicemails.SOURCE_PACKAGE, getCallingPackage());
}
}