/* * 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.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.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.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; /** * 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; public final Context mContext; private final String mInterfaceName; private final String mMetaDataName; private final String mAttributesName; private final XmlSerializerAndParser mSerializerAndParser; private final Object mServicesLock = new Object(); @GuardedBy("mServicesLock") private boolean mPersistentServicesFileDidNotExist; @GuardedBy("mServicesLock") private final SparseArray> mUserServices = new SparseArray>(2); private static class UserServices { @GuardedBy("mServicesLock") public final Map persistentServices = Maps.newHashMap(); @GuardedBy("mServicesLock") public Map> services = null; } private UserServices findOrCreateUserLocked(int userId) { UserServices services = mUserServices.get(userId); if (services == null) { services = new UserServices(); mUserServices.put(userId, services); } return services; } /** * This file contains the list of known services. We would like to maintain this forever * so we store it as an XML file. */ private final AtomicFile mPersistentServicesFile; // 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; File dataDir = Environment.getDataDirectory(); File systemDir = new File(dataDir, "system"); File syncDir = new File(systemDir, "registered_services"); mPersistentServicesFile = new AtomicFile(new File(syncDir, interfaceName + ".xml")); // Load persisted services from disk readPersistentServicesLocked(); 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); } 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 { // either we're adding/changing, or it's a removal without replacement, so // we need to recalculate the set of available services generateServicesMap(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_OWNER); } }; public void invalidateCache(int userId) { synchronized (mServicesLock) { final UserServices user = findOrCreateUserLocked(userId); user.services = null; } } 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 ComponentName componentName; public final int uid; /** @hide */ public ServiceInfo(V type, ComponentName componentName, int uid) { this.type = type; this.componentName = componentName; this.uid = uid; } @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(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(userId); } return Collections.unmodifiableCollection( new ArrayList>(user.services.values())); } } private 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; } /** * Populate {@link UserServices#services} by scanning installed packages for * given {@link UserHandle}. */ private void generateServicesMap(int userId) { if (DEBUG) { Slog.d(TAG, "generateServicesMap() for " + userId); } final PackageManager pm = mContext.getPackageManager(); final ArrayList> serviceInfos = new ArrayList>(); final List resolveInfos = pm.queryIntentServicesAsUser( new Intent(mInterfaceName), PackageManager.GET_META_DATA, 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 e) { Log.w(TAG, "Unable to load service info " + resolveInfo.toString(), e); } catch (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(); } else { user.services.clear(); } 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 (!(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()) { if (!containsType(serviceInfos, v1)) { toBeRemoved.add(v1); } } for (V v1 : toBeRemoved) { if (DEBUG) { changes.append(" Service removed: ").append(v1).append("\n"); } changed = true; user.persistentServices.remove(v1); notifyListener(v1, userId, true /* removed */); } 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) { writePersistentServicesLocked(); } } } 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; } private 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; final ApplicationInfo applicationInfo = serviceInfo.applicationInfo; final int uid = applicationInfo.uid; return new ServiceInfo(v, componentName, uid); } 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() { mUserServices.clear(); if (mSerializerAndParser == null) { return; } FileInputStream fis = null; try { mPersistentServicesFileDidNotExist = !mPersistentServicesFile.getBaseFile().exists(); if (mPersistentServicesFileDidNotExist) { return; } fis = mPersistentServicesFile.openRead(); XmlPullParser parser = Xml.newPullParser(); parser.setInput(fis, null); 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); user.persistentServices.put(service, uid); } } eventType = parser.next(); } while (eventType != XmlPullParser.END_DOCUMENT); } } catch (Exception e) { Log.w(TAG, "Error reading persistent services, starting from scratch", e); } finally { if (fis != null) { try { fis.close(); } catch (java.io.IOException e1) { } } } } /** * Write all sync status to the sync status file. */ private void writePersistentServicesLocked() { if (mSerializerAndParser == null) { return; } FileOutputStream fos = null; try { fos = mPersistentServicesFile.startWrite(); XmlSerializer out = new FastXmlSerializer(); out.setOutput(fos, "utf-8"); out.startDocument(null, true); out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); out.startTag(null, "services"); for (int i = 0; i < mUserServices.size(); i++) { final UserServices user = mUserServices.valueAt(i); 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(); mPersistentServicesFile.finishWrite(fos); } catch (java.io.IOException e1) { Log.w(TAG, "Error writing accounts", e1); if (fos != null) { mPersistentServicesFile.failWrite(fos); } } } public abstract V parseServiceAttributes(Resources res, String packageName, AttributeSet attrs); }