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