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