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