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