Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 package com.android.providers.contacts;
     17 
     18 import static android.provider.VoicemailContract.SOURCE_PACKAGE_FIELD;
     19 import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses;
     20 import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause;
     21 
     22 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
     23 import com.android.providers.contacts.util.SelectionBuilder;
     24 import com.android.providers.contacts.util.TypedUriMatcherImpl;
     25 import com.google.common.annotations.VisibleForTesting;
     26 
     27 import android.content.ContentProvider;
     28 import android.content.ContentValues;
     29 import android.content.Context;
     30 import android.content.Intent;
     31 import android.content.pm.PackageManager;
     32 import android.database.Cursor;
     33 import android.net.Uri;
     34 import android.os.Binder;
     35 import android.os.ParcelFileDescriptor;
     36 import android.provider.BaseColumns;
     37 import android.provider.VoicemailContract;
     38 import android.provider.VoicemailContract.Voicemails;
     39 import android.util.Log;
     40 
     41 import java.io.FileNotFoundException;
     42 import java.util.List;
     43 
     44 /**
     45  * An implementation of the Voicemail content provider. This class in the entry point for both
     46  * voicemail content ('calls') table and 'voicemail_status' table. This class performs all common
     47  * permission checks and then delegates database level operations to respective table delegate
     48  * objects.
     49  */
     50 public class VoicemailContentProvider extends ContentProvider
     51         implements VoicemailTable.DelegateHelper {
     52     private static final String TAG = "VoicemailContentProvider";
     53 
     54     private VoicemailPermissions mVoicemailPermissions;
     55     private VoicemailTable.Delegate mVoicemailContentTable;
     56     private VoicemailTable.Delegate mVoicemailStatusTable;
     57 
     58     @Override
     59     public boolean onCreate() {
     60         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
     61             Log.d(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate start");
     62         }
     63         Context context = context();
     64         mVoicemailPermissions = new VoicemailPermissions(context);
     65         mVoicemailContentTable = new VoicemailContentTable(Tables.CALLS, context,
     66                 getDatabaseHelper(context), this, createCallLogInsertionHelper(context));
     67         mVoicemailStatusTable = new VoicemailStatusTable(Tables.VOICEMAIL_STATUS, context,
     68                 getDatabaseHelper(context), this);
     69         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
     70             Log.d(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate finish");
     71         }
     72         return true;
     73     }
     74 
     75     @VisibleForTesting
     76     /*package*/ CallLogInsertionHelper createCallLogInsertionHelper(Context context) {
     77         return DefaultCallLogInsertionHelper.getInstance(context);
     78     }
     79 
     80     @VisibleForTesting
     81     /*package*/ ContactsDatabaseHelper getDatabaseHelper(Context context) {
     82         return ContactsDatabaseHelper.getInstance(context);
     83     }
     84 
     85     @VisibleForTesting
     86     /*package*/ Context context() {
     87         return getContext();
     88     }
     89 
     90     @Override
     91     public String getType(Uri uri) {
     92         UriData uriData = null;
     93         try {
     94             uriData = UriData.createUriData(uri);
     95         } catch (IllegalArgumentException ignored) {
     96             // Special case: for illegal URIs, we return null rather than thrown an exception.
     97             return null;
     98         }
     99         return getTableDelegate(uriData).getType(uriData);
    100     }
    101 
    102     @Override
    103     public Uri insert(Uri uri, ContentValues values) {
    104         UriData uriData = checkPermissionsAndCreateUriData(uri, values);
    105         return getTableDelegate(uriData).insert(uriData, values);
    106     }
    107 
    108     @Override
    109     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    110             String sortOrder) {
    111         UriData uriData = checkPermissionsAndCreateUriDataForReadOperation(uri);
    112         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
    113         selectionBuilder.addClause(getPackageRestrictionClause());
    114         return getTableDelegate(uriData).query(uriData, projection, selectionBuilder.build(),
    115                 selectionArgs, sortOrder);
    116     }
    117 
    118     @Override
    119     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    120         UriData uriData = checkPermissionsAndCreateUriData(uri, values);
    121         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
    122         selectionBuilder.addClause(getPackageRestrictionClause());
    123         return getTableDelegate(uriData).update(uriData, values, selectionBuilder.build(),
    124                 selectionArgs);
    125     }
    126 
    127     @Override
    128     public int delete(Uri uri, String selection, String[] selectionArgs) {
    129         UriData uriData = checkPermissionsAndCreateUriData(uri);
    130         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
    131         selectionBuilder.addClause(getPackageRestrictionClause());
    132         return getTableDelegate(uriData).delete(uriData, selectionBuilder.build(), selectionArgs);
    133     }
    134 
    135     @Override
    136     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    137         UriData uriData = null;
    138         if (mode.equals("r")) {
    139             uriData = checkPermissionsAndCreateUriDataForReadOperation(uri);
    140         } else {
    141             uriData = checkPermissionsAndCreateUriData(uri);
    142         }
    143         // openFileHelper() relies on "_data" column to be populated with the file path.
    144         return getTableDelegate(uriData).openFile(uriData, mode);
    145     }
    146 
    147     /** Returns the correct table delegate object that can handle this URI. */
    148     private VoicemailTable.Delegate getTableDelegate(UriData uriData) {
    149         switch (uriData.getUriType()) {
    150             case STATUS:
    151             case STATUS_ID:
    152                 return mVoicemailStatusTable;
    153             case VOICEMAILS:
    154             case VOICEMAILS_ID:
    155                 return mVoicemailContentTable;
    156             case NO_MATCH:
    157                 throw new IllegalStateException("Invalid uri type for uri: " + uriData.getUri());
    158             default:
    159                 throw new IllegalStateException("Impossible, all cases are covered.");
    160         }
    161     }
    162 
    163     /**
    164      * Decorates a URI by providing methods to get various properties from the URI.
    165      */
    166     public static class UriData {
    167         private final Uri mUri;
    168         private final String mId;
    169         private final String mSourcePackage;
    170         private final VoicemailUriType mUriType;
    171 
    172         public UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage) {
    173             mUriType = uriType;
    174             mUri = uri;
    175             mId = id;
    176             mSourcePackage = sourcePackage;
    177         }
    178 
    179         /** Gets the original URI to which this {@link UriData} corresponds. */
    180         public final Uri getUri() {
    181             return mUri;
    182         }
    183 
    184         /** Tells us if our URI has an individual voicemail id. */
    185         public final boolean hasId() {
    186             return mId != null;
    187         }
    188 
    189         /** Gets the ID for the voicemail. */
    190         public final String getId() {
    191             return mId;
    192         }
    193 
    194         /** Tells us if our URI has a source package string. */
    195         public final boolean hasSourcePackage() {
    196             return mSourcePackage != null;
    197         }
    198 
    199         /** Gets the source package. */
    200         public final String getSourcePackage() {
    201             return mSourcePackage;
    202         }
    203 
    204         /** Gets the Voicemail URI type. */
    205         public final VoicemailUriType getUriType() {
    206             return mUriType;
    207         }
    208 
    209         /** Builds a where clause from the URI data. */
    210         public final String getWhereClause() {
    211             return concatenateClauses(
    212                     (hasId() ? getEqualityClause(BaseColumns._ID, getId()) : null),
    213                     (hasSourcePackage() ? getEqualityClause(SOURCE_PACKAGE_FIELD,
    214                             getSourcePackage()) : null));
    215         }
    216 
    217         /** Create a {@link UriData} corresponding to a given uri. */
    218         public static UriData createUriData(Uri uri) {
    219             String sourcePackage = uri.getQueryParameter(
    220                     VoicemailContract.PARAM_KEY_SOURCE_PACKAGE);
    221             List<String> segments = uri.getPathSegments();
    222             VoicemailUriType uriType = createUriMatcher().match(uri);
    223             switch (uriType) {
    224                 case VOICEMAILS:
    225                 case STATUS:
    226                     return new UriData(uri, uriType, null, sourcePackage);
    227                 case VOICEMAILS_ID:
    228                 case STATUS_ID:
    229                     return new UriData(uri, uriType, segments.get(1), sourcePackage);
    230                 case NO_MATCH:
    231                     throw new IllegalArgumentException("Invalid URI: " + uri);
    232                 default:
    233                     throw new IllegalStateException("Impossible, all cases are covered");
    234             }
    235         }
    236 
    237         private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() {
    238             return new TypedUriMatcherImpl<VoicemailUriType>(
    239                     VoicemailContract.AUTHORITY, VoicemailUriType.values());
    240         }
    241     }
    242 
    243     @Override
    244     // VoicemailTable.DelegateHelper interface.
    245     public void checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values) {
    246         // If content values don't contain the provider, calculate the right provider to use.
    247         if (!values.containsKey(SOURCE_PACKAGE_FIELD)) {
    248             String provider = uriData.hasSourcePackage() ?
    249                     uriData.getSourcePackage() : getCallingPackage();
    250             values.put(SOURCE_PACKAGE_FIELD, provider);
    251         }
    252         // You must have access to the provider given in values.
    253         if (!mVoicemailPermissions.callerHasFullAccess()) {
    254             checkPackagesMatch(getCallingPackage(),
    255                     values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD),
    256                     uriData.getUri());
    257         }
    258     }
    259 
    260     /**
    261      * Checks that the source_package field is same in uriData and ContentValues, if it happens
    262      * to be set in both.
    263      */
    264     private void checkSourcePackageSameIfSet(UriData uriData, ContentValues values) {
    265         if (uriData.hasSourcePackage() && values.containsKey(SOURCE_PACKAGE_FIELD)) {
    266             if (!uriData.getSourcePackage().equals(values.get(SOURCE_PACKAGE_FIELD))) {
    267                 throw new SecurityException(
    268                         "source_package in URI was " + uriData.getSourcePackage() +
    269                         " but doesn't match source_package in ContentValues which was "
    270                         + values.get(SOURCE_PACKAGE_FIELD));
    271             }
    272         }
    273     }
    274 
    275     @Override
    276     /** Implementation of  {@link VoicemailTable.DelegateHelper#openDataFile(UriData, String)} */
    277     public ParcelFileDescriptor openDataFile(UriData uriData, String mode)
    278             throws FileNotFoundException {
    279         return openFileHelper(uriData.getUri(), mode);
    280     }
    281 
    282     /**
    283      * Performs necessary voicemail permission checks common to all operations and returns
    284      * the structured representation, {@link UriData}, of the supplied uri.
    285      */
    286     private UriData checkPermissionsAndCreateUriDataForReadOperation(Uri uri) {
    287         // If the caller has been explicitly granted read permission to this URI then no need to
    288         // check further.
    289         if (context().checkCallingUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
    290                 == PackageManager.PERMISSION_GRANTED) {
    291             return UriData.createUriData(uri);
    292         }
    293         return checkPermissionsAndCreateUriData(uri);
    294     }
    295 
    296     /**
    297      * Performs necessary voicemail permission checks common to all operations and returns
    298      * the structured representation, {@link UriData}, of the supplied uri.
    299      */
    300     private UriData checkPermissionsAndCreateUriData(Uri uri) {
    301         mVoicemailPermissions.checkCallerHasOwnVoicemailAccess();
    302         UriData uriData = UriData.createUriData(uri);
    303         checkPackagePermission(uriData);
    304         return uriData;
    305     }
    306 
    307     /**
    308      * Same as {@link #checkPackagePermission(UriData)}. In addition does permission check
    309      * on the ContentValues.
    310      */
    311     private UriData checkPermissionsAndCreateUriData(Uri uri, ContentValues... valuesArray) {
    312         UriData uriData = checkPermissionsAndCreateUriData(uri);
    313         for (ContentValues values : valuesArray) {
    314             checkSourcePackageSameIfSet(uriData, values);
    315         }
    316         return uriData;
    317     }
    318 
    319     /**
    320      * Checks that the callingPackage is same as voicemailSourcePackage. Throws {@link
    321      * SecurityException} if they don't match.
    322      */
    323     private final void checkPackagesMatch(String callingPackage, String voicemailSourcePackage,
    324             Uri uri) {
    325         if (!voicemailSourcePackage.equals(callingPackage)) {
    326             String errorMsg = String.format("Permission denied for URI: %s\n. " +
    327                     "Package %s cannot perform this operation for %s. Requires %s permission.",
    328                     uri, callingPackage, voicemailSourcePackage,
    329                     Manifest.permission.READ_WRITE_ALL_VOICEMAIL);
    330             throw new SecurityException(errorMsg);
    331         }
    332     }
    333 
    334     /**
    335      * Checks that either the caller has READ_WRITE_ALL_VOICEMAIL permission, or has the
    336      * ADD_VOICEMAIL permission and is using a URI that matches
    337      * /voicemail/?source_package=[source-package] where [source-package] is the same as the calling
    338      * package.
    339      *
    340      * @throws SecurityException if the check fails.
    341      */
    342     private void checkPackagePermission(UriData uriData) {
    343         if (!mVoicemailPermissions.callerHasFullAccess()) {
    344             if (!uriData.hasSourcePackage()) {
    345                 // You cannot have a match if this is not a provider URI.
    346                 throw new SecurityException(String.format(
    347                         "Provider %s does not have %s permission." +
    348                                 "\nPlease set query parameter '%s' in the URI.\nURI: %s",
    349                         getCallingPackage(), Manifest.permission.READ_WRITE_ALL_VOICEMAIL,
    350                         VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, uriData.getUri()));
    351             }
    352             checkPackagesMatch(getCallingPackage(), uriData.getSourcePackage(), uriData.getUri());
    353         }
    354     }
    355 
    356     /**
    357      * Gets the name of the calling package.
    358      * <p>
    359      * It's possible (though unlikely) for there to be more than one calling package (requires that
    360      * your manifest say you want to share process ids) in which case we will return an arbitrary
    361      * package name. It's also possible (though very unlikely) for us to be unable to work out what
    362      * your calling package is, in which case we will return null.
    363      */
    364     /* package for test */String getCallingPackage() {
    365         int caller = Binder.getCallingUid();
    366         if (caller == 0) {
    367             return null;
    368         }
    369         String[] callerPackages = context().getPackageManager().getPackagesForUid(caller);
    370         if (callerPackages == null || callerPackages.length == 0) {
    371             return null;
    372         }
    373         if (callerPackages.length == 1) {
    374             return callerPackages[0];
    375         }
    376         // If we have more than one caller package, which is very unlikely, let's return the one
    377         // with the highest permissions. If more than one has the same permission, we don't care
    378         // which one we return.
    379         String bestSoFar = callerPackages[0];
    380         for (String callerPackage : callerPackages) {
    381             if (mVoicemailPermissions.packageHasFullAccess(callerPackage)) {
    382                 // Full always wins, we can return early.
    383                 return callerPackage;
    384             }
    385             if (mVoicemailPermissions.packageHasOwnVoicemailAccess(callerPackage)) {
    386                 bestSoFar = callerPackage;
    387             }
    388         }
    389         return bestSoFar;
    390     }
    391 
    392     /**
    393      * Creates a clause to restrict the selection to the calling provider or null if the caller has
    394      * access to all data.
    395      */
    396     private String getPackageRestrictionClause() {
    397         if (mVoicemailPermissions.callerHasFullAccess()) {
    398             return null;
    399         }
    400         return getEqualityClause(Voicemails.SOURCE_PACKAGE, getCallingPackage());
    401     }
    402 }
    403