package com.android.server;
import com.android.internal.app.IAssetRedirectionManager;
import com.android.internal.util.XmlUtils;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ThemeInfo;
import android.content.res.AssetManager;
import android.content.res.PackageRedirectionMap;
import android.content.res.Resources;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class AssetRedirectionManagerService extends IAssetRedirectionManager.Stub {
private static final String TAG = "AssetRedirectionManager";
private final Context mContext;
/*
* TODO: This data structure should have some way to expire very old cache
* entries. Would be nice to optimize for the removal path as well.
*/
private final HashMap<RedirectionKey, PackageRedirectionMap> mRedirections =
new HashMap<RedirectionKey, PackageRedirectionMap>();
public AssetRedirectionManagerService(Context context) {
mContext = context;
}
@Override
public void clearRedirectionMapsByTheme(String themePackageName, String themeId)
throws RemoteException {
synchronized (mRedirections) {
Set<RedirectionKey> keys = mRedirections.keySet();
Iterator<RedirectionKey> iter = keys.iterator();
while (iter.hasNext()) {
RedirectionKey key = iter.next();
if (themePackageName.equals(key.themePackageName) &&
(themeId == null || themeId.equals(key.themeId))) {
iter.remove();
}
}
}
}
@Override
public void clearPackageRedirectionMap(String targetPackageName) throws RemoteException {
synchronized (mRedirections) {
Set<RedirectionKey> keys = mRedirections.keySet();
Iterator<RedirectionKey> iter = keys.iterator();
while (iter.hasNext()) {
RedirectionKey key = iter.next();
if (targetPackageName.equals(key.targetPackageName)) {
iter.remove();
}
}
}
}
@Override
public PackageRedirectionMap getPackageRedirectionMap(String themePackageName,
String themeId, String targetPackageName) throws RemoteException {
synchronized (mRedirections) {
RedirectionKey key = new RedirectionKey();
key.themePackageName = themePackageName;
key.themeId = themeId;
key.targetPackageName = targetPackageName;
PackageRedirectionMap map = mRedirections.get(key);
if (map != null) {
return map;
} else {
map = generatePackageRedirectionMap(key);
if (map != null) {
mRedirections.put(key, map);
}
return map;
}
}
}
private PackageRedirectionMap generatePackageRedirectionMap(RedirectionKey key) {
AssetManager assets = new AssetManager();
boolean frameworkAssets = key.targetPackageName.equals("android");
if (!frameworkAssets) {
PackageInfo pi = getPackageInfo(mContext, key.targetPackageName);
if (pi == null || pi.applicationInfo == null ||
assets.addAssetPath(pi.applicationInfo.publicSourceDir) == 0) {
Log.w(TAG, "Unable to attach target package assets for " + key.targetPackageName);
return null;
}
}
PackageInfo pi = getPackageInfo(mContext, key.themePackageName);
if (pi == null || pi.applicationInfo == null || pi.themeInfos == null ||
assets.addAssetPath(pi.applicationInfo.publicSourceDir) == 0) {
Log.w(TAG, "Unable to attach theme package assets from " + key.themePackageName);
return null;
}
PackageRedirectionMap resMap = new PackageRedirectionMap();
/*
* Apply a special redirection hack for the highest level <style>
* replacing @android:style/Theme.
*/
if (frameworkAssets) {
int themeResourceId = findThemeResourceId(pi.themeInfos, key.themeId);
assets.generateStyleRedirections(resMap.getNativePointer(), android.R.style.Theme,
themeResourceId);
}
Resources res = new Resources(assets, null, null);
generateExplicitRedirections(resMap, res, key.themePackageName, key.targetPackageName);
return resMap;
}
private void generateExplicitRedirections(PackageRedirectionMap resMap, Resources res,
String themePackageName, String targetPackageName) {
/*
* XXX: We should be parsing the <theme> tag's <meta-data>! Instead,
* we're just assuming that res/xml/<package>.xml exists and describes
* the redirects we want!
*/
String redirectXmlName = targetPackageName.replace('.', '_');
int redirectXmlResId = res.getIdentifier(redirectXmlName, "xml", themePackageName);
if (redirectXmlResId == 0) {
return;
}
ResourceRedirectionsProcessor processor = new ResourceRedirectionsProcessor(res,
redirectXmlResId, themePackageName, targetPackageName, resMap);
processor.process();
}
private static PackageInfo getPackageInfo(Context context, String packageName) {
try {
return context.getPackageManager().getPackageInfo(packageName, 0);
} catch (NameNotFoundException e) {
return null;
}
}
/**
* Searches for the high-level theme resource id for the specific
* <theme> tag being applied.
* <p>
* An individual theme package can contain multiple <theme> tags, each
* representing a separate theme choice from the user's perspective, even
* though the most common case is for there to be only 1.
*
* @return The style resource id or 0 if no match was found.
*/
private static int findThemeResourceId(ThemeInfo[] themeInfos, String needle) {
if (themeInfos != null && !TextUtils.isEmpty(needle)) {
int n = themeInfos.length;
for (int i = 0; i < n; i++) {
ThemeInfo info = themeInfos[i];
if (needle.equals(info.themeId)) {
return info.styleResourceId;
}
}
}
return 0;
}
private static Resources getUnredirectedResourcesForPackage(Context context, String packageName) {
AssetManager assets = new AssetManager();
if (!packageName.equals("android")) {
PackageInfo pi = getPackageInfo(context, packageName);
if (pi == null || pi.applicationInfo == null ||
assets.addAssetPath(pi.applicationInfo.publicSourceDir) == 0) {
Log.w(TAG, "Unable to get resources for package " + packageName);
return null;
}
}
return new Resources(assets, null, null);
}
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
synchronized (mRedirections) {
final ArrayList<RedirectionKey> filteredKeySet = new ArrayList<RedirectionKey>();
for (Map.Entry<RedirectionKey, PackageRedirectionMap> entry: mRedirections.entrySet()) {
PackageRedirectionMap map = entry.getValue();
if (map != null && map.getPackageId() != -1) {
filteredKeySet.add(entry.getKey());
}
}
Collections.sort(filteredKeySet, new Comparator<RedirectionKey>() {
@Override
public int compare(RedirectionKey a, RedirectionKey b) {
int comp = a.themePackageName.compareTo(b.themePackageName);
if (comp != 0) {
return comp;
}
comp = a.themeId.compareTo(b.themeId);
if (comp != 0) {
return comp;
}
return a.targetPackageName.compareTo(b.targetPackageName);
}
});
pw.println("Theme asset redirections:");
String lastPackageName = null;
String lastId = null;
Resources themeRes = null;
for (RedirectionKey key: filteredKeySet) {
if (lastPackageName == null || !lastPackageName.equals(key.themePackageName)) {
pw.println("* Theme package " + key.themePackageName + ":");
lastPackageName = key.themePackageName;
themeRes = getUnredirectedResourcesForPackage(mContext, key.themePackageName);
}
if (lastId == null || !lastId.equals(key.themeId)) {
pw.println(" theme id #" + key.themeId + ":");
lastId = key.themeId;
}
pw.println(" " + key.targetPackageName + ":");
Resources targetRes = getUnredirectedResourcesForPackage(mContext, key.targetPackageName);
PackageRedirectionMap resMap = mRedirections.get(key);
int[] fromIdents = resMap.getRedirectionKeys();
int N = fromIdents.length;
for (int i = 0; i < N; i++) {
int fromIdent = fromIdents[i];
int toIdent = resMap.lookupRedirection(fromIdent);
String fromName = targetRes != null ? targetRes.getResourceName(fromIdent) : null;
String toName = themeRes != null ? themeRes.getResourceName(toIdent) : null;
pw.println(String.format(" %s (0x%08x) => %s (0x%08x)", fromName, fromIdent,
toName, toIdent));
}
}
}
}
private static class RedirectionKey {
public String themePackageName;
public String themeId;
public String targetPackageName;
@Override
public boolean equals(Object o) {
if (o == this) return true;
if (!(o instanceof RedirectionKey)) return false;
final RedirectionKey oo = (RedirectionKey)o;
if (!nullSafeEquals(themePackageName, oo.themePackageName)) {
return false;
}
if (!nullSafeEquals(themeId, oo.themeId)) {
return false;
}
if (!nullSafeEquals(targetPackageName, oo.targetPackageName)) {
return false;
}
return true;
}
@Override
public int hashCode() {
return themePackageName.hashCode() +
themeId.hashCode() +
targetPackageName.hashCode();
}
private static boolean nullSafeEquals(Object a, Object b) {
if (a == null) {
return b == a;
} else if (b == null) {
return false;
} else {
return a.equals(b);
}
}
}
/**
* Parses and processes explicit redirection XML files.
*/
private static class ResourceRedirectionsProcessor {
private final Resources mResources;
private final XmlPullParser mParser;
private final int mResourceId;
private final String mThemePackageName;
private final String mTargetPackageName;
private final PackageRedirectionMap mResMap;
public ResourceRedirectionsProcessor(Resources res, int resourceId,
String themePackageName, String targetPackageName,
PackageRedirectionMap outMap) {
mResources = res;
mParser = res.getXml(resourceId);
mResourceId = resourceId;
mThemePackageName = themePackageName;
mTargetPackageName = targetPackageName;
mResMap = outMap;
}
public void process() {
XmlPullParser parser = mParser;
int type;
try {
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
// just loop...
}
String tagName = parser.getName();
if (parser.getName().equals("resource-redirections")) {
processResourceRedirectionsTag();
} else {
Log.w(TAG, "Unknown root element: " + tagName + " at " + getResourceLabel() + " " +
parser.getPositionDescription());
}
} catch (XmlPullParserException e) {
Log.w(TAG, "Malformed theme redirection meta at " + getResourceLabel());
} catch (IOException e) {
Log.w(TAG, "Unknown error reading redirection meta at " + getResourceLabel());
}
}
private void processResourceRedirectionsTag() throws XmlPullParserException, IOException {
XmlPullParser parser = mParser;
int type;
final int innerDepth = parser.getDepth();
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT &&
(type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
String tagName = parser.getName();
if (tagName.equals("item")) {
processItemTag();
} else {
Log.w(TAG, "Unknown element under <resource-redirections>: " + tagName
+ " at " + getResourceLabel() + " "
+ parser.getPositionDescription());
XmlUtils.skipCurrentTag(parser);
continue;
}
}
}
private void processItemTag() throws XmlPullParserException, IOException {
XmlPullParser parser = mParser;
String fromName = parser.getAttributeValue(null, "name");
if (TextUtils.isEmpty(fromName)) {
Log.w(TAG, "Missing android:name attribute on <item> tag at " + getResourceLabel() + " " +
parser.getPositionDescription());
return;
}
String toName = parser.nextText();
if (TextUtils.isEmpty(toName)) {
Log.w(TAG, "Missing <item> text at " + getResourceLabel() + " " +
parser.getPositionDescription());
return;
}
int fromIdent = mResources.getIdentifier(fromName, null, mTargetPackageName);
if (fromIdent == 0) {
Log.w(TAG, "No such resource found for " + mTargetPackageName + ":" + fromName);
return;
}
int toIdent = mResources.getIdentifier(toName, null, mThemePackageName);
if (toIdent == 0) {
Log.w(TAG, "No such resource found for " + mThemePackageName + ":" + toName);
return;
}
mResMap.addRedirection(fromIdent, toIdent);
}
private String getResourceLabel() {
return "resource #0x" + Integer.toHexString(mResourceId);
}
}
}