• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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