1 /*
2  * Copyright 2021 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 android.app.appsearch.testutil;
18 
19 import android.annotation.NonNull;
20 import android.app.appsearch.observer.DocumentChangeInfo;
21 import android.app.appsearch.observer.ObserverCallback;
22 import android.app.appsearch.observer.SchemaChangeInfo;
23 
24 import com.android.internal.annotations.GuardedBy;
25 
26 import java.util.ArrayList;
27 import java.util.List;
28 
29 /**
30  * An implementation of {@link android.app.appsearch.observer.ObserverCallback} for testing that
31  * caches its notifications in memory.
32  *
33  * <p>You should wait for all notifications to be delivered using {@link #waitForNotificationCount}
34  * before using the public lists to avoid concurrency issues.
35  *
36  * @hide
37  */
38 public class TestObserverCallback implements ObserverCallback {
39     private final Object mLock = new Object();
40 
41     private final List<SchemaChangeInfo> mSchemaChanges = new ArrayList<>();
42     private final List<DocumentChangeInfo> mDocumentChanges = new ArrayList<>();
43 
44     @GuardedBy("mLock")
45     private int mNotificationCountLocked = 0;
46 
47     @Override
onSchemaChanged(@onNull SchemaChangeInfo changeInfo)48     public void onSchemaChanged(@NonNull SchemaChangeInfo changeInfo) {
49         synchronized (mLock) {
50             mSchemaChanges.add(changeInfo);
51             incrementNotificationCountLocked();
52         }
53     }
54 
55     @Override
onDocumentChanged(@onNull DocumentChangeInfo changeInfo)56     public void onDocumentChanged(@NonNull DocumentChangeInfo changeInfo) {
57         synchronized (mLock) {
58             mDocumentChanges.add(changeInfo);
59             incrementNotificationCountLocked();
60         }
61     }
62 
63     /**
64      * Waits for the total number of notifications this observer has seen to be equal to {@code
65      * expectedCount}.
66      *
67      * <p>If the number of cumulative received notifications is currently less than {@code
68      * expectedCount}, this method will block.
69      *
70      * @throws IllegalStateException If the current count of received notifications is currently
71      *     greater than {@code expectedCount}.
72      */
waitForNotificationCount(int expectedCount)73     public void waitForNotificationCount(int expectedCount) throws InterruptedException {
74         while (true) {
75             synchronized (mLock) {
76                 int actualCount = mNotificationCountLocked;
77                 if (actualCount > expectedCount) {
78                     throw new IllegalStateException(
79                             "Caller was waiting for "
80                                     + expectedCount
81                                     + " notifications but there are"
82                                     + " already "
83                                     + actualCount
84                                     + ".\n  Schema changes: "
85                                     + mSchemaChanges
86                                     + "\n  Document changes: "
87                                     + mDocumentChanges);
88                 } else if (actualCount == expectedCount) {
89                     return;
90                 } else {
91                     mLock.wait();
92                 }
93             }
94         }
95     }
96 
97     /** Gets all schema changes that have been observed so far. */
98     // Note: although these are lists, their order is arbitrary and depends on the order in which
99     // the executor provided to GlobalSearchSessionShim#adObserver dispatches the notifications.
100     // Therefore they are declared as iterables instead of lists, to reduce the risk that they will
101     // be inspected by index.
102     @NonNull
getSchemaChanges()103     public Iterable<SchemaChangeInfo> getSchemaChanges() {
104         return mSchemaChanges;
105     }
106 
107     /** Gets all document changes that have been observed so far. */
108     // Note: although these are lists, their order is arbitrary and depends on the order in which
109     // the executor provided to GlobalSearchSessionShim#adObserver dispatches the notifications.
110     // Therefore they are declared as iterables instead of lists, to reduce the risk that they will
111     // be inspected by index.
112     @NonNull
getDocumentChanges()113     public Iterable<DocumentChangeInfo> getDocumentChanges() {
114         return mDocumentChanges;
115     }
116 
117     /** Removes all notifications captured by this callback and resets the count to 0. */
clear()118     public void clear() {
119         synchronized (mLock) {
120             mSchemaChanges.clear();
121             mDocumentChanges.clear();
122             mNotificationCountLocked = 0;
123             mLock.notifyAll();
124         }
125     }
126 
incrementNotificationCountLocked()127     private void incrementNotificationCountLocked() {
128         synchronized (mLock) {
129             mNotificationCountLocked++;
130             mLock.notifyAll();
131         }
132     }
133 }
134