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