1 /*
2  * Copyright 2020 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;
18 
19 import static android.app.appsearch.AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE;
20 import static android.app.appsearch.AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID;
21 import static android.app.appsearch.SearchSessionUtil.safeExecute;
22 
23 import android.annotation.CallbackExecutor;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 import android.app.appsearch.aidl.AppSearchAttributionSource;
27 import android.app.appsearch.aidl.AppSearchResultParcel;
28 import android.app.appsearch.aidl.GetNextPageAidlRequest;
29 import android.app.appsearch.aidl.GlobalSearchAidlRequest;
30 import android.app.appsearch.aidl.IAppSearchManager;
31 import android.app.appsearch.aidl.IAppSearchResultCallback;
32 import android.app.appsearch.aidl.InvalidateNextPageTokenAidlRequest;
33 import android.app.appsearch.aidl.SearchAidlRequest;
34 import android.app.appsearch.util.ExceptionUtil;
35 import android.os.RemoteException;
36 import android.os.SystemClock;
37 import android.os.UserHandle;
38 import android.util.Log;
39 
40 import com.android.internal.util.Preconditions;
41 
42 import java.io.Closeable;
43 import java.util.List;
44 import java.util.Objects;
45 import java.util.concurrent.Executor;
46 import java.util.function.Consumer;
47 
48 /**
49  * Encapsulates results of a search operation.
50  *
51  * <p>Each {@link AppSearchSession#search} operation returns a list of {@link SearchResult} objects,
52  * referred to as a "page", limited by the size configured by {@link
53  * SearchSpec.Builder#setResultCountPerPage}.
54  *
55  * <p>To fetch a page of results, call {@link #getNextPage}.
56  *
57  * <p>All instances of {@link SearchResults} must call {@link SearchResults#close()} after the
58  * results are fetched.
59  *
60  * <p>This class is not thread safe.
61  */
62 public class SearchResults implements Closeable {
63     private static final String TAG = "SearchResults";
64 
65     private final IAppSearchManager mService;
66 
67     // The permission identity of the caller
68     private final AppSearchAttributionSource mAttributionSource;
69 
70     // The database name to search over. If null, this will search over all database names.
71     @Nullable private final String mDatabaseName;
72 
73     private final String mQueryExpression;
74 
75     private final SearchSpec mSearchSpec;
76 
77     private final UserHandle mUserHandle;
78 
79     private final boolean mIsForEnterprise;
80 
81     private long mNextPageToken;
82 
83     private boolean mIsFirstLoad = true;
84 
85     private boolean mIsClosed = false;
86 
SearchResults( @onNull IAppSearchManager service, @NonNull AppSearchAttributionSource attributionSource, @Nullable String databaseName, @NonNull String queryExpression, @NonNull SearchSpec searchSpec, @NonNull UserHandle userHandle, boolean isForEnterprise)87     SearchResults(
88             @NonNull IAppSearchManager service,
89             @NonNull AppSearchAttributionSource attributionSource,
90             @Nullable String databaseName,
91             @NonNull String queryExpression,
92             @NonNull SearchSpec searchSpec,
93             @NonNull UserHandle userHandle,
94             boolean isForEnterprise) {
95         mService = Objects.requireNonNull(service);
96         mAttributionSource = Objects.requireNonNull(attributionSource);
97         mDatabaseName = databaseName;
98         mQueryExpression = Objects.requireNonNull(queryExpression);
99         mSearchSpec = Objects.requireNonNull(searchSpec);
100         mUserHandle = Objects.requireNonNull(userHandle);
101         mIsForEnterprise = isForEnterprise;
102     }
103 
104     /**
105      * Retrieves the next page of {@link SearchResult} objects.
106      *
107      * <p>The page size is configured by {@link SearchSpec.Builder#setResultCountPerPage}.
108      *
109      * <p>Continue calling this method to access results until it returns an empty list, signifying
110      * there are no more results.
111      *
112      * @param executor Executor on which to invoke the callback.
113      * @param callback Callback to receive the pending result of performing this operation.
114      */
getNextPage( @onNull @allbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<List<SearchResult>>> callback)115     public void getNextPage(
116             @NonNull @CallbackExecutor Executor executor,
117             @NonNull Consumer<AppSearchResult<List<SearchResult>>> callback) {
118         Objects.requireNonNull(executor);
119         Objects.requireNonNull(callback);
120         Preconditions.checkState(!mIsClosed, "SearchResults has already been closed");
121         try {
122             long binderCallStartTimeMillis = SystemClock.elapsedRealtime();
123             if (mIsFirstLoad) {
124                 mIsFirstLoad = false;
125                 if (mDatabaseName == null) {
126                     // Global search, there's no one package-database combination to check.
127                     mService.globalSearch(
128                             new GlobalSearchAidlRequest(
129                                     mAttributionSource,
130                                     mQueryExpression,
131                                     mSearchSpec,
132                                     mUserHandle,
133                                     binderCallStartTimeMillis,
134                                     mIsForEnterprise),
135                             wrapCallback(executor, callback));
136                 } else {
137                     // Normal local search, pass in specified database.
138                     mService.search(
139                             new SearchAidlRequest(
140                                     mAttributionSource,
141                                     mDatabaseName,
142                                     mQueryExpression,
143                                     mSearchSpec,
144                                     mUserHandle,
145                                     binderCallStartTimeMillis),
146                             wrapCallback(executor, callback));
147                 }
148             } else {
149                 // TODO(b/276349029): Log different join types when they get added.
150                 @AppSearchSchema.StringPropertyConfig.JoinableValueType
151                 int joinType = JOINABLE_VALUE_TYPE_NONE;
152                 JoinSpec joinSpec = mSearchSpec.getJoinSpec();
153                 if (joinSpec != null && !joinSpec.getChildPropertyExpression().isEmpty()) {
154                     joinType = JOINABLE_VALUE_TYPE_QUALIFIED_ID;
155                 }
156                 mService.getNextPage(
157                         new GetNextPageAidlRequest(
158                                 mAttributionSource,
159                                 mDatabaseName,
160                                 mNextPageToken,
161                                 joinType,
162                                 mUserHandle,
163                                 binderCallStartTimeMillis,
164                                 mIsForEnterprise),
165                         wrapCallback(executor, callback));
166             }
167         } catch (RemoteException e) {
168             ExceptionUtil.handleRemoteException(e);
169         }
170     }
171 
172     @Override
close()173     public void close() {
174         if (!mIsClosed) {
175             try {
176                 mService.invalidateNextPageToken(
177                         new InvalidateNextPageTokenAidlRequest(
178                                 mAttributionSource,
179                                 mNextPageToken,
180                                 mUserHandle,
181                                 /* binderCallStartTimeMillis= */ SystemClock.elapsedRealtime(),
182                                 mIsForEnterprise));
183                 mIsClosed = true;
184             } catch (RemoteException e) {
185                 Log.e(TAG, "Unable to close the SearchResults", e);
186             }
187         }
188     }
189 
wrapCallback( @onNull @allbackExecutor Executor executor, @NonNull Consumer<AppSearchResult<List<SearchResult>>> callback)190     private IAppSearchResultCallback wrapCallback(
191             @NonNull @CallbackExecutor Executor executor,
192             @NonNull Consumer<AppSearchResult<List<SearchResult>>> callback) {
193         return new IAppSearchResultCallback.Stub() {
194             @Override
195             public void onResult(AppSearchResultParcel resultParcel) {
196                 safeExecute(
197                         executor,
198                         callback,
199                         () -> invokeCallback(resultParcel.getResult(), callback));
200             }
201         };
202     }
203 
204     private void invokeCallback(
205             @NonNull AppSearchResult<SearchResultPage> searchResultPageResult,
206             @NonNull Consumer<AppSearchResult<List<SearchResult>>> callback) {
207         if (searchResultPageResult.isSuccess()) {
208             try {
209                 SearchResultPage searchResultPage =
210                         Objects.requireNonNull(searchResultPageResult.getResultValue());
211                 mNextPageToken = searchResultPage.getNextPageToken();
212                 callback.accept(AppSearchResult.newSuccessfulResult(searchResultPage.getResults()));
213             } catch (RuntimeException e) {
214                 callback.accept(AppSearchResult.throwableToFailedResult(e));
215             }
216         } else {
217             callback.accept(AppSearchResult.newFailedResult(searchResultPageResult));
218         }
219     }
220 }
221