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