blob: b293e2a8340003a7cea91d8b3b6c33be49aef929 [file] [log] [blame]
/*
* 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.
* <p>
* The services are referred to by type V and are made available via the
* {@link #getServiceInfo} method.
*
* @hide
*/
public abstract class RegisteredServicesCache<V> {
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<V> mSerializerAndParser;
protected final Object mServicesLock = new Object();
@GuardedBy("mServicesLock")
private final SparseArray<UserServices<V>> mUserServices = new SparseArray<UserServices<V>>(2);
private static class UserServices<V> {
@GuardedBy("mServicesLock")
final Map<V, Integer> persistentServices = Maps.newHashMap();
@GuardedBy("mServicesLock")
Map<V, ServiceInfo<V>> services = null;
@GuardedBy("mServicesLock")
boolean mPersistentServicesFileDidNotExist = true;
}
@GuardedBy("mServicesLock")
private UserServices<V> findOrCreateUserLocked(int userId) {
return findOrCreateUserLocked(userId, true);
}
@GuardedBy("mServicesLock")
private UserServices<V> findOrCreateUserLocked(int userId, boolean loadFromFileIfNew) {
UserServices<V> services = mUserServices.get(userId);
if (services == null) {
services = new UserServices<V>();
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<V> mListener;
private Handler mHandler;
public RegisteredServicesCache(Context context, String interfaceName, String metaDataName,
String attributeName, XmlSerializerAndParser<V> 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_OWNER);
}
};
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<V> user = findOrCreateUserLocked(userId);
user.services = null;
onServicesChangedLocked(userId);
}
}
public void dump(FileDescriptor fd, PrintWriter fout, String[] args, int userId) {
synchronized (mServicesLock) {
final UserServices<V> 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<V> getListener() {
synchronized (this) {
return mListener;
}
}
public void setListener(RegisteredServicesCacheListener<V> 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<V> listener;
Handler handler;
synchronized (this) {
listener = mListener;
handler = mHandler;
}
if (listener == null) {
return;
}
final RegisteredServicesCacheListener<V> 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<V> {
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<V> getServiceInfo(V type, int userId) {
synchronized (mServicesLock) {
// Find user and lazily populate cache
final UserServices<V> 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<ServiceInfo<V>> getAllServices(int userId) {
synchronized (mServicesLock) {
// Find user and lazily populate cache
final UserServices<V> user = findOrCreateUserLocked(userId);
if (user.services == null) {
generateServicesMap(null, userId);
}
return Collections.unmodifiableCollection(
new ArrayList<ServiceInfo<V>>(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<ResolveInfo> queryIntentServices(int userId) {
final PackageManager pm = mContext.getPackageManager();
return pm.queryIntentServicesAsUser(
new Intent(mInterfaceName), PackageManager.GET_META_DATA, 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<ServiceInfo<V>> serviceInfos = new ArrayList<ServiceInfo<V>>();
final List<ResolveInfo> resolveInfos = queryIntentServices(userId);
for (ResolveInfo resolveInfo : resolveInfos) {
try {
ServiceInfo<V> 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<V> user = findOrCreateUserLocked(userId);
final boolean firstScan = user.services == null;
if (firstScan) {
user.services = Maps.newHashMap();
}
StringBuilder changes = new StringBuilder();
boolean changed = false;
for (ServiceInfo<V> 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<V> 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<ServiceInfo<V>> 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<ServiceInfo<V>> serviceInfos, V type, int uid) {
for (int i = 0, N = serviceInfos.size(); i < N; i++) {
final ServiceInfo<V> serviceInfo = serviceInfos.get(i);
if (serviceInfo.type.equals(type) && serviceInfo.uid == uid) {
return true;
}
}
return false;
}
@VisibleForTesting
protected ServiceInfo<V> 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>(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(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<V> 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<V> 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<V> 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<V, Integer> 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<UserInfo> 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<V, Integer> getPersistentServices(int userId) {
return findOrCreateUserLocked(userId).persistentServices;
}
public abstract V parseServiceAttributes(Resources res,
String packageName, AttributeSet attrs);
}