1 /* 2 * Copyright (C) 2010 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 17 package com.android.contacts.test.mocks; 18 19 import android.content.ContentValues; 20 import android.content.UriMatcher; 21 import android.database.Cursor; 22 import android.database.MatrixCursor; 23 import android.net.Uri; 24 import androidx.annotation.Nullable; 25 26 import com.google.common.base.Preconditions; 27 import com.google.common.collect.Maps; 28 29 import junit.framework.Assert; 30 31 import java.util.ArrayList; 32 import java.util.Arrays; 33 import java.util.Iterator; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Objects; 37 38 /** 39 * A programmable mock content provider. 40 */ 41 public class MockContentProvider extends android.test.mock.MockContentProvider { 42 private static final String TAG = "MockContentProvider"; 43 44 public static class Query { 45 46 private final Uri mUri; 47 private UriMatcher mMatcher; 48 49 private String[] mProjection; 50 private String[] mDefaultProjection; 51 private String mSelection; 52 private String[] mSelectionArgs; 53 private String mSortOrder; 54 private List<Object> mRows = new ArrayList<>(); 55 private boolean mAnyProjection; 56 private boolean mAnySelection; 57 private boolean mAnySortOrder; 58 private boolean mAnyNumberOfTimes; 59 60 private boolean mExecuted; 61 Query()62 private Query() { 63 mUri = null; 64 } 65 Query(UriMatcher matcher)66 private Query(UriMatcher matcher) { 67 mUri = null; 68 mMatcher = matcher; 69 } 70 Query(Uri uri)71 public Query(Uri uri) { 72 mUri = uri; 73 } 74 75 @Override toString()76 public String toString() { 77 return queryToString(mUri, mProjection, mSelection, mSelectionArgs, mSortOrder); 78 } 79 withProjection(String... projection)80 public Query withProjection(String... projection) { 81 mProjection = projection; 82 return this; 83 } 84 withDefaultProjection(String... projection)85 public Query withDefaultProjection(String... projection) { 86 mDefaultProjection = projection; 87 return this; 88 } 89 withAnyProjection()90 public Query withAnyProjection() { 91 mAnyProjection = true; 92 return this; 93 } 94 withSelection(String selection, String... selectionArgs)95 public Query withSelection(String selection, String... selectionArgs) { 96 mSelection = selection; 97 mSelectionArgs = selectionArgs; 98 return this; 99 } 100 withAnySelection()101 public Query withAnySelection() { 102 mAnySelection = true; 103 return this; 104 } 105 withSortOrder(String sortOrder)106 public Query withSortOrder(String sortOrder) { 107 mSortOrder = sortOrder; 108 return this; 109 } 110 withAnySortOrder()111 public Query withAnySortOrder() { 112 mAnySortOrder = true; 113 return this; 114 } 115 returnRow(ContentValues values)116 public Query returnRow(ContentValues values) { 117 mRows.add(values); 118 return this; 119 } 120 returnRow(Object... row)121 public Query returnRow(Object... row) { 122 mRows.add(row); 123 return this; 124 } 125 returnEmptyCursor()126 public Query returnEmptyCursor() { 127 mRows.clear(); 128 return this; 129 } 130 anyNumberOfTimes()131 public Query anyNumberOfTimes() { 132 mAnyNumberOfTimes = true; 133 return this; 134 } 135 equals(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)136 public boolean equals(Uri uri, String[] projection, String selection, 137 String[] selectionArgs, String sortOrder) { 138 if (mUri == null) { 139 if (mMatcher != null && mMatcher.match(uri) == UriMatcher.NO_MATCH) { 140 return false; 141 } 142 } else if (!uri.equals(mUri)) { 143 return false; 144 } 145 146 if (!mAnyProjection && !Arrays.equals(projection, mProjection)) { 147 return false; 148 } 149 150 if (!mAnySelection && !Objects.equals(selection, mSelection)) { 151 return false; 152 } 153 154 if (!mAnySelection && !Arrays.equals(selectionArgs, mSelectionArgs)) { 155 return false; 156 } 157 158 if (!mAnySortOrder && !Objects.equals(sortOrder, mSortOrder)) { 159 return false; 160 } 161 162 return true; 163 } 164 getResult(String[] projection)165 public Cursor getResult(String[] projection) { 166 String[] columnNames; 167 if (mAnyProjection) { 168 columnNames = projection != null ? projection : mDefaultProjection; 169 } else { 170 columnNames = mProjection != null ? mProjection : mDefaultProjection; 171 } 172 173 MatrixCursor cursor = new MatrixCursor(columnNames); 174 for (Object row : mRows) { 175 if (row instanceof Object[]) { 176 cursor.addRow((Object[]) row); 177 } else { 178 ContentValues values = (ContentValues) row; 179 Object[] columns = new Object[columnNames.length]; 180 for (int i = 0; i < columnNames.length; i++) { 181 columns[i] = values.get(columnNames[i]); 182 } 183 cursor.addRow(columns); 184 } 185 } 186 return cursor; 187 } 188 forAnyUri()189 public static Query forAnyUri() { 190 return new Query(); 191 } 192 forUrisMatching(UriMatcher matcher)193 public static Query forUrisMatching(UriMatcher matcher) { 194 return new Query(matcher); 195 } 196 forUrisMatching(String authority, String... paths)197 public static Query forUrisMatching(String authority, String... paths) { 198 final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); 199 for (int i = 0; i < paths.length; i++) { 200 matcher.addURI(authority, paths[i], i); 201 } 202 return new Query(matcher); 203 } 204 205 } 206 207 public static class TypeQuery { 208 private final Uri mUri; 209 private final String mType; 210 TypeQuery(Uri uri, String type)211 public TypeQuery(Uri uri, String type) { 212 mUri = uri; 213 mType = type; 214 } 215 getUri()216 public Uri getUri() { 217 return mUri; 218 } 219 getType()220 public String getType() { 221 return mType; 222 } 223 224 @Override toString()225 public String toString() { 226 return mUri + " --> " + mType; 227 } 228 equals(Uri uri)229 public boolean equals(Uri uri) { 230 return getUri().equals(uri); 231 } 232 } 233 234 public static class Insert { 235 private final Uri mUri; 236 private final ContentValues mContentValues; 237 private final Uri mResultUri; 238 private boolean mAnyNumberOfTimes; 239 private boolean mIsExecuted; 240 241 /** 242 * Creates a new Insert to expect. 243 * 244 * @param uri the uri of the insertion request. 245 * @param contentValues the ContentValues to insert. 246 * @param resultUri the {@link Uri} for the newly inserted item. 247 * @throws NullPointerException if any parameter is {@code null}. 248 */ Insert(Uri uri, ContentValues contentValues, Uri resultUri)249 public Insert(Uri uri, ContentValues contentValues, Uri resultUri) { 250 mUri = Preconditions.checkNotNull(uri); 251 mContentValues = Preconditions.checkNotNull(contentValues); 252 mResultUri = Preconditions.checkNotNull(resultUri); 253 } 254 255 /** 256 * Causes this insert expectation to be useable for mutliple calls to insert, rather than 257 * just one. 258 * 259 * @return this 260 */ anyNumberOfTimes()261 public Insert anyNumberOfTimes() { 262 mAnyNumberOfTimes = true; 263 return this; 264 } 265 equals(Uri uri, ContentValues contentValues)266 private boolean equals(Uri uri, ContentValues contentValues) { 267 return mUri.equals(uri) && mContentValues.equals(contentValues); 268 } 269 270 @Override equals(Object o)271 public boolean equals(Object o) { 272 if (this == o) { 273 return true; 274 } 275 if (o == null || getClass() != o.getClass()) { 276 return false; 277 } 278 Insert insert = (Insert) o; 279 return mAnyNumberOfTimes == insert.mAnyNumberOfTimes && 280 mIsExecuted == insert.mIsExecuted && 281 Objects.equals(mUri, insert.mUri) && 282 Objects.equals(mContentValues, insert.mContentValues) && 283 Objects.equals(mResultUri, insert.mResultUri); 284 } 285 286 @Override hashCode()287 public int hashCode() { 288 return Objects.hash(mUri, mContentValues, mResultUri, mAnyNumberOfTimes, mIsExecuted); 289 } 290 291 @Override toString()292 public String toString() { 293 return "Insert{" + 294 "mUri=" + mUri + 295 ", mContentValues=" + mContentValues + 296 ", mResultUri=" + mResultUri + 297 ", mAnyNumberOfTimes=" + mAnyNumberOfTimes + 298 ", mIsExecuted=" + mIsExecuted + 299 '}'; 300 } 301 } 302 303 public static class Delete { 304 private final Uri mUri; 305 306 private boolean mAnyNumberOfTimes; 307 private boolean mAnySelection; 308 @Nullable private String mSelection; 309 @Nullable private String[] mSelectionArgs; 310 private boolean mIsExecuted; 311 private int mRowsAffected; 312 313 /** 314 * Creates a new Delete to expect. 315 * @param uri the uri of the delete request. 316 * @throws NullPointerException if uri is {@code null}. 317 */ Delete(Uri uri)318 public Delete(Uri uri) { 319 mUri = Preconditions.checkNotNull(uri); 320 } 321 322 /** 323 * Sets the given information as expected selection arguments. 324 * 325 * @param selection The selection to expect. 326 * @param selectionArgs The selection args to expect. 327 * @return this. 328 */ withSelection(String selection, @Nullable String[] selectionArgs)329 public Delete withSelection(String selection, @Nullable String[] selectionArgs) { 330 mSelection = Preconditions.checkNotNull(selection); 331 mSelectionArgs = selectionArgs; 332 mAnySelection = false; 333 return this; 334 } 335 336 /** 337 * Sets this delete to expect any selection arguments. 338 * 339 * @return this. 340 */ withAnySelection()341 public Delete withAnySelection() { 342 mAnySelection = true; 343 return this; 344 } 345 346 /** 347 * Sets this delete to return the given number of rows affected. 348 * 349 * @param rowsAffected The value to return when this expected delete is executed. 350 * @return this. 351 */ returnRowsAffected(int rowsAffected)352 public Delete returnRowsAffected(int rowsAffected) { 353 mRowsAffected = rowsAffected; 354 return this; 355 } 356 357 /** 358 * Causes this delete expectation to be useable for multiple calls to delete, rather than 359 * just one. 360 * 361 * @return this. 362 */ anyNumberOfTimes()363 public Delete anyNumberOfTimes() { 364 mAnyNumberOfTimes = true; 365 return this; 366 } 367 equals(Uri uri, String selection, String[] selectionArgs)368 private boolean equals(Uri uri, String selection, String[] selectionArgs) { 369 return mUri.equals(uri) && Objects.equals(mSelection, selection) 370 && Arrays.equals(mSelectionArgs, selectionArgs); 371 } 372 } 373 374 public static class Update { 375 private final Uri mUri; 376 private final ContentValues mContentValues; 377 @Nullable private String mSelection; 378 @Nullable private String[] mSelectionArgs; 379 private boolean mAnyNumberOfTimes; 380 private boolean mIsExecuted; 381 private int mRowsAffected; 382 383 /** 384 * Creates a new Update to expect. 385 * 386 * @param uri the uri of the update request. 387 * @param contentValues the ContentValues to update. 388 * 389 * @throws NullPointerException if any parameter is {@code null}. 390 */ Update(Uri uri, ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs)391 public Update(Uri uri, 392 ContentValues contentValues, 393 @Nullable String selection, 394 @Nullable String[] selectionArgs) { 395 mUri = Preconditions.checkNotNull(uri); 396 mContentValues = Preconditions.checkNotNull(contentValues); 397 mSelection = selection; 398 mSelectionArgs = selectionArgs; 399 } 400 401 /** 402 * Causes this update expectation to be useable for mutliple calls to update, rather than 403 * just one. 404 * 405 * @return this 406 */ anyNumberOfTimes()407 public Update anyNumberOfTimes() { 408 mAnyNumberOfTimes = true; 409 return this; 410 } 411 412 /** 413 * Sets this update to return the given number of rows affected. 414 * 415 * @param rowsAffected The value to return when this expected update is executed. 416 * @return this. 417 */ returnRowsAffected(int rowsAffected)418 public Update returnRowsAffected(int rowsAffected) { 419 mRowsAffected = rowsAffected; 420 return this; 421 } 422 equals(Uri uri, ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs)423 private boolean equals(Uri uri, 424 ContentValues contentValues, 425 @Nullable String selection, 426 @Nullable String[] selectionArgs) { 427 return mUri.equals(uri) && mContentValues.equals(contentValues) && 428 Objects.equals(mSelection, selection) && 429 Arrays.equals(mSelectionArgs, selectionArgs); 430 } 431 432 @Override equals(Object o)433 public boolean equals(Object o) { 434 if (this == o) { 435 return true; 436 } 437 if (o == null || getClass() != o.getClass()) { 438 return false; 439 } 440 Update update = (Update) o; 441 return mAnyNumberOfTimes == update.mAnyNumberOfTimes && 442 mIsExecuted == update.mIsExecuted && 443 Objects.equals(mUri, update.mUri) && 444 Objects.equals(mContentValues, update.mContentValues) && 445 Objects.equals(mSelection, update.mSelection) && 446 Arrays.equals(mSelectionArgs, update.mSelectionArgs); 447 } 448 449 @Override hashCode()450 public int hashCode() { 451 return Objects.hash(mUri, mContentValues, mAnyNumberOfTimes, mIsExecuted, mSelection, 452 Arrays.hashCode(mSelectionArgs)); 453 } 454 455 @Override toString()456 public String toString() { 457 return "Update{" + 458 "mUri=" + mUri + 459 ", mContentValues=" + mContentValues + 460 ", mAnyNumberOfTimes=" + mAnyNumberOfTimes + 461 ", mIsExecuted=" + mIsExecuted + 462 ", mSelection=" + mSelection + 463 ", mSelectionArgs=" + Arrays.toString(mSelectionArgs) + 464 '}'; 465 } 466 } 467 468 private List<Query> mExpectedQueries = new ArrayList<>(); 469 private Map<Uri, String> mExpectedTypeQueries = Maps.newHashMap(); 470 private List<Insert> mExpectedInserts = new ArrayList<>(); 471 private List<Delete> mExpectedDeletes = new ArrayList<>(); 472 private List<Update> mExpectedUpdates = new ArrayList<>(); 473 474 @Override onCreate()475 public boolean onCreate() { 476 return true; 477 } 478 expect(Query query)479 public Query expect(Query query) { 480 mExpectedQueries.add(query); 481 return query; 482 } 483 expectQuery(Uri contentUri)484 public Query expectQuery(Uri contentUri) { 485 return expect(new Query(contentUri)); 486 } 487 expectQuery(String contentUri)488 public Query expectQuery(String contentUri) { 489 return expectQuery(Uri.parse(contentUri)); 490 } 491 expectTypeQuery(Uri uri, String type)492 public void expectTypeQuery(Uri uri, String type) { 493 mExpectedTypeQueries.put(uri, type); 494 } 495 expectInsert(Uri contentUri, ContentValues contentValues, Uri resultUri)496 public void expectInsert(Uri contentUri, ContentValues contentValues, Uri resultUri) { 497 mExpectedInserts.add(new Insert(contentUri, contentValues, resultUri)); 498 } 499 expectUpdate(Uri contentUri, ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs)500 public Update expectUpdate(Uri contentUri, 501 ContentValues contentValues, 502 @Nullable String selection, 503 @Nullable String[] selectionArgs) { 504 Update update = new Update(contentUri, contentValues, selection, selectionArgs); 505 mExpectedUpdates.add(update); 506 return update; 507 } 508 expectDelete(Uri contentUri)509 public Delete expectDelete(Uri contentUri) { 510 Delete delete = new Delete(contentUri); 511 mExpectedDeletes.add(delete); 512 return delete; 513 } 514 515 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)516 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 517 String sortOrder) { 518 if (mExpectedQueries.isEmpty()) { 519 Assert.fail("Unexpected query: Actual:" 520 + queryToString(uri, projection, selection, selectionArgs, sortOrder)); 521 } 522 523 for (Iterator<Query> iterator = mExpectedQueries.iterator(); iterator.hasNext();) { 524 Query query = iterator.next(); 525 if (query.equals(uri, projection, selection, selectionArgs, sortOrder)) { 526 query.mExecuted = true; 527 if (!query.mAnyNumberOfTimes) { 528 iterator.remove(); 529 } 530 return query.getResult(projection); 531 } 532 } 533 534 Assert.fail("Incorrect query. Expected one of: " + mExpectedQueries + ". Actual: " + 535 queryToString(uri, projection, selection, selectionArgs, sortOrder)); 536 return null; 537 } 538 539 @Override getType(Uri uri)540 public String getType(Uri uri) { 541 if (mExpectedTypeQueries.isEmpty()) { 542 Assert.fail("Unexpected getType query: " + uri); 543 } 544 545 String mimeType = mExpectedTypeQueries.get(uri); 546 if (mimeType != null) { 547 return mimeType; 548 } 549 550 Assert.fail("Unknown mime type for: " + uri); 551 return null; 552 } 553 554 @Override insert(Uri uri, ContentValues values)555 public Uri insert(Uri uri, ContentValues values) { 556 if (mExpectedInserts.isEmpty()) { 557 Assert.fail("Unexpected insert. Actual: " + insertToString(uri, values)); 558 } 559 for (Iterator<Insert> iterator = mExpectedInserts.iterator(); iterator.hasNext(); ) { 560 Insert insert = iterator.next(); 561 if (insert.equals(uri, values)) { 562 insert.mIsExecuted = true; 563 if (!insert.mAnyNumberOfTimes) { 564 iterator.remove(); 565 } 566 return insert.mResultUri; 567 } 568 } 569 570 Assert.fail("Incorrect insert. Expected one of: " + mExpectedInserts + ". Actual: " 571 + insertToString(uri, values)); 572 return null; 573 } 574 insertToString(Uri uri, ContentValues contentValues)575 private String insertToString(Uri uri, ContentValues contentValues) { 576 return "Insert { uri=" + uri + ", contentValues=" + contentValues + '}'; 577 } 578 579 @Override update(Uri uri, ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs)580 public int update(Uri uri, 581 ContentValues values, 582 @Nullable String selection, 583 @Nullable String[] selectionArgs) { 584 if (mExpectedUpdates.isEmpty()) { 585 Assert.fail("Unexpected update. Actual: " 586 + updateToString(uri, values, selection, selectionArgs)); 587 } 588 for (Iterator<Update> iterator = mExpectedUpdates.iterator(); iterator.hasNext(); ) { 589 Update update = iterator.next(); 590 if (update.equals(uri, values, selection, selectionArgs)) { 591 update.mIsExecuted = true; 592 if (!update.mAnyNumberOfTimes) { 593 iterator.remove(); 594 } 595 return update.mRowsAffected; 596 } 597 } 598 599 Assert.fail("Incorrect update. Expected one of: " + mExpectedUpdates + ". Actual: " 600 + updateToString(uri, values, selection, selectionArgs)); 601 return - 1; 602 } 603 updateToString(Uri uri, ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs)604 private String updateToString(Uri uri, 605 ContentValues contentValues, 606 @Nullable String selection, 607 @Nullable String[] selectionArgs) { 608 return "Update { uri=" + uri + ", contentValues=" + contentValues + ", selection=" + 609 selection + ", selectionArgs" + Arrays.toString(selectionArgs) + '}'; 610 } 611 612 @Override delete(Uri uri, String selection, String[] selectionArgs)613 public int delete(Uri uri, String selection, String[] selectionArgs) { 614 if (mExpectedDeletes.isEmpty()) { 615 Assert.fail("Unexpected delete. Actual: " + deleteToString(uri, selection, 616 selectionArgs)); 617 } 618 for (Iterator<Delete> iterator = mExpectedDeletes.iterator(); iterator.hasNext(); ) { 619 Delete delete = iterator.next(); 620 if (delete.equals(uri, selection, selectionArgs)) { 621 delete.mIsExecuted = true; 622 if (!delete.mAnyNumberOfTimes) { 623 iterator.remove(); 624 } 625 return delete.mRowsAffected; 626 } 627 } 628 Assert.fail("Incorrect delete. Expected one of: " + mExpectedDeletes + ". Actual: " 629 + deleteToString(uri, selection, selectionArgs)); 630 return -1; 631 } 632 deleteToString(Uri uri, String selection, String[] selectionArgs)633 private String deleteToString(Uri uri, String selection, String[] selectionArgs) { 634 return "Delete { uri=" + uri + ", selection=" + selection + ", selectionArgs" 635 + Arrays.toString(selectionArgs) + '}'; 636 } 637 queryToString(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)638 private static String queryToString(Uri uri, String[] projection, String selection, 639 String[] selectionArgs, String sortOrder) { 640 StringBuilder sb = new StringBuilder(); 641 sb.append(uri == null ? "<Any Uri>" : uri).append(" "); 642 if (projection != null) { 643 sb.append(Arrays.toString(projection)); 644 } else { 645 sb.append("[]"); 646 } 647 if (selection != null) { 648 sb.append(" selection: '").append(selection).append("'"); 649 if (selectionArgs != null) { 650 sb.append(Arrays.toString(selectionArgs)); 651 } else { 652 sb.append("[]"); 653 } 654 } 655 if (sortOrder != null) { 656 sb.append(" sort: '").append(sortOrder).append("'"); 657 } 658 return sb.toString(); 659 } 660 verify()661 public void verify() { 662 verifyQueries(); 663 verifyInserts(); 664 verifyDeletes(); 665 } 666 verifyQueries()667 private void verifyQueries() { 668 List<Query> missedQueries = new ArrayList<>(); 669 for (Query query : mExpectedQueries) { 670 if (!query.mExecuted) { 671 missedQueries.add(query); 672 } 673 } 674 Assert.assertTrue("Not all expected queries have been called: " + missedQueries, 675 missedQueries.isEmpty()); 676 } 677 verifyInserts()678 private void verifyInserts() { 679 List<Insert> missedInserts = new ArrayList<>(); 680 for (Insert insert : mExpectedInserts) { 681 if (!insert.mIsExecuted) { 682 missedInserts.add(insert); 683 } 684 } 685 Assert.assertTrue("Not all expected inserts have been called: " + missedInserts, 686 missedInserts.isEmpty()); 687 } 688 verifyDeletes()689 private void verifyDeletes() { 690 List<Delete> missedDeletes = new ArrayList<>(); 691 for (Delete delete : mExpectedDeletes) { 692 if (!delete.mIsExecuted) { 693 missedDeletes.add(delete); 694 } 695 } 696 Assert.assertTrue("Not all expected deletes have been called: " + missedDeletes, 697 missedDeletes.isEmpty()); 698 } 699 } 700