/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.content.pm; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.os.Environment; import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; import android.util.AtomicFile; import android.util.AttributeSet; import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastXmlSerializer; import com.google.android.collect.Lists; import com.google.android.collect.Maps; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import libcore.io.IoUtils; /** * Cache of registered services. This cache is lazily built by interrogating * {@link PackageManager} on a per-user basis. It's updated as packages are * added, removed and changed. Users are responsible for calling * {@link #invalidateCache(int)} when a user is started, since * {@link PackageManager} broadcasts aren't sent for stopped users. *

* The services are referred to by type V and are made available via the * {@link #getServiceInfo} method. * * @hide */ public abstract class RegisteredServicesCache { private static final String TAG = "PackageManager"; private static final boolean DEBUG = false; protected static final String REGISTERED_SERVICES_DIR = "registered_services"; public final Context mContext; private final String mInterfaceName; private final String mMetaDataName; private final String mAttributesName; private final XmlSerializerAndParser mSerializerAndParser; protected final Object mServicesLock = new Object(); @GuardedBy("mServicesLock") private final SparseArray> mUserServices = new SparseArray>(2); private static class UserServices { @GuardedBy("mServicesLock") final Map persistentServices = Maps.newHashMap(); @GuardedBy("mServicesLock") Map> services = null; @GuardedBy("mServicesLock") boolean mPersistentServicesFileDidNotExist = true; } @GuardedBy("mServicesLock") private UserServices findOrCreateUserLocked(int userId) { return findOrCreateUserLocked(userId, true); } @GuardedBy("mServicesLock") private UserServices findOrCreateUserLocked(int userId, boolean loadFromFileIfNew) { UserServices services = mUserServices.get(userId); if (services == null) { services = new UserServices(); mUserServices.put(userId, services); if (loadFromFileIfNew && mSerializerAndParser != null) { // Check if user exists and try loading data from file // clear existing data if there was an error during migration UserInfo user = getUser(userId); if (user != null) { AtomicFile file = createFileForUser(user.id); if (file.getBaseFile().exists()) { if (DEBUG) { Slog.i(TAG, String.format("Loading u%s data from %s", user.id, file)); } InputStream is = null; try { is = file.openRead(); readPersistentServicesLocked(is); } catch (Exception e) { Log.w(TAG, "Error reading persistent services for user " + user.id, e); } finally { IoUtils.closeQuietly(is); } } } } } return services; } // the listener and handler are synchronized on "this" and must be updated together private RegisteredServicesCacheListener mListener; private Handler mHandler; public RegisteredServicesCache(Context context, String interfaceName, String metaDataName, String attributeName, XmlSerializerAndParser serializerAndParser) { mContext = context; mInterfaceName = interfaceName; mMetaDataName = metaDataName; mAttributesName = attributeName; mSerializerAndParser = serializerAndParser; migrateIfNecessaryLocked(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); intentFilter.addDataScheme("package"); mContext.registerReceiverAsUser(mPackageReceiver, UserHandle.ALL, intentFilter, null, null); // Register for events related to sdcard installation. IntentFilter sdFilter = new IntentFilter(); sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); mContext.registerReceiver(mExternalReceiver, sdFilter); // Register for user-related events IntentFilter userFilter = new IntentFilter(); sdFilter.addAction(Intent.ACTION_USER_REMOVED); mContext.registerReceiver(mUserRemovedReceiver, userFilter); } private final void handlePackageEvent(Intent intent, int userId) { // Don't regenerate the services map when the package is removed or its // ASEC container unmounted as a step in replacement. The subsequent // _ADDED / _AVAILABLE call will regenerate the map in the final state. final String action = intent.getAction(); // it's a new-component action if it isn't some sort of removal final boolean isRemoval = Intent.ACTION_PACKAGE_REMOVED.equals(action) || Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action); // if it's a removal, is it part of an update-in-place step? final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); if (isRemoval && replacing) { // package is going away, but it's the middle of an upgrade: keep the current // state and do nothing here. This clause is intentionally empty. } else { int[] uids = null; // either we're adding/changing, or it's a removal without replacement, so // we need to update the set of available services if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action) || Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { uids = intent.getIntArrayExtra(Intent.EXTRA_CHANGED_UID_LIST); } else { int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); if (uid > 0) { uids = new int[] { uid }; } } generateServicesMap(uids, userId); } } private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); if (uid != -1) { handlePackageEvent(intent, UserHandle.getUserId(uid)); } } }; private final BroadcastReceiver mExternalReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // External apps can't coexist with multi-user, so scan owner handlePackageEvent(intent, UserHandle.USER_SYSTEM); } }; private final BroadcastReceiver mUserRemovedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); if (DEBUG) { Slog.d(TAG, "u" + userId + " removed - cleaning up"); } onUserRemoved(userId); } }; public void invalidateCache(int userId) { synchronized (mServicesLock) { final UserServices user = findOrCreateUserLocked(userId); user.services = null; onServicesChangedLocked(userId); } } public void dump(FileDescriptor fd, PrintWriter fout, String[] args, int userId) { synchronized (mServicesLock) { final UserServices user = findOrCreateUserLocked(userId); if (user.services != null) { fout.println("RegisteredServicesCache: " + user.services.size() + " services"); for (ServiceInfo info : user.services.values()) { fout.println(" " + info); } } else { fout.println("RegisteredServicesCache: services not loaded"); } } } public RegisteredServicesCacheListener getListener() { synchronized (this) { return mListener; } } public void setListener(RegisteredServicesCacheListener listener, Handler handler) { if (handler == null) { handler = new Handler(mContext.getMainLooper()); } synchronized (this) { mHandler = handler; mListener = listener; } } private void notifyListener(final V type, final int userId, final boolean removed) { if (DEBUG) { Log.d(TAG, "notifyListener: " + type + " is " + (removed ? "removed" : "added")); } RegisteredServicesCacheListener listener; Handler handler; synchronized (this) { listener = mListener; handler = mHandler; } if (listener == null) { return; } final RegisteredServicesCacheListener listener2 = listener; handler.post(new Runnable() { public void run() { listener2.onServiceChanged(type, userId, removed); } }); } /** * Value type that describes a Service. The information within can be used * to bind to the service. */ public static class ServiceInfo { public final V type; public final ComponentInfo componentInfo; public final ComponentName componentName; public final int uid; /** @hide */ public ServiceInfo(V type, ComponentInfo componentInfo, ComponentName componentName) { this.type = type; this.componentInfo = componentInfo; this.componentName = componentName; this.uid = (componentInfo != null) ? componentInfo.applicationInfo.uid : -1; } @Override public String toString() { return "ServiceInfo: " + type + ", " + componentName + ", uid " + uid; } } /** * Accessor for the registered authenticators. * @param type the account type of the authenticator * @return the AuthenticatorInfo that matches the account type or null if none is present */ public ServiceInfo getServiceInfo(V type, int userId) { synchronized (mServicesLock) { // Find user and lazily populate cache final UserServices user = findOrCreateUserLocked(userId); if (user.services == null) { generateServicesMap(null, userId); } return user.services.get(type); } } /** * @return a collection of {@link RegisteredServicesCache.ServiceInfo} objects for all * registered authenticators. */ public Collection> getAllServices(int userId) { synchronized (mServicesLock) { // Find user and lazily populate cache final UserServices user = findOrCreateUserLocked(userId); if (user.services == null) { generateServicesMap(null, userId); } return Collections.unmodifiableCollection( new ArrayList>(user.services.values())); } } @VisibleForTesting protected boolean inSystemImage(int callerUid) { String[] packages = mContext.getPackageManager().getPackagesForUid(callerUid); for (String name : packages) { try { PackageInfo packageInfo = mContext.getPackageManager().getPackageInfo(name, 0 /* flags */); if ((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { return true; } } catch (PackageManager.NameNotFoundException e) { return false; } } return false; } @VisibleForTesting protected List queryIntentServices(int userId) { final PackageManager pm = mContext.getPackageManager(); return pm.queryIntentServicesAsUser(new Intent(mInterfaceName), PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId); } /** * Populate {@link UserServices#services} by scanning installed packages for * given {@link UserHandle}. * @param changedUids the array of uids that have been affected, as mentioned in the broadcast * or null to assume that everything is affected. * @param userId the user for whom to update the services map. */ private void generateServicesMap(int[] changedUids, int userId) { if (DEBUG) { Slog.d(TAG, "generateServicesMap() for " + userId + ", changed UIDs = " + changedUids); } final ArrayList> serviceInfos = new ArrayList>(); final List resolveInfos = queryIntentServices(userId); for (ResolveInfo resolveInfo : resolveInfos) { try { ServiceInfo info = parseServiceInfo(resolveInfo); if (info == null) { Log.w(TAG, "Unable to load service info " + resolveInfo.toString()); continue; } serviceInfos.add(info); } catch (XmlPullParserException|IOException e) { Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e); } } synchronized (mServicesLock) { final UserServices user = findOrCreateUserLocked(userId); final boolean firstScan = user.services == null; if (firstScan) { user.services = Maps.newHashMap(); } StringBuilder changes = new StringBuilder(); boolean changed = false; for (ServiceInfo info : serviceInfos) { // four cases: // - doesn't exist yet // - add, notify user that it was added // - exists and the UID is the same // - replace, don't notify user // - exists, the UID is different, and the new one is not a system package // - ignore // - exists, the UID is different, and the new one is a system package // - add, notify user that it was added Integer previousUid = user.persistentServices.get(info.type); if (previousUid == null) { if (DEBUG) { changes.append(" New service added: ").append(info).append("\n"); } changed = true; user.services.put(info.type, info); user.persistentServices.put(info.type, info.uid); if (!(user.mPersistentServicesFileDidNotExist && firstScan)) { notifyListener(info.type, userId, false /* removed */); } } else if (previousUid == info.uid) { if (DEBUG) { changes.append(" Existing service (nop): ").append(info).append("\n"); } user.services.put(info.type, info); } else if (inSystemImage(info.uid) || !containsTypeAndUid(serviceInfos, info.type, previousUid)) { if (DEBUG) { if (inSystemImage(info.uid)) { changes.append(" System service replacing existing: ").append(info) .append("\n"); } else { changes.append(" Existing service replacing a removed service: ") .append(info).append("\n"); } } changed = true; user.services.put(info.type, info); user.persistentServices.put(info.type, info.uid); notifyListener(info.type, userId, false /* removed */); } else { // ignore if (DEBUG) { changes.append(" Existing service with new uid ignored: ").append(info) .append("\n"); } } } ArrayList toBeRemoved = Lists.newArrayList(); for (V v1 : user.persistentServices.keySet()) { // Remove a persisted service that's not in the currently available services list. // And only if it is in the list of changedUids. if (!containsType(serviceInfos, v1) && containsUid(changedUids, user.persistentServices.get(v1))) { toBeRemoved.add(v1); } } for (V v1 : toBeRemoved) { if (DEBUG) { changes.append(" Service removed: ").append(v1).append("\n"); } changed = true; user.persistentServices.remove(v1); user.services.remove(v1); notifyListener(v1, userId, true /* removed */); } if (DEBUG) { Log.d(TAG, "user.services="); for (V v : user.services.keySet()) { Log.d(TAG, " " + v + " " + user.services.get(v)); } Log.d(TAG, "user.persistentServices="); for (V v : user.persistentServices.keySet()) { Log.d(TAG, " " + v + " " + user.persistentServices.get(v)); } } if (DEBUG) { if (changes.length() > 0) { Log.d(TAG, "generateServicesMap(" + mInterfaceName + "): " + serviceInfos.size() + " services:\n" + changes); } else { Log.d(TAG, "generateServicesMap(" + mInterfaceName + "): " + serviceInfos.size() + " services unchanged"); } } if (changed) { onServicesChangedLocked(userId); writePersistentServicesLocked(user, userId); } } } protected void onServicesChangedLocked(int userId) { // Feel free to override } /** * Returns true if the list of changed uids is null (wildcard) or the specified uid * is contained in the list of changed uids. */ private boolean containsUid(int[] changedUids, int uid) { return changedUids == null || ArrayUtils.contains(changedUids, uid); } private boolean containsType(ArrayList> serviceInfos, V type) { for (int i = 0, N = serviceInfos.size(); i < N; i++) { if (serviceInfos.get(i).type.equals(type)) { return true; } } return false; } private boolean containsTypeAndUid(ArrayList> serviceInfos, V type, int uid) { for (int i = 0, N = serviceInfos.size(); i < N; i++) { final ServiceInfo serviceInfo = serviceInfos.get(i); if (serviceInfo.type.equals(type) && serviceInfo.uid == uid) { return true; } } return false; } @VisibleForTesting protected ServiceInfo parseServiceInfo(ResolveInfo service) throws XmlPullParserException, IOException { android.content.pm.ServiceInfo si = service.serviceInfo; ComponentName componentName = new ComponentName(si.packageName, si.name); PackageManager pm = mContext.getPackageManager(); XmlResourceParser parser = null; try { parser = si.loadXmlMetaData(pm, mMetaDataName); if (parser == null) { throw new XmlPullParserException("No " + mMetaDataName + " meta-data"); } AttributeSet attrs = Xml.asAttributeSet(parser); int type; while ((type=parser.next()) != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { } String nodeName = parser.getName(); if (!mAttributesName.equals(nodeName)) { throw new XmlPullParserException( "Meta-data does not start with " + mAttributesName + " tag"); } V v = parseServiceAttributes(pm.getResourcesForApplication(si.applicationInfo), si.packageName, attrs); if (v == null) { return null; } final android.content.pm.ServiceInfo serviceInfo = service.serviceInfo; return new ServiceInfo(v, serviceInfo, componentName); } catch (NameNotFoundException e) { throw new XmlPullParserException( "Unable to load resources for pacakge " + si.packageName); } finally { if (parser != null) parser.close(); } } /** * Read all sync status back in to the initial engine state. */ private void readPersistentServicesLocked(InputStream is) throws XmlPullParserException, IOException { XmlPullParser parser = Xml.newPullParser(); parser.setInput(is, StandardCharsets.UTF_8.name()); int eventType = parser.getEventType(); while (eventType != XmlPullParser.START_TAG && eventType != XmlPullParser.END_DOCUMENT) { eventType = parser.next(); } String tagName = parser.getName(); if ("services".equals(tagName)) { eventType = parser.next(); do { if (eventType == XmlPullParser.START_TAG && parser.getDepth() == 2) { tagName = parser.getName(); if ("service".equals(tagName)) { V service = mSerializerAndParser.createFromXml(parser); if (service == null) { break; } String uidString = parser.getAttributeValue(null, "uid"); final int uid = Integer.parseInt(uidString); final int userId = UserHandle.getUserId(uid); final UserServices user = findOrCreateUserLocked(userId, false /*loadFromFileIfNew*/) ; user.persistentServices.put(service, uid); } } eventType = parser.next(); } while (eventType != XmlPullParser.END_DOCUMENT); } } private void migrateIfNecessaryLocked() { if (mSerializerAndParser == null) { return; } File systemDir = new File(getDataDirectory(), "system"); File syncDir = new File(systemDir, REGISTERED_SERVICES_DIR); AtomicFile oldFile = new AtomicFile(new File(syncDir, mInterfaceName + ".xml")); boolean oldFileExists = oldFile.getBaseFile().exists(); if (oldFileExists) { File marker = new File(syncDir, mInterfaceName + ".xml.migrated"); // if not migrated, perform the migration and add a marker if (!marker.exists()) { if (DEBUG) { Slog.i(TAG, "Marker file " + marker + " does not exist - running migration"); } InputStream is = null; try { is = oldFile.openRead(); mUserServices.clear(); readPersistentServicesLocked(is); } catch (Exception e) { Log.w(TAG, "Error reading persistent services, starting from scratch", e); } finally { IoUtils.closeQuietly(is); } try { for (UserInfo user : getUsers()) { UserServices userServices = mUserServices.get(user.id); if (userServices != null) { if (DEBUG) { Slog.i(TAG, "Migrating u" + user.id + " services " + userServices.persistentServices); } writePersistentServicesLocked(userServices, user.id); } } marker.createNewFile(); } catch (Exception e) { Log.w(TAG, "Migration failed", e); } // Migration is complete and we don't need to keep data for all users anymore, // It will be loaded from a new location when requested mUserServices.clear(); } } } /** * Writes services of a specified user to the file. */ private void writePersistentServicesLocked(UserServices user, int userId) { if (mSerializerAndParser == null) { return; } AtomicFile atomicFile = createFileForUser(userId); FileOutputStream fos = null; try { fos = atomicFile.startWrite(); XmlSerializer out = new FastXmlSerializer(); out.setOutput(fos, StandardCharsets.UTF_8.name()); out.startDocument(null, true); out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); out.startTag(null, "services"); for (Map.Entry service : user.persistentServices.entrySet()) { out.startTag(null, "service"); out.attribute(null, "uid", Integer.toString(service.getValue())); mSerializerAndParser.writeAsXml(service.getKey(), out); out.endTag(null, "service"); } out.endTag(null, "services"); out.endDocument(); atomicFile.finishWrite(fos); } catch (IOException e1) { Log.w(TAG, "Error writing accounts", e1); if (fos != null) { atomicFile.failWrite(fos); } } } @VisibleForTesting protected void onUserRemoved(int userId) { synchronized (mServicesLock) { mUserServices.remove(userId); } } @VisibleForTesting protected List getUsers() { return UserManager.get(mContext).getUsers(true); } @VisibleForTesting protected UserInfo getUser(int userId) { return UserManager.get(mContext).getUserInfo(userId); } private AtomicFile createFileForUser(int userId) { File userDir = getUserSystemDirectory(userId); File userFile = new File(userDir, REGISTERED_SERVICES_DIR + "/" + mInterfaceName + ".xml"); return new AtomicFile(userFile); } @VisibleForTesting protected File getUserSystemDirectory(int userId) { return Environment.getUserSystemDirectory(userId); } @VisibleForTesting protected File getDataDirectory() { return Environment.getDataDirectory(); } @VisibleForTesting protected Map getPersistentServices(int userId) { return findOrCreateUserLocked(userId).persistentServices; } public abstract V parseServiceAttributes(Resources res, String packageName, AttributeSet attrs); }