1 /* 2 * Copyright (C) 2024 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.server.appsearch.appsindexer; 18 19 import android.annotation.NonNull; 20 import android.app.appsearch.AppSearchBatchResult; 21 import android.app.appsearch.AppSearchEnvironmentFactory; 22 import android.app.appsearch.AppSearchManager; 23 import android.app.appsearch.AppSearchResult; 24 import android.app.appsearch.AppSearchSchema; 25 import android.app.appsearch.BatchResultCallback; 26 import android.app.appsearch.PackageIdentifier; 27 import android.app.appsearch.PutDocumentsRequest; 28 import android.app.appsearch.SearchResult; 29 import android.app.appsearch.SearchSpec; 30 import android.app.appsearch.SetSchemaRequest; 31 import android.app.appsearch.exceptions.AppSearchException; 32 import android.content.Context; 33 import android.util.AndroidRuntimeException; 34 import android.util.ArrayMap; 35 import android.util.Log; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.server.appsearch.appsindexer.appsearchtypes.MobileApplication; 39 40 import java.io.Closeable; 41 import java.util.Collections; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Objects; 45 import java.util.concurrent.ExecutorService; 46 47 /** 48 * Helper class to manage the App corpus in AppSearch. 49 * 50 * <p>There are two primary methods in this class, {@link #setSchemasForPackages} and {@link 51 * #indexApps}. On a given Apps Index update, they may not necessarily both be called. For instance, 52 * if the indexer determines that the only change is that an app was deleted, there is no reason to 53 * insert any * apps, so we can save time by only calling setSchemas to erase the deleted app 54 * schema. On the other hand, if the only change is that an app was update, there is no reason to 55 * call setSchema. We can instead just update the updated app with a call to indexApps. Figuring out 56 * what needs to be done is left to {@link AppsIndexerImpl}. 57 * 58 * <p>This class is thread-safe. 59 * 60 * @hide 61 */ 62 public class AppSearchHelper implements Closeable { 63 private static final String TAG = "AppSearchAppsIndexerAppSearchHelper"; 64 65 // The apps indexer uses one database, and in that database we have one schema for every app 66 // that is indexed. The reason for this is that we keep the schema types the same for every app 67 // (MobileApplication), but we need different visibility settings for each app. These different 68 // visibility settings are set with Public ACL and rely on PackageManager#canPackageQuery. 69 // Therefore each application needs its own schema. We put all these schema into a single 70 // database by dynamically renaming the schema so that they have different names. 71 public static final String APP_DATABASE = "apps-db"; 72 private static final int GET_APP_IDS_PAGE_SIZE = 1000; 73 private final Context mContext; 74 private final ExecutorService mExecutor; 75 private final AppSearchManager mAppSearchManager; 76 private SyncAppSearchSession mSyncAppSearchSession; 77 private SyncGlobalSearchSession mSyncGlobalSearchSession; 78 79 /** Creates and initializes an {@link AppSearchHelper} */ 80 @NonNull createAppSearchHelper(@onNull Context context)81 public static AppSearchHelper createAppSearchHelper(@NonNull Context context) 82 throws AppSearchException { 83 Objects.requireNonNull(context); 84 85 AppSearchHelper appSearchHelper = new AppSearchHelper(context); 86 appSearchHelper.initializeAppSearchSessions(); 87 return appSearchHelper; 88 } 89 90 /** Creates an initialized {@link AppSearchHelper}. */ 91 @VisibleForTesting AppSearchHelper(@onNull Context context)92 private AppSearchHelper(@NonNull Context context) { 93 mContext = Objects.requireNonNull(context); 94 95 mAppSearchManager = context.getSystemService(AppSearchManager.class); 96 if (mAppSearchManager == null) { 97 throw new AndroidRuntimeException( 98 "Can't get AppSearchManager to initialize AppSearchHelper."); 99 } 100 mExecutor = 101 AppSearchEnvironmentFactory.getEnvironmentInstance().createSingleThreadExecutor(); 102 } 103 104 /** 105 * Sets up the search session. 106 * 107 * @throws AppSearchException if unable to initialize the {@link SyncAppSearchSession} or the 108 * {@link SyncGlobalSearchSession}. 109 */ initializeAppSearchSessions()110 private void initializeAppSearchSessions() throws AppSearchException { 111 AppSearchManager.SearchContext searchContext = 112 new AppSearchManager.SearchContext.Builder(APP_DATABASE).build(); 113 mSyncAppSearchSession = 114 new SyncAppSearchSessionImpl(mAppSearchManager, searchContext, mExecutor); 115 mSyncGlobalSearchSession = new SyncGlobalSearchSessionImpl(mAppSearchManager, mExecutor); 116 } 117 118 /** Just for testing, allows us to test various scenarios involving SyncAppSearchSession. */ 119 @VisibleForTesting setAppSearchSession(@onNull SyncAppSearchSession session)120 /* package */ void setAppSearchSession(@NonNull SyncAppSearchSession session) { 121 // Close the old one 122 mSyncAppSearchSession.close(); 123 mSyncAppSearchSession = Objects.requireNonNull(session); 124 } 125 126 /** 127 * Sets the AppsIndexer database schema to correspond to the list of passed in {@link 128 * PackageIdentifier}s. Note that this means if a schema exists in AppSearch that does not get 129 * passed in to this method, it will be erased. And if a schema does not exist in AppSearch that 130 * is passed in to this method, it will be created. 131 */ setSchemasForPackages(@onNull List<PackageIdentifier> pkgs)132 public void setSchemasForPackages(@NonNull List<PackageIdentifier> pkgs) 133 throws AppSearchException { 134 Objects.requireNonNull(pkgs); 135 SetSchemaRequest.Builder schemaBuilder = 136 new SetSchemaRequest.Builder() 137 // If MobileApplication schema later gets changed to a compatible schema, we 138 // should first try setting the schema with forceOverride = false. 139 .setForceOverride(true); 140 for (int i = 0; i < pkgs.size(); i++) { 141 PackageIdentifier pkg = pkgs.get(i); 142 // As all apps are in the same db, we have to make sure that even if it's getting 143 // updated, the schema is in the list of schemas 144 String packageName = pkg.getPackageName(); 145 AppSearchSchema schemaVariant = 146 MobileApplication.createMobileApplicationSchemaForPackage(packageName); 147 schemaBuilder.addSchemas(schemaVariant); 148 // Since the Android package of the underlying apps are different from the package name 149 // that "owns" the builtin:MobileApplication corpus in AppSearch, we needed to add the 150 // PackageIdentifier parameter to setPubliclyVisibleSchema. 151 schemaBuilder.setPubliclyVisibleSchema(schemaVariant.getSchemaType(), pkg); 152 } 153 154 // TODO(b/275592563): Log app removal in metrics 155 mSyncAppSearchSession.setSchema(schemaBuilder.build()); 156 } 157 158 /** 159 * Indexes a collection of apps into AppSearch. This requires that the corresponding 160 * MobileApplication schemas are already set by a previous call to {@link 161 * #setSchemasForPackages}. The call doesn't necessarily have to happen in the current sync. 162 * 163 * @throws AppSearchException if indexing results in a {@link 164 * AppSearchResult#RESULT_OUT_OF_SPACE} result code. It will also throw this if the put call 165 * results in a system error as in {@link BatchResultCallback#onSystemError}. This may 166 * happen if the AppSearch service unexpectedly fails to initialize and can't be recovered, 167 * for instance. 168 */ indexApps(@onNull List<MobileApplication> apps)169 public void indexApps(@NonNull List<MobileApplication> apps) throws AppSearchException { 170 Objects.requireNonNull(apps); 171 172 // At this point, the document schema names have already been set to the per-package name. 173 // We can just add them to the request. 174 PutDocumentsRequest request = 175 new PutDocumentsRequest.Builder().addGenericDocuments(apps).build(); 176 177 AppSearchBatchResult<String, Void> result = mSyncAppSearchSession.put(request); 178 if (!result.isSuccess()) { 179 Map<String, AppSearchResult<Void>> failures = result.getFailures(); 180 for (AppSearchResult<Void> failure : failures.values()) { 181 // If it's out of space, stop indexing 182 if (failure.getResultCode() == AppSearchResult.RESULT_OUT_OF_SPACE) { 183 throw new AppSearchException( 184 failure.getResultCode(), failure.getErrorMessage()); 185 } else { 186 Log.e(TAG, "Ran into error while indexing apps: " + failure); 187 } 188 } 189 } 190 } 191 192 /** 193 * Searches AppSearch and returns a Map with the package ids and their last updated times. This 194 * helps us determine which app documents need to be re-indexed. 195 */ 196 @NonNull getAppsFromAppSearch()197 public Map<String, Long> getAppsFromAppSearch() { 198 SearchSpec allAppsSpec = 199 new SearchSpec.Builder() 200 .addFilterNamespaces(MobileApplication.APPS_NAMESPACE) 201 .addProjection( 202 SearchSpec.SCHEMA_TYPE_WILDCARD, 203 Collections.singletonList( 204 MobileApplication.APP_PROPERTY_UPDATED_TIMESTAMP)) 205 .addFilterPackageNames(mContext.getPackageName()) 206 .setResultCountPerPage(GET_APP_IDS_PAGE_SIZE) 207 .build(); 208 SyncSearchResults results = mSyncGlobalSearchSession.search(/* query= */ "", allAppsSpec); 209 return collectUpdatedTimestampFromAllPages(results); 210 } 211 212 /** Iterates through result pages to get the last updated times */ 213 @NonNull collectUpdatedTimestampFromAllPages( @onNull SyncSearchResults results)214 private Map<String, Long> collectUpdatedTimestampFromAllPages( 215 @NonNull SyncSearchResults results) { 216 Objects.requireNonNull(results); 217 Map<String, Long> appUpdatedMap = new ArrayMap<>(); 218 219 try { 220 List<SearchResult> resultList = results.getNextPage(); 221 222 while (!resultList.isEmpty()) { 223 for (int i = 0; i < resultList.size(); i++) { 224 SearchResult result = resultList.get(i); 225 appUpdatedMap.put( 226 result.getGenericDocument().getId(), 227 result.getGenericDocument() 228 .getPropertyLong( 229 MobileApplication.APP_PROPERTY_UPDATED_TIMESTAMP)); 230 } 231 232 resultList = results.getNextPage(); 233 } 234 } catch (AppSearchException e) { 235 Log.e(TAG, "Error while searching for all app documents", e); 236 } 237 // Return what we have so far. Even if this doesn't fetch all documents, that is fine as we 238 // can continue with indexing. The documents that aren't fetched will be detected as new 239 // apps and re-indexed. 240 return appUpdatedMap; 241 } 242 243 /** Closes the AppSearch sessions. */ 244 @Override close()245 public void close() { 246 mSyncAppSearchSession.close(); 247 mSyncGlobalSearchSession.close(); 248 } 249 } 250