• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.adservices.service.appsearch;
18 
19 import static com.android.adservices.service.consent.ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE;
20 
21 import android.annotation.NonNull;
22 import android.os.Build;
23 
24 import androidx.annotation.Nullable;
25 import androidx.annotation.RequiresApi;
26 import androidx.appsearch.app.AppSearchBatchResult;
27 import androidx.appsearch.app.AppSearchSession;
28 import androidx.appsearch.app.GenericDocument;
29 import androidx.appsearch.app.GlobalSearchSession;
30 import androidx.appsearch.app.PackageIdentifier;
31 import androidx.appsearch.app.PutDocumentsRequest;
32 import androidx.appsearch.app.RemoveByDocumentIdRequest;
33 import androidx.appsearch.app.SearchResults;
34 import androidx.appsearch.app.SearchSpec;
35 import androidx.appsearch.app.SetSchemaRequest;
36 import androidx.appsearch.app.SetSchemaResponse.MigrationFailure;
37 import androidx.appsearch.exceptions.AppSearchException;
38 
39 import com.android.adservices.AdServicesCommon;
40 import com.android.adservices.LogUtil;
41 import com.android.adservices.service.Flags;
42 import com.android.adservices.service.FlagsFactory;
43 import com.android.adservices.service.common.AllowLists;
44 import com.android.internal.annotations.VisibleForTesting;
45 
46 import com.google.common.util.concurrent.FluentFuture;
47 import com.google.common.util.concurrent.Futures;
48 import com.google.common.util.concurrent.ListenableFuture;
49 
50 import java.util.Collections;
51 import java.util.List;
52 import java.util.Objects;
53 import java.util.concurrent.ExecutionException;
54 import java.util.concurrent.Executor;
55 import java.util.concurrent.TimeUnit;
56 import java.util.concurrent.TimeoutException;
57 import java.util.function.BiFunction;
58 
59 /**
60  * Base class for all data access objects for AppSearch. This class handles the common logic for
61  * reading from and writing to AppSearch.
62  */
63 @RequiresApi(Build.VERSION_CODES.S)
64 class AppSearchDao {
65     /**
66      * Iterate over the search results returned for the search query by AppSearch.
67      *
68      * @return future containing instance of the subclass type.
69      * @param <T> the subclass of AppSearchDao that this Document is of type.
70      */
71     @VisibleForTesting
iterateSearchResults( Class<T> cls, SearchResults searchResults, Executor executor)72     static <T> ListenableFuture<T> iterateSearchResults(
73             Class<T> cls, SearchResults searchResults, Executor executor) {
74         return Futures.transform(
75                 searchResults.getNextPageAsync(),
76                 page -> {
77                     if (page.isEmpty()) return null;
78 
79                     // Gets GenericDocument from SearchResult.
80                     GenericDocument genericDocument = page.get(0).getGenericDocument();
81                     String schemaType = genericDocument.getSchemaType();
82                     T documentResult = null;
83 
84                     if (schemaType.equals(cls.getSimpleName())) {
85                         try {
86                             // Converts GenericDocument object to the type of object passed in cls.
87                             documentResult = genericDocument.toDocumentClass(cls);
88                         } catch (AppSearchException e) {
89                             LogUtil.e(e, "Failed to convert GenericDocument to %s", cls.getName());
90                         }
91                     }
92 
93                     return documentResult;
94                 },
95                 executor);
96     }
97 
98     @VisibleForTesting
getAllowedPackages(String adServicesPackageName)99     static List<String> getAllowedPackages(String adServicesPackageName) {
100         Flags flags = FlagsFactory.getFlags();
101         String overrideAllowList = flags.getAppsearchWriterAllowListOverride();
102 
103         if (!overrideAllowList.isEmpty()) {
104             return AllowLists.splitAllowList(overrideAllowList);
105         }
106 
107         /* We want the extservices package name, not the adservices package name, so replace the
108          * suffix from adservices with the extservices suffix.
109          */
110         return Collections.singletonList(
111                 adServicesPackageName.replace(
112                         AdServicesCommon.ADSERVICES_APK_PACKAGE_NAME_SUFFIX,
113                         AdServicesCommon.ADEXTSERVICES_PACKAGE_NAME_SUFFIX));
114     }
115 
116     /**
117      * Read the consent data from the provided GlobalSearchSession. This requires a query to be
118      * specified. If the query is not specified, we do not perform a search since multiple rows will
119      * be returned.
120      *
121      * @return the instance of subclass type that was read from AppSearch.
122      */
123     @Nullable
readConsentData( @onNull Class<T> cls, @NonNull ListenableFuture<GlobalSearchSession> searchSession, @NonNull Executor executor, @NonNull String namespace, @NonNull String query, @NonNull String adServicesPackageName)124     protected static <T> T readConsentData(
125             @NonNull Class<T> cls,
126             @NonNull ListenableFuture<GlobalSearchSession> searchSession,
127             @NonNull Executor executor,
128             @NonNull String namespace,
129             @NonNull String query,
130             @NonNull String adServicesPackageName) {
131         return readData(
132                 cls,
133                 searchSession,
134                 executor,
135                 namespace,
136                 query,
137                 (session, spec) -> session.search(query, spec),
138                 adServicesPackageName);
139     }
140 
141     /**
142      * Read the session data from the provided AppSearchSession. This requires a query to be
143      * specified. If the query is not specified, we do not perform a search since multiple rows will
144      * be returned.
145      *
146      * @return the instance of subclass type that was read from AppSearch.
147      */
148     @Nullable
readAppSearchSessionData( @onNull Class<T> cls, @NonNull ListenableFuture<AppSearchSession> searchSession, @NonNull Executor executor, @NonNull String namespace, @NonNull String query, @NonNull String adServicesPackageName)149     protected static <T> T readAppSearchSessionData(
150             @NonNull Class<T> cls,
151             @NonNull ListenableFuture<AppSearchSession> searchSession,
152             @NonNull Executor executor,
153             @NonNull String namespace,
154             @NonNull String query,
155             @NonNull String adServicesPackageName) {
156         return readData(
157                 cls,
158                 searchSession,
159                 executor,
160                 namespace,
161                 query,
162                 (session, spec) -> session.search(query, spec),
163                 adServicesPackageName);
164     }
165 
166     @Nullable
readData( @onNull Class<T> cls, @NonNull ListenableFuture<S> searchSession, @NonNull Executor executor, @NonNull String namespace, @NonNull String query, @NonNull BiFunction<S, SearchSpec, SearchResults> sessionQuery, @NonNull String adServicesPackageName)167     private static <T, S> T readData(
168             @NonNull Class<T> cls,
169             @NonNull ListenableFuture<S> searchSession,
170             @NonNull Executor executor,
171             @NonNull String namespace,
172             @NonNull String query,
173             @NonNull BiFunction<S, SearchSpec, SearchResults> sessionQuery,
174             @NonNull String adServicesPackageName) {
175         Objects.requireNonNull(cls);
176         Objects.requireNonNull(searchSession);
177         Objects.requireNonNull(executor);
178         Objects.requireNonNull(namespace);
179         Objects.requireNonNull(sessionQuery);
180 
181         // Namespace and Query cannot be empty.
182         if (query == null || query.isEmpty() || namespace.isEmpty()) {
183             return null;
184         }
185 
186         try {
187             List<String> allowedPackages = getAllowedPackages(adServicesPackageName);
188             SearchSpec searchSpec =
189                     new SearchSpec.Builder()
190                             .addFilterNamespaces(namespace)
191                             .addFilterPackageNames(allowedPackages)
192                             .build();
193             ListenableFuture<SearchResults> searchFuture =
194                     Futures.transform(
195                             searchSession,
196                             session -> sessionQuery.apply(session, searchSpec),
197                             executor);
198             FluentFuture<T> future =
199                     FluentFuture.from(searchFuture)
200                             .transformAsync(
201                                     results -> iterateSearchResults(cls, results, executor),
202                                     executor)
203                             .transform(result -> ((T) result), executor);
204 
205             // Currently all read operations have the same timeout, so reading it directly from the
206             // flags here. In the future, if we want these operations to have independent timeouts,
207             // we should add the timeout value as a parameter to this function.
208             int timeout = FlagsFactory.getFlags().getAppSearchReadTimeout();
209             return future.get(timeout, TimeUnit.MILLISECONDS);
210         } catch (ExecutionException | InterruptedException | TimeoutException e) {
211             LogUtil.e(e, "getConsent() AppSearch lookup failed");
212         }
213         return null;
214     }
215 
216     /**
217      * Write consent/session data to AppSearch. This requires knowing the packageIdentifier of the
218      * package that needs to be allowed read access to the data. When we write the data on S- device
219      * we specify the packageIdentifier as that of the T+ AdServices APK, which after OTA, needs
220      * access to the data written before OTA. What is written is the subclass type of DAO.
221      *
222      * @return the result of the write operation.
223      */
writeData( @onNull ListenableFuture<AppSearchSession> appSearchSession, @NonNull List<PackageIdentifier> packageIdentifiers, @NonNull Executor executor)224     AppSearchBatchResult<String, Void> writeData(
225             @NonNull ListenableFuture<AppSearchSession> appSearchSession,
226             @NonNull List<PackageIdentifier> packageIdentifiers,
227             @NonNull Executor executor) {
228         Objects.requireNonNull(appSearchSession);
229         Objects.requireNonNull(packageIdentifiers);
230         Objects.requireNonNull(executor);
231 
232         try {
233             SetSchemaRequest.Builder setSchemaRequestBuilder = new SetSchemaRequest.Builder();
234             setSchemaRequestBuilder.addDocumentClasses(getClass());
235             for (PackageIdentifier packageIdentifier : packageIdentifiers) {
236                 setSchemaRequestBuilder.setSchemaTypeVisibilityForPackage(
237                         getClass().getSimpleName(), true, packageIdentifier);
238             }
239             SetSchemaRequest setSchemaRequest = setSchemaRequestBuilder.build();
240             PutDocumentsRequest putRequest =
241                     new PutDocumentsRequest.Builder().addDocuments(this).build();
242             FluentFuture<AppSearchBatchResult<String, Void>> putFuture =
243                     FluentFuture.from(appSearchSession)
244                             .transformAsync(
245                                     session -> session.setSchemaAsync(setSchemaRequest), executor)
246                             .transformAsync(
247                                     setSchemaResponse -> {
248                                         // If we get failures in schemaResponse then we cannot try
249                                         // to write.
250                                         if (!setSchemaResponse.getMigrationFailures().isEmpty()) {
251                                             MigrationFailure failure =
252                                                     setSchemaResponse.getMigrationFailures().get(0);
253                                             LogUtil.e(
254                                                     "SetSchemaResponse migration failure: "
255                                                             + failure);
256                                             String message =
257                                                     String.format(
258                                                             "%s Migration failure: %s",
259                                                             ERROR_MESSAGE_APPSEARCH_FAILURE,
260                                                             failure.getAppSearchResult());
261                                             throw new RuntimeException(message);
262                                         }
263                                         // The database knows about this schemaType and write can
264                                         // occur.
265                                         return Futures.transformAsync(
266                                                 appSearchSession,
267                                                 session -> session.putAsync(putRequest),
268                                                 executor);
269                                     },
270                                     executor);
271 
272             // Currently all write operations have the same timeout, so reading it directly from the
273             // flags here. In the future, if we want these operations to have independent timeouts,
274             // we should add the timeout value as a parameter to this function.
275             int timeout = FlagsFactory.getFlags().getAppSearchWriteTimeout();
276             return putFuture.get(timeout, TimeUnit.MILLISECONDS);
277         } catch (AppSearchException e) {
278             LogUtil.e(e, "Cannot instantiate AppSearch database");
279             throw new RuntimeException(ERROR_MESSAGE_APPSEARCH_FAILURE, e);
280         } catch (ExecutionException | InterruptedException | TimeoutException e) {
281             LogUtil.e(e, "Failed to write data to AppSearch database");
282             throw new RuntimeException(ERROR_MESSAGE_APPSEARCH_FAILURE, e);
283         }
284     }
285 
286     /**
287      * Delete a row from the database.
288      *
289      * @return the result of the delete operation.
290      */
deleteData( @onNull Class<T> cls, @NonNull ListenableFuture<AppSearchSession> appSearchSession, @NonNull Executor executor, @NonNull String rowId, @NonNull String namespace)291     protected static <T> AppSearchBatchResult<String, Void> deleteData(
292             @NonNull Class<T> cls,
293             @NonNull ListenableFuture<AppSearchSession> appSearchSession,
294             @NonNull Executor executor,
295             @NonNull String rowId,
296             @NonNull String namespace) {
297         Objects.requireNonNull(cls);
298         Objects.requireNonNull(appSearchSession);
299         Objects.requireNonNull(executor);
300         Objects.requireNonNull(rowId);
301         Objects.requireNonNull(namespace);
302 
303         try {
304             SetSchemaRequest setSchemaRequest =
305                     new SetSchemaRequest.Builder().addDocumentClasses(cls).build();
306             RemoveByDocumentIdRequest deleteRequest =
307                     new RemoveByDocumentIdRequest.Builder(namespace).addIds(rowId).build();
308             FluentFuture<AppSearchBatchResult<String, Void>> deleteFuture =
309                     FluentFuture.from(appSearchSession)
310                             .transformAsync(
311                                     session -> session.setSchemaAsync(setSchemaRequest), executor)
312                             .transformAsync(
313                                     setSchemaResponse -> {
314                                         // If we get failures in schemaResponse then we cannot try
315                                         // to write.
316                                         if (!setSchemaResponse.getMigrationFailures().isEmpty()) {
317                                             MigrationFailure failure =
318                                                     setSchemaResponse.getMigrationFailures().get(0);
319                                             LogUtil.e(
320                                                     "SetSchemaResponse migration failure: "
321                                                             + failure);
322                                             String message =
323                                                     String.format(
324                                                             "%s Migration failure: %s",
325                                                             ERROR_MESSAGE_APPSEARCH_FAILURE,
326                                                             failure.getAppSearchResult());
327                                             throw new RuntimeException(message);
328                                         }
329                                         // The database knows about this schemaType and write can
330                                         // occur.
331                                         return Futures.transformAsync(
332                                                 appSearchSession,
333                                                 session -> session.removeAsync(deleteRequest),
334                                                 executor);
335                                     },
336                                     executor);
337 
338             // Currently all write operations have the same timeout, so reading it directly from the
339             // flags here. In the future, if we want these operations to have independent timeouts,
340             // we should add the timeout value as a parameter to this function.
341             int timeout = FlagsFactory.getFlags().getAppSearchWriteTimeout();
342             return deleteFuture.get(timeout, TimeUnit.MILLISECONDS);
343         } catch (AppSearchException e) {
344             LogUtil.e(e, "Cannot instantiate AppSearch database");
345             throw new RuntimeException(ERROR_MESSAGE_APPSEARCH_FAILURE, e);
346         } catch (ExecutionException | InterruptedException | TimeoutException e) {
347             LogUtil.e(e, "Failed to delete data from AppSearch");
348             throw new RuntimeException(ERROR_MESSAGE_APPSEARCH_FAILURE, e);
349         }
350     }
351 }
352