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 static com.android.server.appsearch.appsindexer.AppIndexerVersions.CURR_APP_INDEXER_VERSION; 20 import static com.android.server.appsearch.indexer.IndexerMaintenanceConfig.APPS_INDEXER; 21 22 import android.annotation.NonNull; 23 import android.annotation.WorkerThread; 24 import android.app.appsearch.AppSearchEnvironmentFactory; 25 import android.app.appsearch.exceptions.AppSearchException; 26 import android.content.Context; 27 import android.os.SystemClock; 28 import android.util.Log; 29 import android.util.Slog; 30 31 import com.android.appsearch.flags.Flags; 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.server.appsearch.indexer.IndexerMaintenanceService; 34 import com.android.server.appsearch.stats.AppSearchStatsLog; 35 36 import java.io.File; 37 import java.io.FileNotFoundException; 38 import java.io.IOException; 39 import java.io.PrintWriter; 40 import java.util.Objects; 41 import java.util.concurrent.ExecutorService; 42 import java.util.concurrent.Semaphore; 43 import java.util.concurrent.TimeUnit; 44 45 /** 46 * Apps Indexer for a single user. 47 * 48 * <p>It reads the updated/newly-inserted/deleted apps from PackageManager, and syncs the changes 49 * into AppSearch. 50 * 51 * <p>This class is thread safe. 52 * 53 * @hide 54 */ 55 public final class AppsIndexerUserInstance { 56 57 private static final String TAG = "AppSearchAppsIndexerUserInst"; 58 59 private final File mDataDir; 60 // While AppsIndexerSettings is not thread safe, it is only accessed through a single-threaded 61 // executor service. It will be read and updated before the next scheduled task accesses it. 62 private final AppsIndexerSettings mSettings; 63 64 // Used for handling the app change notification so we won't schedule too many updates. At any 65 // time, only two threads can run an update. But since we use a single-threaded executor, it 66 // means that at most one thread can be running, and another thread can be waiting to run. This 67 // will happen in the case that an update is requested while another is running. 68 private final Semaphore mRunningOrScheduledSemaphore = new Semaphore(2); 69 70 private AppsIndexerImpl mAppsIndexerImpl; 71 72 /** 73 * Single threaded executor to make sure there is only one active sync for this {@link 74 * AppsIndexerUserInstance}. Background tasks should be scheduled using {@link 75 * #executeOnSingleThreadedExecutor(Runnable)} which ensures that they are not executed if the 76 * executor is shutdown during {@link #shutdown()}. 77 * 78 * <p>Note that this executor is used as both work and callback executors which is fine because 79 * AppSearch should be able to handle exceptions thrown by them. 80 */ 81 private final ExecutorService mSingleThreadedExecutor; 82 83 private final Context mContext; 84 private final AppsIndexerConfig mAppsIndexerConfig; 85 86 /** 87 * Constructs and initializes a {@link AppsIndexerUserInstance}. 88 * 89 * <p>Heavy operations such as connecting to AppSearch are performed asynchronously. 90 * 91 * @param appsDir data directory for AppsIndexer. 92 */ 93 @NonNull createInstance( @onNull Context userContext, @NonNull File appsDir, @NonNull AppsIndexerConfig appsIndexerConfig)94 public static AppsIndexerUserInstance createInstance( 95 @NonNull Context userContext, 96 @NonNull File appsDir, 97 @NonNull AppsIndexerConfig appsIndexerConfig) 98 throws AppSearchException { 99 Objects.requireNonNull(userContext); 100 Objects.requireNonNull(appsDir); 101 Objects.requireNonNull(appsIndexerConfig); 102 103 ExecutorService singleThreadedExecutor = 104 AppSearchEnvironmentFactory.getEnvironmentInstance().createSingleThreadExecutor(); 105 return createInstance(userContext, appsDir, appsIndexerConfig, singleThreadedExecutor); 106 } 107 108 @VisibleForTesting 109 @NonNull createInstance( @onNull Context context, @NonNull File appsDir, @NonNull AppsIndexerConfig appsIndexerConfig, @NonNull ExecutorService executorService)110 static AppsIndexerUserInstance createInstance( 111 @NonNull Context context, 112 @NonNull File appsDir, 113 @NonNull AppsIndexerConfig appsIndexerConfig, 114 @NonNull ExecutorService executorService) 115 throws AppSearchException { 116 Objects.requireNonNull(context); 117 Objects.requireNonNull(appsDir); 118 Objects.requireNonNull(appsIndexerConfig); 119 Objects.requireNonNull(executorService); 120 121 AppsIndexerUserInstance indexer = 122 new AppsIndexerUserInstance(appsDir, executorService, context, appsIndexerConfig); 123 indexer.loadSettingsAsync(); 124 indexer.mAppsIndexerImpl = new AppsIndexerImpl(context, appsIndexerConfig); 125 126 return indexer; 127 } 128 129 /** 130 * Constructs a {@link AppsIndexerUserInstance}. 131 * 132 * @param dataDir data directory for storing apps indexer state. 133 * @param singleThreadedExecutor an {@link ExecutorService} with at most one thread to ensure 134 * the thread safety of this class. 135 * @param context Context object passed from {@link AppsIndexerManagerService} 136 */ AppsIndexerUserInstance( @onNull File dataDir, @NonNull ExecutorService singleThreadedExecutor, @NonNull Context context, @NonNull AppsIndexerConfig appsIndexerConfig)137 private AppsIndexerUserInstance( 138 @NonNull File dataDir, 139 @NonNull ExecutorService singleThreadedExecutor, 140 @NonNull Context context, 141 @NonNull AppsIndexerConfig appsIndexerConfig) { 142 mDataDir = Objects.requireNonNull(dataDir); 143 mSettings = new AppsIndexerSettings(mDataDir); 144 mSingleThreadedExecutor = Objects.requireNonNull(singleThreadedExecutor); 145 mContext = Objects.requireNonNull(context); 146 mAppsIndexerConfig = Objects.requireNonNull(appsIndexerConfig); 147 } 148 149 /** Shuts down the AppsIndexerUserInstance */ shutdown()150 public void shutdown() throws InterruptedException { 151 mAppsIndexerImpl.close(); 152 IndexerMaintenanceService.cancelUpdateJobIfScheduled( 153 mContext, mContext.getUser(), APPS_INDEXER); 154 synchronized (mSingleThreadedExecutor) { 155 mSingleThreadedExecutor.shutdown(); 156 } 157 boolean unused = mSingleThreadedExecutor.awaitTermination(30L, TimeUnit.SECONDS); 158 } 159 160 /** Dumps the internal state of this {@link AppsIndexerUserInstance}. */ dump(@onNull PrintWriter pw)161 public void dump(@NonNull PrintWriter pw) { 162 // Those timestamps are not protected by any lock since in AppsIndexerUserInstance 163 // we only have one thread to handle all the updates. It is possible we might run into 164 // race condition if there is an update running while those numbers are being printed. 165 // This is acceptable though for debug purpose, so still no lock here. 166 pw.println("last_update_timestamp_millis: " + mSettings.getLastUpdateTimestampMillis()); 167 pw.println( 168 "last_app_update_timestamp_millis: " + mSettings.getLastAppUpdateTimestampMillis()); 169 } 170 171 /** 172 * Schedule an update. No new update can be scheduled if there are two updates already scheduled 173 * or currently being run. 174 * 175 * @param firstRun boolean indicating if this is a first run and that settings should be checked 176 * for the last update timestamp. 177 */ updateAsync(boolean firstRun)178 public void updateAsync(boolean firstRun) { 179 AppsUpdateStats appsUpdateStats = new AppsUpdateStats(); 180 long updateLatencyStartTimestampMillis = SystemClock.elapsedRealtime(); 181 appsUpdateStats.mUpdateStartTimestampMillis = System.currentTimeMillis(); 182 appsUpdateStats.mUpdateType = AppsUpdateStats.FULL_UPDATE; 183 // Try to acquire a permit. 184 if (!mRunningOrScheduledSemaphore.tryAcquire()) { 185 // If there are none available, that means an update is running and we have ALREADY 186 // received a change mid-update. The third update request was received during the first 187 // update, and will be handled by the scheduled update. 188 return; 189 } 190 // If there is a permit available, that cold mean there is one update running right now 191 // with none scheduled. Since we use a single threaded executor, calling execute on it 192 // right now will run the requested update after the current update. It could also mean 193 // there is no update running right now, so we can just call execute and run the update 194 // right now. 195 executeOnSingleThreadedExecutor( 196 () -> { 197 doUpdate(firstRun, appsUpdateStats); 198 IndexerMaintenanceService.scheduleUpdateJob( 199 mContext, 200 mContext.getUser(), 201 APPS_INDEXER, 202 /* periodic= */ true, 203 /* intervalMillis= */ mAppsIndexerConfig 204 .getAppsMaintenanceUpdateIntervalMillis()); 205 appsUpdateStats.mTotalLatencyMillis = 206 SystemClock.elapsedRealtime() - updateLatencyStartTimestampMillis; 207 logStats(appsUpdateStats); 208 }); 209 } 210 211 /** 212 * Does the update. It also releases a permit from {@link #mRunningOrScheduledSemaphore} 213 * 214 * @param firstRun when set to true, that means this was called from onUserUnlocking. If we 215 * didn't have this check, the apps indexer would run every time the phone got unlocked. It 216 * should only run the first time this happens. 217 * @param appsUpdateStats contains stats about the apps indexer update. This method will 218 * populate the fields of this {@link AppsUpdateStats} structure. 219 */ 220 @VisibleForTesting 221 @WorkerThread doUpdate(boolean firstRun, @NonNull AppsUpdateStats appsUpdateStats)222 void doUpdate(boolean firstRun, @NonNull AppsUpdateStats appsUpdateStats) { 223 try { 224 Objects.requireNonNull(appsUpdateStats); 225 // Check if there was a prior run 226 boolean isAppIndexerUpdated = 227 Flags.enableAllPackageIndexingOnIndexerUpdate() 228 && checkAndUpdateIndexerVersion(); 229 if (firstRun) { 230 if (Flags.enableAppsIndexerCheckPriorAttempt()) { 231 // Special "firstRun" case. 232 long now = System.currentTimeMillis(); 233 long lastRun = mSettings.getLastAttemptedUpdateTimestampMillis(); 234 long timeSinceLastRun = now - lastRun; 235 236 // If timeSinceLastRun is somehow negative, it means that the system clock 237 // must've turned back since the last run. We'll run the update in this case 238 if (timeSinceLastRun >= 0 239 && timeSinceLastRun 240 < mAppsIndexerConfig.getMinTimeBetweenFirstSyncsMillis()) { 241 // Last firstRun was too recent, skip and leave timestamps alone 242 return; 243 } 244 245 mSettings.setLastAttemptedUpdateTimestampMillis(now); 246 mSettings.persist(); 247 } 248 249 // Check if there was a previous successful run and AppSearch wasn't updated since. 250 if (mSettings.getLastUpdateTimestampMillis() != 0 && !isAppIndexerUpdated) { 251 return; 252 } 253 } 254 if (Flags.enableAppsIndexerIncrementalPut()) { 255 mAppsIndexerImpl.doUpdateIncrementalPut( 256 mSettings, 257 appsUpdateStats, 258 /* isFullUpdateRequired= */ isAppIndexerUpdated); 259 } else { 260 // TODO(b/367410454): Remove this method and related code paths once 261 // enable_apps_indexer_incremental_put flag is rolled out. 262 mAppsIndexerImpl.doUpdate(mSettings, appsUpdateStats); 263 } 264 mSettings.persist(); 265 } catch (IOException e) { 266 Log.w(TAG, "Failed to save settings to disk", e); 267 } catch (AppSearchException e) { 268 Log.e(TAG, "Failed to sync Apps to AppSearch", e); 269 } finally { 270 // Finish a update. If there were no permits available, the update that was requested 271 // mid-update will run. If there was one permit available, we won't run another update. 272 // This happens if no updates were scheduled during the update. 273 mRunningOrScheduledSemaphore.release(); 274 } 275 } 276 277 /** 278 * Checks if the current App Indexer versionCode differs from the previously stored versionCode 279 * in {@link AppsIndexerSettings} and updates the stored versionCode if necessary. 280 * 281 * @return {@code true} if the versionCode has changed, {@code false} otherwise. 282 */ checkAndUpdateIndexerVersion()283 private boolean checkAndUpdateIndexerVersion() { 284 if (mSettings.getPreviousIndexerVersionCode() == CURR_APP_INDEXER_VERSION) { 285 return false; 286 } 287 288 mSettings.setPreviousIndexerVersionCode(CURR_APP_INDEXER_VERSION); 289 return true; 290 } 291 292 /** 293 * Loads the persisted data from disk. 294 * 295 * <p>It doesn't throw here. If it fails to load file, AppsIndexer would always use the 296 * timestamps persisted in the memory. 297 */ loadSettingsAsync()298 private void loadSettingsAsync() { 299 executeOnSingleThreadedExecutor( 300 () -> { 301 try { 302 // If the directory already exists, this returns false. That is fine as it 303 // might not be the first sync. If this returns true, that is fine as it is 304 // the first run and we want to make a new directory. 305 mDataDir.mkdirs(); 306 } catch (SecurityException e) { 307 Log.e(TAG, "Failed to create settings directory on disk.", e); 308 return; 309 } 310 311 try { 312 mSettings.load(); 313 } catch (IOException e) { 314 // Ignore file not found errors (bootstrap case) 315 if (!(e instanceof FileNotFoundException)) { 316 Log.e(TAG, "Failed to load settings from disk", e); 317 } 318 } 319 }); 320 } 321 322 /** 323 * Executes the given command on {@link #mSingleThreadedExecutor} if it is still alive. 324 * 325 * <p>If the {@link #mSingleThreadedExecutor} has been shutdown, this method doesn't execute the 326 * given command, and returns silently. Specifically, it does not throw {@link 327 * java.util.concurrent.RejectedExecutionException}. 328 * 329 * @param command the runnable task 330 */ executeOnSingleThreadedExecutor(Runnable command)331 private void executeOnSingleThreadedExecutor(Runnable command) { 332 synchronized (mSingleThreadedExecutor) { 333 if (mSingleThreadedExecutor.isShutdown()) { 334 Log.w(TAG, "Executor is shutdown, not executing task"); 335 return; 336 } 337 mSingleThreadedExecutor.execute( 338 () -> { 339 try { 340 command.run(); 341 } catch (RuntimeException e) { 342 Slog.wtf( 343 TAG, 344 "AppsIndexerUserInstance" 345 + ".executeOnSingleThreadedExecutor() failed ", 346 e); 347 } 348 }); 349 } 350 } 351 logStats(@onNull AppsUpdateStats appsUpdateStats)352 private void logStats(@NonNull AppsUpdateStats appsUpdateStats) { 353 Objects.requireNonNull(appsUpdateStats); 354 int[] updateStatusArr = new int[appsUpdateStats.mUpdateStatusCodes.size()]; 355 int updateIdx = 0; 356 for (int updateStatus : appsUpdateStats.mUpdateStatusCodes) { 357 updateStatusArr[updateIdx] = updateStatus; 358 ++updateIdx; 359 } 360 AppSearchStatsLog.write( 361 AppSearchStatsLog.APP_SEARCH_APPS_INDEXER_STATS_REPORTED, 362 appsUpdateStats.mUpdateType, 363 updateStatusArr, 364 appsUpdateStats.mNumberOfAppsAdded, 365 appsUpdateStats.mNumberOfAppsRemoved, 366 appsUpdateStats.mNumberOfAppsUpdated, 367 appsUpdateStats.mNumberOfAppsUnchanged, 368 appsUpdateStats.mTotalLatencyMillis, 369 appsUpdateStats.mPackageManagerLatencyMillis, 370 appsUpdateStats.mAppSearchGetLatencyMillis, 371 appsUpdateStats.mAppSearchSetSchemaLatencyMillis, 372 appsUpdateStats.mAppSearchPutLatencyMillis, 373 appsUpdateStats.mUpdateStartTimestampMillis, 374 appsUpdateStats.mLastAppUpdateTimestampMillis, 375 appsUpdateStats.mNumberOfFunctionsAdded, 376 appsUpdateStats.mApproximateNumberOfFunctionsRemoved, 377 appsUpdateStats.mNumberOfFunctionsUpdated, 378 appsUpdateStats.mApproximateNumberOfFunctionsUnchanged, 379 appsUpdateStats.mAppSearchRemoveLatencyMillis); 380 } 381 } 382