• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.contactsindexer;
18 
19 import static android.os.Process.INVALID_UID;
20 
21 import android.annotation.BinderThread;
22 import android.annotation.NonNull;
23 import android.app.appsearch.AppSearchEnvironment;
24 import android.app.appsearch.AppSearchEnvironmentFactory;
25 import android.app.appsearch.util.LogUtil;
26 import android.content.BroadcastReceiver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.content.pm.PackageManager;
31 import android.content.pm.ProviderInfo;
32 import android.os.CancellationSignal;
33 import android.os.PatternMatcher;
34 import android.os.UserHandle;
35 import android.provider.ContactsContract;
36 import android.util.ArrayMap;
37 import android.util.Log;
38 import android.util.Slog;
39 
40 import com.android.server.LocalManagerRegistry;
41 import com.android.server.SystemService;
42 import com.android.server.appsearch.indexer.IndexerLocalService;
43 
44 import java.io.File;
45 import java.io.PrintWriter;
46 import java.util.List;
47 import java.util.Map;
48 import java.util.Objects;
49 
50 /**
51  * Manages the per device-user ContactsIndexer instance to index CP2 contacts into AppSearch.
52  *
53  * <p>This class is thread-safe.
54  *
55  * @hide
56  */
57 public final class ContactsIndexerManagerService extends SystemService {
58     static final String TAG = "ContactsIndexerManagerS";
59 
60     private static final String DEFAULT_CONTACTS_PROVIDER_PACKAGE_NAME =
61             "com.android.providers.contacts";
62 
63     private final Context mContext;
64     private final ContactsIndexerConfig mContactsIndexerConfig;
65     private final LocalService mLocalService;
66     // Sparse array of ContactsIndexerUserInstance indexed by the device-user ID.
67     private final Map<UserHandle, ContactsIndexerUserInstance> mContactsIndexersLocked =
68             new ArrayMap<>();
69 
70     private String mContactsProviderPackageName;
71 
72     /** Constructs a {@link ContactsIndexerManagerService}. */
ContactsIndexerManagerService( @onNull Context context, @NonNull ContactsIndexerConfig contactsIndexerConfig)73     public ContactsIndexerManagerService(
74             @NonNull Context context, @NonNull ContactsIndexerConfig contactsIndexerConfig) {
75         super(context);
76         mContext = Objects.requireNonNull(context);
77         mContactsIndexerConfig = Objects.requireNonNull(contactsIndexerConfig);
78         mLocalService = new LocalService();
79     }
80 
81     @Override
onStart()82     public void onStart() {
83         mContactsProviderPackageName = getContactsProviderPackageName();
84         registerReceivers();
85         LocalManagerRegistry.addManager(LocalService.class, mLocalService);
86     }
87 
88     @Override
onUserUnlocking(@onNull TargetUser user)89     public void onUserUnlocking(@NonNull TargetUser user) {
90         try {
91             Objects.requireNonNull(user);
92             UserHandle userHandle = user.getUserHandle();
93             synchronized (mContactsIndexersLocked) {
94                 ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userHandle);
95                 if (instance == null) {
96                     AppSearchEnvironment appSearchEnvironment =
97                             AppSearchEnvironmentFactory.getEnvironmentInstance();
98                     Context userContext =
99                             appSearchEnvironment.createContextAsUser(mContext, userHandle);
100                     File appSearchDir =
101                             appSearchEnvironment.getAppSearchDir(userContext, userHandle);
102                     File contactsDir = new File(appSearchDir, "contacts");
103                     instance =
104                             ContactsIndexerUserInstance.createInstance(
105                                     userContext, contactsDir, mContactsIndexerConfig);
106                     if (LogUtil.DEBUG) {
107                         Log.d(TAG, "Created Contacts Indexer instance for user " + userHandle);
108                     }
109                     mContactsIndexersLocked.put(userHandle, instance);
110                 }
111                 instance.startAsync();
112             }
113         } catch (RuntimeException e) {
114             Slog.wtf(TAG, "ContactsIndexerManagerService.onUserUnlocking() failed ", e);
115         }
116     }
117 
118     @Override
onUserStopping(@onNull TargetUser user)119     public void onUserStopping(@NonNull TargetUser user) {
120         try {
121             Objects.requireNonNull(user);
122             UserHandle userHandle = user.getUserHandle();
123             synchronized (mContactsIndexersLocked) {
124                 ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userHandle);
125                 if (instance != null) {
126                     mContactsIndexersLocked.remove(userHandle);
127                     try {
128                         instance.shutdown();
129                     } catch (InterruptedException e) {
130                         Log.w(TAG, "Failed to shutdown contacts indexer for " + userHandle, e);
131                     }
132                 }
133             }
134         } catch (RuntimeException e) {
135             Slog.wtf(TAG, "ContactsIndexerManagerService.onUserStopping() failed ", e);
136         }
137     }
138 
139     /** Dumps ContactsIndexer internal state for the user. */
140     @BinderThread
dumpContactsIndexerForUser( @onNull UserHandle userHandle, @NonNull PrintWriter pw, boolean verbose)141     public void dumpContactsIndexerForUser(
142             @NonNull UserHandle userHandle, @NonNull PrintWriter pw, boolean verbose) {
143         try {
144             Objects.requireNonNull(userHandle);
145             Objects.requireNonNull(pw);
146             synchronized (mContactsIndexersLocked) {
147                 ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userHandle);
148                 if (instance != null) {
149                     instance.dump(pw, verbose);
150                 } else {
151                     pw.println("ContactsIndexerUserInstance is not created for " + userHandle);
152                 }
153             }
154         } catch (RuntimeException e) {
155             Slog.wtf(TAG, "ContactsIndexerManagerService.dumpContactsIndexerForUser() failed ", e);
156         }
157     }
158 
159     /** Returns the package name where the Contacts Provider is hosted. */
getContactsProviderPackageName()160     private String getContactsProviderPackageName() {
161         PackageManager pm = mContext.getPackageManager();
162         List<ProviderInfo> providers =
163                 pm.queryContentProviders(
164                         /* processName= */ null,
165                         /* uid= */ 0,
166                         PackageManager.ComponentInfoFlags.of(0));
167         for (int i = 0; i < providers.size(); i++) {
168             ProviderInfo providerInfo = providers.get(i);
169             if (ContactsContract.AUTHORITY.equals(providerInfo.authority)) {
170                 return providerInfo.packageName;
171             }
172         }
173         return DEFAULT_CONTACTS_PROVIDER_PACKAGE_NAME;
174     }
175 
176     /**
177      * Registers a broadcast receiver to get package changed (disabled/enabled) and package data
178      * cleared events for CP2.
179      */
registerReceivers()180     private void registerReceivers() {
181         IntentFilter contactsProviderChangedFilter = new IntentFilter();
182         contactsProviderChangedFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
183         contactsProviderChangedFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
184         contactsProviderChangedFilter.addDataScheme("package");
185         contactsProviderChangedFilter.addDataSchemeSpecificPart(
186                 mContactsProviderPackageName, PatternMatcher.PATTERN_LITERAL);
187         mContext.registerReceiverForAllUsers(
188                 new ContactsProviderChangedReceiver(),
189                 contactsProviderChangedFilter,
190                 /* broadcastPermission= */ null,
191                 /* scheduler= */ null);
192         if (LogUtil.DEBUG) {
193             Log.v(
194                     TAG,
195                     "Registered receiver for CP2 (package: "
196                             + mContactsProviderPackageName
197                             + ")"
198                             + " data cleared events");
199         }
200     }
201 
202     /**
203      * Broadcast receiver to handle CP2 changed (disabled/enabled) and package data cleared events.
204      *
205      * <p>Contacts indexer syncs on-device contacts from ContactsProvider (CP2) denoted by {@link
206      * android.provider.ContactsContract.Contacts#AUTHORITY} into the AppSearch "builtin:Person"
207      * corpus under the "android" package name. The default package which hosts CP2 is
208      * "com.android.providers.contacts" but it could be different on OEM devices. Since the Android
209      * package that hosts CP2 is different from the package name that "owns" the builtin:Person
210      * corpus in AppSearch, clearing the CP2 package data doesn't automatically clear the
211      * builtin:Person corpus in AppSearch.
212      *
213      * <p>This broadcast receiver allows contacts indexer to listen to events which indicate that
214      * CP2 data was cleared and force a full sync of CP2 contacts into AppSearch.
215      */
216     private class ContactsProviderChangedReceiver extends BroadcastReceiver {
217 
218         @Override
onReceive(@onNull Context context, @NonNull Intent intent)219         public void onReceive(@NonNull Context context, @NonNull Intent intent) {
220             try {
221                 Objects.requireNonNull(context);
222                 Objects.requireNonNull(intent);
223 
224                 switch (intent.getAction()) {
225                     case Intent.ACTION_PACKAGE_CHANGED:
226                     case Intent.ACTION_PACKAGE_DATA_CLEARED:
227                         String packageName = intent.getData().getSchemeSpecificPart();
228                         if (LogUtil.DEBUG) {
229                             Log.v(TAG, "Received package data cleared event for " + packageName);
230                         }
231                         if (!mContactsProviderPackageName.equals(packageName)) {
232                             return;
233                         }
234                         int uid = intent.getIntExtra(Intent.EXTRA_UID, INVALID_UID);
235                         if (uid == INVALID_UID) {
236                             Log.w(TAG, "uid is missing in the intent: " + intent);
237                             return;
238                         }
239                         mLocalService.doUpdateForUser(
240                                 UserHandle.getUserHandleForUid(uid), new CancellationSignal());
241                         break;
242                     default:
243                         Log.w(TAG, "Received unknown intent: " + intent);
244                 }
245             } catch (RuntimeException e) {
246                 Slog.wtf(TAG, "ContactsProviderChangedReceiver.onReceive() failed ", e);
247             }
248         }
249     }
250 
251     public class LocalService implements IndexerLocalService {
252 
253         /** Runs a full update for the user. */
254         @Override
doUpdateForUser( @onNull UserHandle userHandle, @NonNull CancellationSignal signal)255         public void doUpdateForUser(
256                 @NonNull UserHandle userHandle, @NonNull CancellationSignal signal) {
257             Objects.requireNonNull(signal);
258             synchronized (mContactsIndexersLocked) {
259                 ContactsIndexerUserInstance instance = mContactsIndexersLocked.get(userHandle);
260                 if (instance != null) {
261                     instance.doFullUpdateAsync(signal);
262                 }
263             }
264         }
265     }
266 }
267