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 20 import static com.android.providers.contacts.util.DbQueryUtils.concatenateClauses; 21 import static com.android.providers.contacts.util.DbQueryUtils.getEqualityClause; 22 23 import android.app.AppOpsManager; 24 import android.content.ContentProvider; 25 import android.content.ContentResolver; 26 import android.content.ContentValues; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.database.Cursor; 30 import android.net.Uri; 31 import android.os.Binder; 32 import android.os.ParcelFileDescriptor; 33 import android.provider.BaseColumns; 34 import android.provider.VoicemailContract; 35 import android.provider.VoicemailContract.Status; 36 import android.provider.VoicemailContract.Voicemails; 37 import android.util.ArraySet; 38 import android.util.Log; 39 40 import com.android.providers.contacts.CallLogDatabaseHelper.Tables; 41 import com.android.providers.contacts.util.ContactsPermissions; 42 import com.android.providers.contacts.util.PackageUtils; 43 import com.android.providers.contacts.util.SelectionBuilder; 44 import com.android.providers.contacts.util.TypedUriMatcherImpl; 45 import com.android.providers.contacts.util.UserUtils; 46 47 import com.google.common.annotations.VisibleForTesting; 48 49 import java.io.FileNotFoundException; 50 import java.util.Arrays; 51 import java.util.List; 52 import java.util.Set; 53 54 /** 55 * An implementation of the Voicemail content provider. This class in the entry point for both 56 * voicemail content ('calls') table and 'voicemail_status' table. This class performs all common 57 * permission checks and then delegates database level operations to respective table delegate 58 * objects. 59 */ 60 public class VoicemailContentProvider extends ContentProvider 61 implements VoicemailTable.DelegateHelper { 62 private static final String TAG = "VoicemailProvider"; 63 64 public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE); 65 66 private static final int BACKGROUND_TASK_SCAN_STALE_PACKAGES = 0; 67 68 private ContactsTaskScheduler mTaskScheduler; 69 70 private VoicemailPermissions mVoicemailPermissions; 71 private VoicemailTable.Delegate mVoicemailContentTable; 72 private VoicemailTable.Delegate mVoicemailStatusTable; 73 74 @Override onCreate()75 public boolean onCreate() { 76 if (VERBOSE_LOGGING) { 77 Log.v(TAG, "onCreate: " + this.getClass().getSimpleName() 78 + " user=" + android.os.Process.myUserHandle().getIdentifier()); 79 } 80 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.INFO)) { 81 Log.i(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate start"); 82 } 83 Context context = context(); 84 85 // ADD_VOICEMAIL permission guards read and write. We do the same with app ops. 86 // The permission name doesn't reflect its function but we cannot rename it. 87 setAppOps(AppOpsManager.OP_ADD_VOICEMAIL, AppOpsManager.OP_ADD_VOICEMAIL); 88 89 mVoicemailPermissions = new VoicemailPermissions(context); 90 mVoicemailContentTable = new VoicemailContentTable(Tables.CALLS, context, 91 getDatabaseHelper(context), this, createCallLogInsertionHelper(context)); 92 mVoicemailStatusTable = new VoicemailStatusTable(Tables.VOICEMAIL_STATUS, context, 93 getDatabaseHelper(context), this); 94 95 mTaskScheduler = new ContactsTaskScheduler(getClass().getSimpleName()) { 96 @Override 97 public void onPerformTask(int taskId, Object arg) { 98 performBackgroundTask(taskId, arg); 99 } 100 }; 101 102 scheduleScanStalePackages(); 103 104 ContactsPackageMonitor.start(getContext()); 105 106 if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.INFO)) { 107 Log.i(Constants.PERFORMANCE_TAG, "VoicemailContentProvider.onCreate finish"); 108 } 109 return true; 110 } 111 112 @VisibleForTesting scheduleScanStalePackages()113 void scheduleScanStalePackages() { 114 scheduleTask(BACKGROUND_TASK_SCAN_STALE_PACKAGES, null); 115 } 116 117 @VisibleForTesting scheduleTask(int taskId, Object arg)118 void scheduleTask(int taskId, Object arg) { 119 mTaskScheduler.scheduleTask(taskId, arg); 120 } 121 122 @VisibleForTesting createCallLogInsertionHelper(Context context)123 /*package*/ CallLogInsertionHelper createCallLogInsertionHelper(Context context) { 124 return DefaultCallLogInsertionHelper.getInstance(context); 125 } 126 127 @VisibleForTesting getDatabaseHelper(Context context)128 /*package*/ CallLogDatabaseHelper getDatabaseHelper(Context context) { 129 return CallLogDatabaseHelper.getInstance(context); 130 } 131 132 @VisibleForTesting context()133 /*package*/ Context context() { 134 return getContext(); 135 } 136 137 @Override getType(Uri uri)138 public String getType(Uri uri) { 139 UriData uriData = null; 140 try { 141 uriData = UriData.createUriData(uri); 142 } catch (IllegalArgumentException ignored) { 143 // Special case: for illegal URIs, we return null rather than thrown an exception. 144 return null; 145 } 146 return getTableDelegate(uriData).getType(uriData); 147 } 148 149 @Override insert(Uri uri, ContentValues values)150 public Uri insert(Uri uri, ContentValues values) { 151 if (VERBOSE_LOGGING) { 152 Log.v(TAG, "insert: uri=" + uri + " values=[" + values + "]" + 153 " CPID=" + Binder.getCallingPid()); 154 } 155 UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values); 156 return getTableDelegate(uriData).insert(uriData, values); 157 } 158 159 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)160 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 161 String sortOrder) { 162 if (VERBOSE_LOGGING) { 163 Log.v(TAG, "query: uri=" + uri + " projection=" + Arrays.toString(projection) + 164 " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + 165 " order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() + 166 " User=" + UserUtils.getCurrentUserHandle(getContext())); 167 } 168 UriData uriData = checkPermissionsAndCreateUriDataForRead(uri); 169 SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 170 selectionBuilder.addClause(getPackageRestrictionClause(true/*isQuery*/)); 171 return getTableDelegate(uriData).query(uriData, projection, selectionBuilder.build(), 172 selectionArgs, sortOrder); 173 } 174 175 @Override update(Uri uri, ContentValues values, String selection, String[] selectionArgs)176 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 177 if (VERBOSE_LOGGING) { 178 Log.v(TAG, "update: uri=" + uri + 179 " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + 180 " values=[" + values + "] CPID=" + Binder.getCallingPid() + 181 " User=" + UserUtils.getCurrentUserHandle(getContext())); 182 } 183 UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri, values); 184 SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 185 selectionBuilder.addClause(getPackageRestrictionClause(false/*isQuery*/)); 186 return getTableDelegate(uriData).update(uriData, values, selectionBuilder.build(), 187 selectionArgs); 188 } 189 190 @Override delete(Uri uri, String selection, String[] selectionArgs)191 public int delete(Uri uri, String selection, String[] selectionArgs) { 192 if (VERBOSE_LOGGING) { 193 Log.v(TAG, "delete: uri=" + uri + 194 " selection=[" + selection + "] args=" + Arrays.toString(selectionArgs) + 195 " CPID=" + Binder.getCallingPid() + 196 " User=" + UserUtils.getCurrentUserHandle(getContext())); 197 } 198 UriData uriData = checkPermissionsAndCreateUriDataForWrite(uri); 199 SelectionBuilder selectionBuilder = new SelectionBuilder(selection); 200 selectionBuilder.addClause(getPackageRestrictionClause(false/*isQuery*/)); 201 return getTableDelegate(uriData).delete(uriData, selectionBuilder.build(), selectionArgs); 202 } 203 204 @Override openFile(Uri uri, String mode)205 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 206 boolean success = false; 207 try { 208 UriData uriData = null; 209 if (mode.equals("r")) { 210 uriData = checkPermissionsAndCreateUriDataForRead(uri); 211 } else { 212 uriData = checkPermissionsAndCreateUriDataForWrite(uri); 213 } 214 // openFileHelper() relies on "_data" column to be populated with the file path. 215 final ParcelFileDescriptor ret = getTableDelegate(uriData).openFile(uriData, mode); 216 success = true; 217 return ret; 218 } finally { 219 if (VERBOSE_LOGGING) { 220 Log.v(TAG, "openFile uri=" + uri + " mode=" + mode + " success=" + success + 221 " CPID=" + Binder.getCallingPid() + 222 " User=" + UserUtils.getCurrentUserHandle(getContext())); 223 } 224 } 225 } 226 227 /** Returns the correct table delegate object that can handle this URI. */ getTableDelegate(UriData uriData)228 private VoicemailTable.Delegate getTableDelegate(UriData uriData) { 229 switch (uriData.getUriType()) { 230 case STATUS: 231 case STATUS_ID: 232 return mVoicemailStatusTable; 233 case VOICEMAILS: 234 case VOICEMAILS_ID: 235 return mVoicemailContentTable; 236 case NO_MATCH: 237 throw new IllegalStateException("Invalid uri type for uri: " + uriData.getUri()); 238 default: 239 throw new IllegalStateException("Impossible, all cases are covered."); 240 } 241 } 242 243 /** 244 * Decorates a URI by providing methods to get various properties from the URI. 245 */ 246 public static class UriData { 247 private final Uri mUri; 248 private final String mId; 249 private final String mSourcePackage; 250 private final VoicemailUriType mUriType; 251 UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage)252 private UriData(Uri uri, VoicemailUriType uriType, String id, String sourcePackage) { 253 mUriType = uriType; 254 mUri = uri; 255 mId = id; 256 mSourcePackage = sourcePackage; 257 } 258 259 /** Gets the original URI to which this {@link UriData} corresponds. */ getUri()260 public final Uri getUri() { 261 return mUri; 262 } 263 264 /** Tells us if our URI has an individual voicemail id. */ hasId()265 public final boolean hasId() { 266 return mId != null; 267 } 268 269 /** Gets the ID for the voicemail. */ getId()270 public final String getId() { 271 return mId; 272 } 273 274 /** Tells us if our URI has a source package string. */ hasSourcePackage()275 public final boolean hasSourcePackage() { 276 return mSourcePackage != null; 277 } 278 279 /** Gets the source package. */ getSourcePackage()280 public final String getSourcePackage() { 281 return mSourcePackage; 282 } 283 284 /** Gets the Voicemail URI type. */ getUriType()285 public final VoicemailUriType getUriType() { 286 return mUriType; 287 } 288 289 /** Builds a where clause from the URI data. */ getWhereClause()290 public final String getWhereClause() { 291 return concatenateClauses( 292 (hasId() ? getEqualityClause(BaseColumns._ID, getId()) : null), 293 (hasSourcePackage() ? getEqualityClause(SOURCE_PACKAGE_FIELD, 294 getSourcePackage()) : null)); 295 } 296 297 /** Create a {@link UriData} corresponding to a given uri. */ createUriData(Uri uri)298 public static UriData createUriData(Uri uri) { 299 String sourcePackage = uri.getQueryParameter( 300 VoicemailContract.PARAM_KEY_SOURCE_PACKAGE); 301 List<String> segments = uri.getPathSegments(); 302 VoicemailUriType uriType = createUriMatcher().match(uri); 303 switch (uriType) { 304 case VOICEMAILS: 305 case STATUS: 306 return new UriData(uri, uriType, null, sourcePackage); 307 case VOICEMAILS_ID: 308 case STATUS_ID: 309 return new UriData(uri, uriType, segments.get(1), sourcePackage); 310 case NO_MATCH: 311 throw new IllegalArgumentException("Invalid URI: " + uri); 312 default: 313 throw new IllegalStateException("Impossible, all cases are covered"); 314 } 315 } 316 createUriMatcher()317 private static TypedUriMatcherImpl<VoicemailUriType> createUriMatcher() { 318 return new TypedUriMatcherImpl<VoicemailUriType>( 319 VoicemailContract.AUTHORITY, VoicemailUriType.values()); 320 } 321 } 322 323 @Override 324 // VoicemailTable.DelegateHelper interface. checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values)325 public void checkAndAddSourcePackageIntoValues(UriData uriData, ContentValues values) { 326 // If content values don't contain the provider, calculate the right provider to use. 327 if (!values.containsKey(SOURCE_PACKAGE_FIELD)) { 328 String provider = uriData.hasSourcePackage() ? 329 uriData.getSourcePackage() : getInjectedCallingPackage(); 330 values.put(SOURCE_PACKAGE_FIELD, provider); 331 } 332 333 // You must have access to the provider given in values. 334 if (!mVoicemailPermissions.callerHasWriteAccess(getCallingPackage())) { 335 checkPackagesMatch(getInjectedCallingPackage(), 336 values.getAsString(VoicemailContract.SOURCE_PACKAGE_FIELD), 337 uriData.getUri()); 338 } 339 } 340 341 /** 342 * Checks that the source_package field is same in uriData and ContentValues, if it happens 343 * to be set in both. 344 */ checkSourcePackageSameIfSet(UriData uriData, ContentValues values)345 private void checkSourcePackageSameIfSet(UriData uriData, ContentValues values) { 346 if (uriData.hasSourcePackage() && values.containsKey(SOURCE_PACKAGE_FIELD)) { 347 if (!uriData.getSourcePackage().equals(values.get(SOURCE_PACKAGE_FIELD))) { 348 throw new SecurityException( 349 "source_package in URI was " + uriData.getSourcePackage() + 350 " but doesn't match source_package in ContentValues which was " 351 + values.get(SOURCE_PACKAGE_FIELD)); 352 } 353 } 354 } 355 356 @Override 357 /** Implementation of {@link VoicemailTable.DelegateHelper#openDataFile(UriData, String)} */ openDataFile(UriData uriData, String mode)358 public ParcelFileDescriptor openDataFile(UriData uriData, String mode) 359 throws FileNotFoundException { 360 return openFileHelper(uriData.getUri(), mode); 361 } 362 363 /** 364 * Ensures that the caller has the permissions to perform a query/read operation, and 365 * then returns the structured representation {@link UriData} of the supplied uri. 366 */ checkPermissionsAndCreateUriDataForRead(Uri uri)367 private UriData checkPermissionsAndCreateUriDataForRead(Uri uri) { 368 // If the caller has been explicitly granted read permission to this URI then no need to 369 // check further. 370 if (ContactsPermissions.hasCallerUriPermission( 371 getContext(), uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)) { 372 return UriData.createUriData(uri); 373 } 374 375 if (mVoicemailPermissions.callerHasReadAccess(getCallingPackage())) { 376 return UriData.createUriData(uri); 377 } 378 379 return checkPermissionsAndCreateUriData(uri, true); 380 } 381 382 /** 383 * Performs necessary voicemail permission checks common to all operations and returns 384 * the structured representation, {@link UriData}, of the supplied uri. 385 */ checkPermissionsAndCreateUriData(Uri uri, boolean read)386 private UriData checkPermissionsAndCreateUriData(Uri uri, boolean read) { 387 UriData uriData = UriData.createUriData(uri); 388 if (!hasReadWritePermission(read)) { 389 mVoicemailPermissions.checkCallerHasOwnVoicemailAccess(); 390 checkPackagePermission(uriData); 391 } 392 return uriData; 393 } 394 395 /** 396 * Ensures that the caller has the permissions to perform an update/delete operation, and 397 * then returns the structured representation {@link UriData} of the supplied uri. 398 * Also does a permission check on the ContentValues. 399 */ checkPermissionsAndCreateUriDataForWrite(Uri uri, ContentValues... valuesArray)400 private UriData checkPermissionsAndCreateUriDataForWrite(Uri uri, ContentValues... valuesArray) { 401 UriData uriData = checkPermissionsAndCreateUriData(uri, false); 402 for (ContentValues values : valuesArray) { 403 checkSourcePackageSameIfSet(uriData, values); 404 } 405 return uriData; 406 } 407 408 /** 409 * Checks that the callingPackage is same as voicemailSourcePackage. Throws {@link 410 * SecurityException} if they don't match. 411 */ checkPackagesMatch(String callingPackage, String voicemailSourcePackage, Uri uri)412 private final void checkPackagesMatch(String callingPackage, String voicemailSourcePackage, 413 Uri uri) { 414 if (!voicemailSourcePackage.equals(callingPackage)) { 415 String errorMsg = String.format("Permission denied for URI: %s\n. " + 416 "Package %s cannot perform this operation for %s. Requires %s permission.", 417 uri, callingPackage, voicemailSourcePackage, 418 android.Manifest.permission.WRITE_VOICEMAIL); 419 throw new SecurityException(errorMsg); 420 } 421 } 422 423 /** 424 * Checks that either the caller has the MANAGE_VOICEMAIL permission, 425 * or has the ADD_VOICEMAIL permission and is using a URI that matches 426 * /voicemail/?source_package=[source-package] where [source-package] is the same as the calling 427 * package. 428 * 429 * @throws SecurityException if the check fails. 430 */ checkPackagePermission(UriData uriData)431 private void checkPackagePermission(UriData uriData) { 432 if (!mVoicemailPermissions.callerHasWriteAccess(getCallingPackage())) { 433 if (!uriData.hasSourcePackage()) { 434 // You cannot have a match if this is not a provider URI. 435 throw new SecurityException(String.format( 436 "Provider %s does not have %s permission." + 437 "\nPlease set query parameter '%s' in the URI.\nURI: %s", 438 getInjectedCallingPackage(), android.Manifest.permission.WRITE_VOICEMAIL, 439 VoicemailContract.PARAM_KEY_SOURCE_PACKAGE, uriData.getUri())); 440 } 441 checkPackagesMatch(getInjectedCallingPackage(), uriData.getSourcePackage(), 442 uriData.getUri()); 443 } 444 } 445 446 @VisibleForTesting getInjectedCallingPackage()447 String getInjectedCallingPackage() { 448 return super.getCallingPackage(); 449 } 450 451 /** 452 * Creates a clause to restrict the selection to the calling provider or null if the caller has 453 * access to all data. 454 */ getPackageRestrictionClause(boolean isQuery)455 private String getPackageRestrictionClause(boolean isQuery) { 456 if (hasReadWritePermission(isQuery)) { 457 return null; 458 } 459 return getEqualityClause(Voicemails.SOURCE_PACKAGE, getInjectedCallingPackage()); 460 } 461 462 /** 463 * Whether or not the calling package has the appropriate read/write permission. The user 464 * selected default and/or system dialers are always allowed to read and write to the 465 * VoicemailContentProvider. 466 * 467 * @param read Whether or not this operation is a read 468 * 469 * @return True if the package has the permission required to perform the read/write operation 470 */ hasReadWritePermission(boolean read)471 private boolean hasReadWritePermission(boolean read) { 472 return read ? mVoicemailPermissions.callerHasReadAccess(getCallingPackage()) : 473 mVoicemailPermissions.callerHasWriteAccess(getCallingPackage()); 474 } 475 476 /** Remove all records from a given source package. */ removeBySourcePackage(String packageName)477 public void removeBySourcePackage(String packageName) { 478 delete(Voicemails.buildSourceUri(packageName), null, null); 479 delete(Status.buildSourceUri(packageName), null, null); 480 } 481 482 @VisibleForTesting performBackgroundTask(int task, Object arg)483 void performBackgroundTask(int task, Object arg) { 484 switch (task) { 485 case BACKGROUND_TASK_SCAN_STALE_PACKAGES: 486 removeStalePackages(); 487 break; 488 } 489 } 490 491 /** 492 * Remove all records made by packages that no longer exist. 493 */ removeStalePackages()494 private void removeStalePackages() { 495 if (VERBOSE_LOGGING) { 496 Log.v(TAG, "scanStalePackages start"); 497 } 498 499 // Make sure all source tables still exists. 500 501 // First, list all source packages. 502 final ArraySet<String> packages = mVoicemailContentTable.getSourcePackages(); 503 packages.addAll(mVoicemailStatusTable.getSourcePackages()); 504 505 // Remove the ones that still exist. 506 for (int i = packages.size() - 1; i >= 0; i--) { 507 final String pkg = packages.valueAt(i); 508 final boolean installed = PackageUtils.isPackageInstalled(getContext(), pkg); 509 if (VERBOSE_LOGGING) { 510 Log.v(TAG, " " + pkg + (installed ? " installed" : " removed")); 511 } 512 if (!installed) { 513 removeBySourcePackage(pkg); 514 } 515 } 516 517 if (VERBOSE_LOGGING) { 518 Log.v(TAG, "scanStalePackages finish"); 519 } 520 } 521 } 522