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