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.indexer.IndexerMaintenanceConfig.APPS_INDEXER;
20 
21 import android.annotation.NonNull;
22 import android.app.appsearch.AppSearchEnvironmentFactory;
23 import android.app.appsearch.exceptions.AppSearchException;
24 import android.content.Context;
25 import android.util.Log;
26 import android.util.Slog;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.server.appsearch.indexer.IndexerMaintenanceService;
30 
31 import java.io.File;
32 import java.io.FileNotFoundException;
33 import java.io.IOException;
34 import java.io.PrintWriter;
35 import java.util.Objects;
36 import java.util.concurrent.ExecutorService;
37 import java.util.concurrent.Semaphore;
38 import java.util.concurrent.TimeUnit;
39 
40 /**
41  * Apps Indexer for a single user.
42  *
43  * <p>It reads the updated/newly-inserted/deleted apps from PackageManager, and syncs the changes
44  * into AppSearch.
45  *
46  * <p>This class is thread safe.
47  *
48  * @hide
49  */
50 public final class AppsIndexerUserInstance {
51 
52     private static final String TAG = "AppSearchAppsIndexerUserInst";
53 
54     private final File mDataDir;
55     // While AppsIndexerSettings is not thread safe, it is only accessed through a single-threaded
56     // executor service. It will be read and updated before the next scheduled task accesses it.
57     private final AppsIndexerSettings mSettings;
58 
59     // Used for handling the app change notification so we won't schedule too many updates. At any
60     // time, only two threads can run an update. But since we use a single-threaded executor, it
61     // means that at most one thread can be running, and another thread can be waiting to run. This
62     // will happen in the case that an update is requested while another is running.
63     private final Semaphore mRunningOrScheduledSemaphore = new Semaphore(2);
64 
65     private AppsIndexerImpl mAppsIndexerImpl;
66 
67     /**
68      * Single threaded executor to make sure there is only one active sync for this {@link
69      * AppsIndexerUserInstance}. Background tasks should be scheduled using {@link
70      * #executeOnSingleThreadedExecutor(Runnable)} which ensures that they are not executed if the
71      * executor is shutdown during {@link #shutdown()}.
72      *
73      * <p>Note that this executor is used as both work and callback executors which is fine because
74      * AppSearch should be able to handle exceptions thrown by them.
75      */
76     private final ExecutorService mSingleThreadedExecutor;
77 
78     private final Context mContext;
79     private final AppsIndexerConfig mAppsIndexerConfig;
80 
81     /**
82      * Constructs and initializes a {@link AppsIndexerUserInstance}.
83      *
84      * <p>Heavy operations such as connecting to AppSearch are performed asynchronously.
85      *
86      * @param appsDir data directory for AppsIndexer.
87      */
88     @NonNull
createInstance( @onNull Context userContext, @NonNull File appsDir, @NonNull AppsIndexerConfig appsIndexerConfig)89     public static AppsIndexerUserInstance createInstance(
90             @NonNull Context userContext,
91             @NonNull File appsDir,
92             @NonNull AppsIndexerConfig appsIndexerConfig)
93             throws AppSearchException {
94         Objects.requireNonNull(userContext);
95         Objects.requireNonNull(appsDir);
96         Objects.requireNonNull(appsIndexerConfig);
97 
98         ExecutorService singleThreadedExecutor =
99                 AppSearchEnvironmentFactory.getEnvironmentInstance().createSingleThreadExecutor();
100         return createInstance(userContext, appsDir, appsIndexerConfig, singleThreadedExecutor);
101     }
102 
103     @VisibleForTesting
104     @NonNull
createInstance( @onNull Context context, @NonNull File appsDir, @NonNull AppsIndexerConfig appsIndexerConfig, @NonNull ExecutorService executorService)105     static AppsIndexerUserInstance createInstance(
106             @NonNull Context context,
107             @NonNull File appsDir,
108             @NonNull AppsIndexerConfig appsIndexerConfig,
109             @NonNull ExecutorService executorService)
110             throws AppSearchException {
111         Objects.requireNonNull(context);
112         Objects.requireNonNull(appsDir);
113         Objects.requireNonNull(appsIndexerConfig);
114         Objects.requireNonNull(executorService);
115 
116         AppsIndexerUserInstance indexer =
117                 new AppsIndexerUserInstance(appsDir, executorService, context, appsIndexerConfig);
118         indexer.loadSettingsAsync();
119         indexer.mAppsIndexerImpl = new AppsIndexerImpl(context);
120 
121         return indexer;
122     }
123 
124     /**
125      * Constructs a {@link AppsIndexerUserInstance}.
126      *
127      * @param dataDir data directory for storing apps indexer state.
128      * @param singleThreadedExecutor an {@link ExecutorService} with at most one thread to ensure
129      *     the thread safety of this class.
130      * @param context Context object passed from {@link AppsIndexerManagerService}
131      */
AppsIndexerUserInstance( @onNull File dataDir, @NonNull ExecutorService singleThreadedExecutor, @NonNull Context context, @NonNull AppsIndexerConfig appsIndexerConfig)132     private AppsIndexerUserInstance(
133             @NonNull File dataDir,
134             @NonNull ExecutorService singleThreadedExecutor,
135             @NonNull Context context,
136             @NonNull AppsIndexerConfig appsIndexerConfig) {
137         mDataDir = Objects.requireNonNull(dataDir);
138         mSettings = new AppsIndexerSettings(mDataDir);
139         mSingleThreadedExecutor = Objects.requireNonNull(singleThreadedExecutor);
140         mContext = Objects.requireNonNull(context);
141         mAppsIndexerConfig = Objects.requireNonNull(appsIndexerConfig);
142     }
143 
144     /** Shuts down the AppsIndexerUserInstance */
shutdown()145     public void shutdown() throws InterruptedException {
146         mAppsIndexerImpl.close();
147         IndexerMaintenanceService.cancelUpdateJobIfScheduled(
148                 mContext, mContext.getUser(), APPS_INDEXER);
149         synchronized (mSingleThreadedExecutor) {
150             mSingleThreadedExecutor.shutdown();
151         }
152         boolean unused = mSingleThreadedExecutor.awaitTermination(30L, TimeUnit.SECONDS);
153     }
154 
155     /** Dumps the internal state of this {@link AppsIndexerUserInstance}. */
dump(@onNull PrintWriter pw)156     public void dump(@NonNull PrintWriter pw) {
157         // Those timestamps are not protected by any lock since in AppsIndexerUserInstance
158         // we only have one thread to handle all the updates. It is possible we might run into
159         // race condition if there is an update running while those numbers are being printed.
160         // This is acceptable though for debug purpose, so still no lock here.
161         pw.println("last_update_timestamp_millis: " + mSettings.getLastUpdateTimestampMillis());
162         pw.println(
163                 "last_app_update_timestamp_millis: " + mSettings.getLastAppUpdateTimestampMillis());
164     }
165 
166     /**
167      * Schedule an update. No new update can be scheduled if there are two updates already scheduled
168      * or currently being run.
169      *
170      * @param firstRun boolean indicating if this is a first run and that settings should be checked
171      *     for the last update timestamp.
172      */
updateAsync(boolean firstRun)173     public void updateAsync(boolean firstRun) {
174         // Try to acquire a permit.
175         if (!mRunningOrScheduledSemaphore.tryAcquire()) {
176             // If there are none available, that means an update is running and we have ALREADY
177             // received a change mid-update. The third update request was received during the first
178             // update, and will be handled by the scheduled update.
179             return;
180         }
181         // If there is a permit available, that cold mean there is one update running right now
182         // with none scheduled. Since we use a single threaded executor, calling execute on it
183         // right now will run the requested update after the current update. It could also mean
184         // there is no update running right now, so we can just call execute and run the update
185         // right now.
186         executeOnSingleThreadedExecutor(
187                 () -> {
188                     doUpdate(firstRun);
189                     IndexerMaintenanceService.scheduleUpdateJob(
190                             mContext,
191                             mContext.getUser(),
192                             APPS_INDEXER,
193                             /* periodic= */ true,
194                             /* intervalMillis= */ mAppsIndexerConfig
195                                     .getAppsMaintenanceUpdateIntervalMillis());
196                 });
197     }
198 
199     /**
200      * Does the update. It also releases a permit from {@link #mRunningOrScheduledSemaphore}
201      *
202      * @param firstRun when set to true, that means this was called from onUserUnlocking. If we
203      *     didn't have this check, the apps indexer would run every time the phone got unlocked. It
204      *     should only run the first time this happens.
205      */
206     @VisibleForTesting
doUpdate(boolean firstRun)207     void doUpdate(boolean firstRun) {
208         try {
209             // Check if there was a prior run
210             if (firstRun && mSettings.getLastUpdateTimestampMillis() != 0) {
211                 return;
212             }
213             mAppsIndexerImpl.doUpdate(mSettings);
214             mSettings.persist();
215         } catch (IOException e) {
216             Log.w(TAG, "Failed to save settings to disk", e);
217         } catch (AppSearchException e) {
218             Log.e(TAG, "Failed to sync Apps to AppSearch", e);
219         } finally {
220             // Finish a update. If there were no permits available, the update that was requested
221             // mid-update will run. If there was one permit available, we won't run another update.
222             // This happens if no updates were scheduled during the update.
223             mRunningOrScheduledSemaphore.release();
224         }
225     }
226 
227     /**
228      * Loads the persisted data from disk.
229      *
230      * <p>It doesn't throw here. If it fails to load file, AppsIndexer would always use the
231      * timestamps persisted in the memory.
232      */
loadSettingsAsync()233     private void loadSettingsAsync() {
234         executeOnSingleThreadedExecutor(
235                 () -> {
236                     try {
237                         // If the directory already exists, this returns false. That is fine as it
238                         // might not be the first sync. If this returns true, that is fine as it is
239                         // the first run and we want to make a new directory.
240                         mDataDir.mkdirs();
241                     } catch (SecurityException e) {
242                         Log.e(TAG, "Failed to create settings directory on disk.", e);
243                         return;
244                     }
245 
246                     try {
247                         mSettings.load();
248                     } catch (IOException e) {
249                         // Ignore file not found errors (bootstrap case)
250                         if (!(e instanceof FileNotFoundException)) {
251                             Log.e(TAG, "Failed to load settings from disk", e);
252                         }
253                     }
254                 });
255     }
256 
257     /**
258      * Executes the given command on {@link #mSingleThreadedExecutor} if it is still alive.
259      *
260      * <p>If the {@link #mSingleThreadedExecutor} has been shutdown, this method doesn't execute the
261      * given command, and returns silently. Specifically, it does not throw {@link
262      * java.util.concurrent.RejectedExecutionException}.
263      *
264      * @param command the runnable task
265      */
executeOnSingleThreadedExecutor(Runnable command)266     private void executeOnSingleThreadedExecutor(Runnable command) {
267         synchronized (mSingleThreadedExecutor) {
268             if (mSingleThreadedExecutor.isShutdown()) {
269                 Log.w(TAG, "Executor is shutdown, not executing task");
270                 return;
271             }
272             mSingleThreadedExecutor.execute(
273                     () -> {
274                         try {
275                             command.run();
276                         } catch (RuntimeException e) {
277                             Slog.wtf(
278                                     TAG,
279                                     "AppsIndexerUserInstance"
280                                             + ".executeOnSingleThreadedExecutor() failed ",
281                                     e);
282                         }
283                     });
284         }
285     }
286 }
287