/* * 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.app.AppOpsManager; 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.ContactsPermissions; 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(); // ADD_VOICEMAIL permission guards read and write. We do the same with app ops. // The permission name doesn't reflect its function but we cannot rename it. setAppOps(AppOpsManager.OP_ADD_VOICEMAIL, AppOpsManager.OP_ADD_VOICEMAIL); 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 = checkPermissionsAndCreateUriDataForWrite(uri, values); return getTableDelegate(uriData).insert(uriData, values); } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { UriData uriData = checkPermissionsAndCreateUriDataForRead(uri); SelectionBuilder selectionBuilder = new SelectionBuilder(selection); selectionBuilder.addClause(getPackageRestrictionClause(true/*isQuery*/)); return getTableDelegate(uriData).query(uriData, projection, selectionBuilder.build(), selectionArgs, sortOrder); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values); SelectionBuilder selectionBuilder = new SelectionBuilder(selection); selectionBuilder.addClause(getPackageRestrictionClause(false/*isQuery*/)); return getTableDelegate(uriData).update(uriData, values, selectionBuilder.build(), selectionArgs); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri); SelectionBuilder selectionBuilder = new SelectionBuilder(selection); selectionBuilder.addClause(getPackageRestrictionClause(false/*isQuery*/)); 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 = checkPermissionsAndCreateUriDataForRead(uri); } else { uriData = checkPermissionsAndCreateUriDataForWrite(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 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 createUriMatcher() { return new TypedUriMatcherImpl( 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.callerHasWriteAccess(getCallingPackage())) { 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); } /** * Ensures that the caller has the permissions to perform a query/read operation, and * then returns the structured representation {@link UriData} of the supplied uri. */ private UriData checkPermissionsAndCreateUriDataForRead(Uri uri) { // If the caller has been explicitly granted read permission to this URI then no need to // check further. if (ContactsPermissions.hasCallerUriPermission( getContext(), uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)) { return UriData.createUriData(uri); } if (mVoicemailPermissions.callerHasReadAccess(getCallingPackage())) { return UriData.createUriData(uri); } return checkPermissionsAndCreateUriData(uri, true); } /** * 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, boolean read) { UriData uriData = UriData.createUriData(uri); if (!hasReadWritePermission(read)) { mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); checkPackagePermission(uriData); } return uriData; } /** * Ensures that the caller has the permissions to perform an update/delete operation, and * then returns the structured representation {@link UriData} of the supplied uri. * Also does a permission check on the ContentValues. */ private UriData checkPermissionsAndCreateUriDataForWrite(Uri uri, ContentValues... valuesArray) { UriData uriData = checkPermissionsAndCreateUriData(uri, false); 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, android.Manifest.permission.WRITE_VOICEMAIL); throw new SecurityException(errorMsg); } } /** * Checks that either the caller has the MANAGE_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.callerHasWriteAccess(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 set query parameter '%s' in the URI.\nURI: %s", getCallingPackage_(), android.Manifest.permission.WRITE_VOICEMAIL, VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, uriData.getUri())); } checkPackagesMatch(getCallingPackage_(), uriData.getSourcePackage(), uriData.getUri()); } } /** * Gets the name of the calling package. *

* 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.packageHasWriteAccess(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(boolean isQuery) { if (hasReadWritePermission(isQuery)) { return null; } return getEqualityClause(Voicemails.SOURCE_PACKAGE, getCallingPackage_()); } /** * Whether or not the calling package has the appropriate read/write permission. The user * selected default and/or system dialers are always allowed to read and write to the * VoicemailContentProvider. * * @param read Whether or not this operation is a read * * @return True if the package has the permission required to perform the read/write operation */ private boolean hasReadWritePermission(boolean read) { return read ? mVoicemailPermissions.callerHasReadAccess(getCallingPackage()) : mVoicemailPermissions.callerHasWriteAccess(getCallingPackage()); } }