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