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