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 android.os.Process.INVALID_UID; 20 21 import android.annotation.BinderThread; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.app.appsearch.AppSearchEnvironment; 25 import android.app.appsearch.AppSearchEnvironmentFactory; 26 import android.app.appsearch.exceptions.AppSearchException; 27 import android.app.appsearch.util.LogUtil; 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.os.CancellationSignal; 33 import android.os.UserHandle; 34 import android.util.ArrayMap; 35 import android.util.Log; 36 import android.util.Slog; 37 38 import com.android.internal.annotations.GuardedBy; 39 import com.android.server.LocalManagerRegistry; 40 import com.android.server.SystemService; 41 import com.android.server.appsearch.indexer.IndexerLocalService; 42 43 import java.io.File; 44 import java.io.PrintWriter; 45 import java.util.Map; 46 import java.util.Objects; 47 48 /** 49 * Manages the per device-user AppsIndexer instance to index apps into AppSearch. 50 * 51 * <p>This class is thread-safe. 52 * 53 * @hide 54 */ 55 public final class AppsIndexerManagerService extends SystemService { 56 private static final String TAG = "AppSearchAppsIndexerManagerS"; 57 58 private final Context mContext; 59 private final LocalService mLocalService; 60 61 // Map of AppsIndexerUserInstances indexed by the UserHandle 62 @GuardedBy("mAppsIndexersLocked") 63 private final Map<UserHandle, AppsIndexerUserInstance> mAppsIndexersLocked = new ArrayMap<>(); 64 65 private final AppsIndexerConfig mAppsIndexerConfig; 66 67 /** Constructs a {@link AppsIndexerManagerService}. */ AppsIndexerManagerService( @onNull Context context, @NonNull AppsIndexerConfig appsIndexerConfig)68 public AppsIndexerManagerService( 69 @NonNull Context context, @NonNull AppsIndexerConfig appsIndexerConfig) { 70 super(context); 71 mContext = Objects.requireNonNull(context); 72 mAppsIndexerConfig = Objects.requireNonNull(appsIndexerConfig); 73 mLocalService = new LocalService(); 74 } 75 76 @Override onStart()77 public void onStart() { 78 registerReceivers(); 79 LocalManagerRegistry.addManager(LocalService.class, mLocalService); 80 } 81 82 /** Runs when a user is unlocked. This will attempt to run an initial sync. */ 83 @Override onUserUnlocking(@onNull TargetUser user)84 public void onUserUnlocking(@NonNull TargetUser user) { 85 try { 86 Objects.requireNonNull(user); 87 UserHandle userHandle = user.getUserHandle(); 88 synchronized (mAppsIndexersLocked) { 89 AppsIndexerUserInstance instance = mAppsIndexersLocked.get(userHandle); 90 if (instance == null) { 91 AppSearchEnvironment appSearchEnvironment = 92 AppSearchEnvironmentFactory.getEnvironmentInstance(); 93 Context userContext = 94 appSearchEnvironment.createContextAsUser(mContext, userHandle); 95 File appSearchDir = 96 appSearchEnvironment.getAppSearchDir(userContext, userHandle); 97 File appsDir = new File(appSearchDir, "apps"); 98 instance = 99 AppsIndexerUserInstance.createInstance( 100 userContext, appsDir, mAppsIndexerConfig); 101 if (LogUtil.DEBUG) { 102 Log.d(TAG, "Created Apps Indexer instance for user " + userHandle); 103 } 104 mAppsIndexersLocked.put(userHandle, instance); 105 } 106 107 instance.updateAsync(/* firstRun= */ true); 108 } 109 } catch (RuntimeException e) { 110 Slog.wtf(TAG, "AppsIndexerManagerService.onUserUnlocking() failed ", e); 111 } catch (AppSearchException e) { 112 Log.e(TAG, "Error while start Apps Indexer", e); 113 } 114 } 115 116 /** Handles user stopping by shutting down the instance for the user. */ 117 @Override onUserStopping(@onNull TargetUser user)118 public void onUserStopping(@NonNull TargetUser user) { 119 try { 120 Objects.requireNonNull(user); 121 UserHandle userHandle = user.getUserHandle(); 122 synchronized (mAppsIndexersLocked) { 123 AppsIndexerUserInstance instance = mAppsIndexersLocked.get(userHandle); 124 if (instance != null) { 125 mAppsIndexersLocked.remove(userHandle); 126 try { 127 instance.shutdown(); 128 } catch (InterruptedException e) { 129 Log.w(TAG, "Failed to shutdown apps indexer for " + userHandle, e); 130 } 131 } 132 } 133 } catch (RuntimeException e) { 134 Slog.wtf(TAG, "AppsIndexerManagerService.onUserStopping() failed ", e); 135 } 136 } 137 138 /** Dumps AppsIndexer internal state for the user. */ 139 @BinderThread dumpAppsIndexerForUser(@onNull UserHandle userHandle, @NonNull PrintWriter pw)140 public void dumpAppsIndexerForUser(@NonNull UserHandle userHandle, @NonNull PrintWriter pw) { 141 try { 142 Objects.requireNonNull(userHandle); 143 Objects.requireNonNull(pw); 144 synchronized (mAppsIndexersLocked) { 145 AppsIndexerUserInstance instance = mAppsIndexersLocked.get(userHandle); 146 if (instance != null) { 147 instance.dump(pw); 148 } else { 149 pw.println("AppsIndexerUserInstance is not created for " + userHandle); 150 } 151 } 152 } catch (RuntimeException e) { 153 Slog.wtf(TAG, "AppsIndexerManagerService.dumpAppsIndexerForUser() failed ", e); 154 } 155 } 156 157 /** 158 * Registers a broadcast receiver to get package changed (disabled/enabled) and package data 159 * cleared events. 160 */ registerReceivers()161 private void registerReceivers() { 162 IntentFilter appChangedFilter = new IntentFilter(); 163 appChangedFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); 164 appChangedFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 165 appChangedFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); 166 appChangedFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); 167 appChangedFilter.addDataScheme("package"); 168 169 mContext.registerReceiverForAllUsers( 170 new AppsProviderChangedReceiver(), 171 appChangedFilter, 172 /* broadcastPermission= */ null, 173 /* scheduler= */ null); 174 if (LogUtil.DEBUG) { 175 Log.v(TAG, "Registered receiver for package events"); 176 } 177 } 178 179 /** 180 * Broadcast receiver to handle package events and index them into the AppSearch 181 * "builtin:MobileApplication" schema. 182 * 183 * <p>This broadcast receiver allows the apps indexer to listen to events which indicate that 184 * app info was changed. 185 */ 186 private class AppsProviderChangedReceiver extends BroadcastReceiver { 187 188 /** 189 * Checks if the entire package was changed, or if the intent just represents a component 190 * change. 191 */ isEntirePackageChanged(@onNull Intent intent)192 private boolean isEntirePackageChanged(@NonNull Intent intent) { 193 Objects.requireNonNull(intent); 194 String[] changedComponents = 195 intent.getStringArrayExtra(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST); 196 if (changedComponents == null) { 197 Log.e(TAG, "Received ACTION_PACKAGE_CHANGED event with null changed components"); 198 return false; 199 } 200 if (intent.getData() == null) { 201 Log.e(TAG, "Received ACTION_PACKAGE_CHANGED event with null data"); 202 return false; 203 } 204 String changedPackage = intent.getData().getSchemeSpecificPart(); 205 for (int i = 0; i < changedComponents.length; i++) { 206 String changedComponent = changedComponents[i]; 207 // If the state of the overall package has changed, then it will contain 208 // an entry with the package name itself. 209 if (changedComponent.equals(changedPackage)) { 210 return true; 211 } 212 } 213 return false; 214 } 215 216 /** Handles intents related to package changes. */ 217 @Override onReceive(@onNull Context context, @NonNull Intent intent)218 public void onReceive(@NonNull Context context, @NonNull Intent intent) { 219 try { 220 Objects.requireNonNull(context); 221 Objects.requireNonNull(intent); 222 223 switch (intent.getAction()) { 224 case Intent.ACTION_PACKAGE_CHANGED: 225 if (!isEntirePackageChanged(intent)) { 226 // If it was just a component change, do not run the indexer 227 return; 228 } 229 // fall through 230 case Intent.ACTION_PACKAGE_ADDED: 231 case Intent.ACTION_PACKAGE_REPLACED: 232 case Intent.ACTION_PACKAGE_FULLY_REMOVED: 233 // TODO(b/275592563): handle more efficiently based on package event type 234 // TODO(b/275592563): determine if batching is necessary in the case of 235 // rapid updates 236 237 int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID); 238 if (uid == INVALID_UID) { 239 Log.w(TAG, "uid is missing in the intent: " + intent); 240 return; 241 } 242 Log.d(TAG, "userid in package receiver: " + uid); 243 UserHandle userHandle = UserHandle.getUserHandleForUid(uid); 244 mLocalService.doUpdateForUser(userHandle, /* unused= */ null); 245 break; 246 default: 247 Log.w(TAG, "Received unknown intent: " + intent); 248 } 249 } catch (RuntimeException e) { 250 Slog.wtf(TAG, "AppsProviderChangedReceiver.onReceive() failed ", e); 251 } 252 } 253 } 254 255 public class LocalService implements IndexerLocalService { 256 /** Runs an update for a user. */ 257 @Override doUpdateForUser( @onNull UserHandle userHandle, @Nullable CancellationSignal unused)258 public void doUpdateForUser( 259 @NonNull UserHandle userHandle, @Nullable CancellationSignal unused) { 260 // TODO(b/275592563): handle cancellation signal to abort the job. 261 Objects.requireNonNull(userHandle); 262 synchronized (mAppsIndexersLocked) { 263 AppsIndexerUserInstance instance = mAppsIndexersLocked.get(userHandle); 264 if (instance != null) { 265 instance.updateAsync(/* firstRun= */ false); 266 } 267 } 268 } 269 } 270 } 271