blob: 1ced1be667371143a0a95ff49d3d11e19e19e9f0 [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.app.AppOpsManager.MODE_ALLOWED;
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.annotation.NonNull;
import android.app.AppOpsManager;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
import android.provider.VoicemailContract;
import android.provider.VoicemailContract.Status;
import android.provider.VoicemailContract.Voicemails;
import android.util.ArraySet;
import android.util.Log;
import com.android.providers.contacts.CallLogDatabaseHelper.Tables;
import com.android.providers.contacts.util.ContactsPermissions;
import com.android.providers.contacts.util.PackageUtils;
import com.android.providers.contacts.util.SelectionBuilder;
import com.android.providers.contacts.util.TypedUriMatcherImpl;
import com.android.providers.contacts.util.UserUtils;
import com.google.common.annotations.VisibleForTesting;
import java.io.FileNotFoundException;
import java.util.Arrays;
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 static final String TAG = "VoicemailProvider";
public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
private static final int BACKGROUND_TASK_SCAN_STALE_PACKAGES = 0;
private ContactsTaskScheduler mTaskScheduler;
private VoicemailPermissions mVoicemailPermissions;
private VoicemailTable.Delegate mVoicemailContentTable;
private VoicemailTable.Delegate mVoicemailStatusTable;
@Override
public boolean onCreate() {
if (VERBOSE_LOGGING) {
Log.v(TAG, "onCreate: " + this.getClass().getSimpleName()
+ " user=" + android.os.Process.myUserHandle().getIdentifier());
}
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.INFO)) {
Log.i(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate start");
}
Context context = context();
// Read and write permission requires ADD_VOICEMAIL or carrier privileges. We can't declare
// any permission entries in the manifest because carrier-privileged apps without
// ADD_VOICEMAIL would be blocked by the platform without even reaching our custom
// enforce{Read,Write}PermissionInner functions. These overrides are what allow carrier-
// privileged apps to bypass these runtime-configured permissions.
// TODO(b/74245334): See if these can be removed since individual operations perform their
// own checks.
setReadPermission(android.Manifest.permission.ADD_VOICEMAIL);
setWritePermission(android.Manifest.permission.ADD_VOICEMAIL);
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);
mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) {
@Override
public void onPerformTask(int taskId, Object arg) {
performBackgroundTask(taskId, arg);
}
};
scheduleScanStalePackages();
ContactsPackageMonitor.start(getContext());
if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.INFO)) {
Log.i(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate finish");
}
return true;
}
@Override
protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken)
throws SecurityException {
// Permit carrier-privileged apps regardless of ADD_VOICEMAIL permission state.
if (mVoicemailPermissions.callerHasCarrierPrivileges()) {
return MODE_ALLOWED;
}
return super.enforceReadPermissionInner(uri, callingPkg, callerToken);
}
@Override
protected int enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken)
throws SecurityException {
// Permit carrier-privileged apps regardless of ADD_VOICEMAIL permission state.
if (mVoicemailPermissions.callerHasCarrierPrivileges()) {
return MODE_ALLOWED;
}
return super.enforceWritePermissionInner(uri, callingPkg, callerToken);
}
@VisibleForTesting
void scheduleScanStalePackages() {
scheduleTask(BACKGROUND_TASK_SCAN_STALE_PACKAGES, null);
}
@VisibleForTesting
void scheduleTask(int taskId, Object arg) {
mTaskScheduler.scheduleTask(taskId, arg);
}
@VisibleForTesting
/*package*/ CallLogInsertionHelper createCallLogInsertionHelper(Context context) {
return DefaultCallLogInsertionHelper.getInstance(context);
}
@VisibleForTesting
/*package*/ CallLogDatabaseHelper getDatabaseHelper(Context context) {
return CallLogDatabaseHelper.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) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "insert: uri=" + uri + " values=[" + values + "]" +
" CPID=" + Binder.getCallingPid());
}
UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values);
return getTableDelegate(uriData).insert(uriData, values);
}
@Override
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values);
return getTableDelegate(uriData).bulkInsert(uriData, values);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "query: uri=" + uri + " projection=" + Arrays.toString(projection) +
" selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
" order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
" User=" + UserUtils.getCurrentUserHandle(getContext()));
}
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) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "update: uri=" + uri +
" selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
" values=[" + values + "] CPID=" + Binder.getCallingPid() +
" User=" + UserUtils.getCurrentUserHandle(getContext()));
}
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) {
if (VERBOSE_LOGGING) {
Log.v(TAG, "delete: uri=" + uri +
" selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) +
" CPID=" + Binder.getCallingPid() +
" User=" + UserUtils.getCurrentUserHandle(getContext()));
}
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 {
boolean success = false;
try {
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.
final ParcelFileDescriptor ret = getTableDelegate(uriData).openFile(uriData, mode);
success = true;
return ret;
} finally {
if (VERBOSE_LOGGING) {
Log.v(TAG, "openFile uri=" + uri + " mode=" + mode + " success=" + success +
" CPID=" + Binder.getCallingPid() +
" User=" + UserUtils.getCurrentUserHandle(getContext()));
}
}
}
/** 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;
private 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() : getInjectedCallingPackage();
values.put(SOURCE_PACKAGE_FIELD, provider);
}
// You must have access to the provider given in values.
if (!mVoicemailPermissions.callerHasWriteAccess(getCallingPackage())) {
checkPackagesMatch(getInjectedCallingPackage(),
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",
getInjectedCallingPackage(), android.Manifest.permission.WRITE_VOICEMAIL,
VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, uriData.getUri()));
}
checkPackagesMatch(getInjectedCallingPackage(), uriData.getSourcePackage(),
uriData.getUri());
}
}
@VisibleForTesting
String getInjectedCallingPackage() {
return super.getCallingPackage();
}
/**
* 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, getInjectedCallingPackage());
}
/**
* 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());
}
/** Remove all records from a given source package. */
public void removeBySourcePackage(String packageName) {
delete(Voicemails.buildSourceUri(packageName), null, null);
delete(Status.buildSourceUri(packageName), null, null);
}
@VisibleForTesting
void performBackgroundTask(int task, Object arg) {
switch (task) {
case BACKGROUND_TASK_SCAN_STALE_PACKAGES:
removeStalePackages();
break;
}
}
/**
* Remove all records made by packages that no longer exist.
*/
private void removeStalePackages() {
if (VERBOSE_LOGGING) {
Log.v(TAG, "scanStalePackages start");
}
// Make sure all source tables still exists.
// First, list all source packages.
final ArraySet<String> packages = mVoicemailContentTable.getSourcePackages();
packages.addAll(mVoicemailStatusTable.getSourcePackages());
// Remove the ones that still exist.
for (int i = packages.size() - 1; i >= 0; i--) {
final String pkg = packages.valueAt(i);
final boolean installed = PackageUtils.isPackageInstalled(getContext(), pkg);
if (VERBOSE_LOGGING) {
Log.v(TAG, " " + pkg + (installed ? " installed" : " removed"));
}
if (!installed) {
removeBySourcePackage(pkg);
}
}
if (VERBOSE_LOGGING) {
Log.v(TAG, "scanStalePackages finish");
}
}
}