diff options
11 files changed, 788 insertions, 140 deletions
diff --git a/core/java/android/content/pm/ParceledListSlice.java b/core/java/android/content/pm/ParceledListSlice.java index 8a43472a430b..335a45e40777 100644 --- a/core/java/android/content/pm/ParceledListSlice.java +++ b/core/java/android/content/pm/ParceledListSlice.java @@ -30,6 +30,12 @@ import java.util.List; * Transfer a large list of Parcelable objects across an IPC. Splits into * multiple transactions if needed. * + * Caveat: for efficiency and security, all elements must be the same concrete type. + * In order to avoid writing the class name of each object, we must ensure that + * each object is the same type, or else unparceling then reparceling the data may yield + * a different result if the class name encoded in the Parcelable is a Base type. + * See b/17671747. + * * @hide */ public class ParceledListSlice<T extends Parcelable> implements Parcelable { @@ -56,13 +62,25 @@ public class ParceledListSlice<T extends Parcelable> implements Parcelable { if (N <= 0) { return; } + Parcelable.Creator<T> creator = p.readParcelableCreator(loader); + Class<?> listElementClass = null; + int i = 0; while (i < N) { if (p.readInt() == 0) { break; } - mList.add(p.readCreator(creator, loader)); + + final T parcelable = p.readCreator(creator, loader); + if (listElementClass == null) { + listElementClass = parcelable.getClass(); + } else { + verifySameType(listElementClass, parcelable.getClass()); + } + + mList.add(parcelable); + if (DEBUG) Log.d(TAG, "Read inline #" + i + ": " + mList.get(mList.size()-1)); i++; } @@ -82,7 +100,11 @@ public class ParceledListSlice<T extends Parcelable> implements Parcelable { return; } while (i < N && reply.readInt() != 0) { - mList.add(reply.readCreator(creator, loader)); + final T parcelable = reply.readCreator(creator, loader); + verifySameType(listElementClass, parcelable.getClass()); + + mList.add(parcelable); + if (DEBUG) Log.d(TAG, "Read extra #" + i + ": " + mList.get(mList.size()-1)); i++; } @@ -91,6 +113,14 @@ public class ParceledListSlice<T extends Parcelable> implements Parcelable { } } + private static void verifySameType(final Class<?> expected, final Class<?> actual) { + if (!actual.equals(expected)) { + throw new IllegalArgumentException("Can't unparcel type " + + actual.getName() + " in list of type " + + expected.getName()); + } + } + public List<T> getList() { return mList; } @@ -116,11 +146,16 @@ public class ParceledListSlice<T extends Parcelable> implements Parcelable { dest.writeInt(N); if (DEBUG) Log.d(TAG, "Writing " + N + " items"); if (N > 0) { + final Class<?> listElementClass = mList.get(0).getClass(); dest.writeParcelableCreator(mList.get(0)); int i = 0; while (i < N && dest.dataSize() < MAX_FIRST_IPC_SIZE) { dest.writeInt(1); - mList.get(i).writeToParcel(dest, callFlags); + + final T parcelable = mList.get(i); + verifySameType(listElementClass, parcelable.getClass()); + parcelable.writeToParcel(dest, callFlags); + if (DEBUG) Log.d(TAG, "Wrote inline #" + i + ": " + mList.get(i)); i++; } @@ -137,7 +172,11 @@ public class ParceledListSlice<T extends Parcelable> implements Parcelable { if (DEBUG) Log.d(TAG, "Writing more @" + i + " of " + N); while (i < N && reply.dataSize() < MAX_IPC_SIZE) { reply.writeInt(1); - mList.get(i).writeToParcel(reply, callFlags); + + final T parcelable = mList.get(i); + verifySameType(listElementClass, parcelable.getClass()); + parcelable.writeToParcel(reply, callFlags); + if (DEBUG) Log.d(TAG, "Wrote extra #" + i + ": " + mList.get(i)); i++; } diff --git a/core/jni/android/graphics/NinePatchPeeker.cpp b/core/jni/android/graphics/NinePatchPeeker.cpp index 1dafa1b5397d..d99ddeb9bfc1 100644 --- a/core/jni/android/graphics/NinePatchPeeker.cpp +++ b/core/jni/android/graphics/NinePatchPeeker.cpp @@ -24,7 +24,9 @@ bool NinePatchPeeker::peek(const char tag[], const void* data, size_t length) { if (!strcmp("npTc", tag) && length >= sizeof(Res_png_9patch)) { Res_png_9patch* patch = (Res_png_9patch*) data; size_t patchSize = patch->serializedSize(); - assert(length == patchSize); + if (length != patchSize) { + return false; + } // You have to copy the data because it is owned by the png reader Res_png_9patch* patchNew = (Res_png_9patch*) malloc(patchSize); memcpy(patchNew, patch, patchSize); diff --git a/core/tests/coretests/src/android/content/pm/ParceledListSliceTest.java b/core/tests/coretests/src/android/content/pm/ParceledListSliceTest.java new file mode 100644 index 000000000000..e5a92bf23bdc --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/ParceledListSliceTest.java @@ -0,0 +1,263 @@ +package android.content.pm; + +import android.os.Parcel; +import android.os.Parcelable; +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.List; + +public class ParceledListSliceTest extends TestCase { + + public void testSmallList() throws Exception { + final int objectCount = 100; + List<SmallObject> list = new ArrayList<SmallObject>(); + for (int i = 0; i < objectCount; i++) { + list.add(new SmallObject(i * 2, (i * 2) + 1)); + } + + ParceledListSlice<SmallObject> slice; + + Parcel parcel = Parcel.obtain(); + try { + parcel.writeParcelable(new ParceledListSlice<SmallObject>(list), 0); + parcel.setDataPosition(0); + slice = parcel.readParcelable(getClass().getClassLoader()); + } finally { + parcel.recycle(); + } + + assertNotNull(slice); + assertNotNull(slice.getList()); + assertEquals(objectCount, slice.getList().size()); + + for (int i = 0; i < objectCount; i++) { + assertEquals(i * 2, slice.getList().get(i).mFieldA); + assertEquals((i * 2) + 1, slice.getList().get(i).mFieldB); + } + } + + private static int measureLargeObject() { + Parcel p = Parcel.obtain(); + try { + new LargeObject(0, 0, 0, 0, 0).writeToParcel(p, 0); + return p.dataPosition(); + } finally { + p.recycle(); + } + } + + /** + * Test that when the list is large, the data is successfully parceled + * and unparceled (the implementation will send pieces of the list in + * separate round-trips to avoid the IPC limit). + */ + public void testLargeList() throws Exception { + final int thresholdBytes = 256 * 1024; + final int objectCount = thresholdBytes / measureLargeObject(); + + List<LargeObject> list = new ArrayList<LargeObject>(); + for (int i = 0; i < objectCount; i++) { + list.add(new LargeObject( + i * 5, + (i * 5) + 1, + (i * 5) + 2, + (i * 5) + 3, + (i * 5) + 4 + )); + } + + ParceledListSlice<LargeObject> slice; + + Parcel parcel = Parcel.obtain(); + try { + parcel.writeParcelable(new ParceledListSlice<LargeObject>(list), 0); + parcel.setDataPosition(0); + slice = parcel.readParcelable(getClass().getClassLoader()); + } finally { + parcel.recycle(); + } + + assertNotNull(slice); + assertNotNull(slice.getList()); + assertEquals(objectCount, slice.getList().size()); + + for (int i = 0; i < objectCount; i++) { + assertEquals(i * 5, slice.getList().get(i).mFieldA); + assertEquals((i * 5) + 1, slice.getList().get(i).mFieldB); + assertEquals((i * 5) + 2, slice.getList().get(i).mFieldC); + assertEquals((i * 5) + 3, slice.getList().get(i).mFieldD); + assertEquals((i * 5) + 4, slice.getList().get(i).mFieldE); + } + } + + /** + * Test that only homogeneous elements may be unparceled. + */ + public void testHomogeneousElements() throws Exception { + List<BaseObject> list = new ArrayList<BaseObject>(); + list.add(new LargeObject(0, 1, 2, 3, 4)); + list.add(new SmallObject(5, 6)); + list.add(new SmallObject(7, 8)); + + Parcel parcel = Parcel.obtain(); + try { + writeEvilParceledListSlice(parcel, list); + parcel.setDataPosition(0); + try { + ParceledListSlice.CREATOR.createFromParcel(parcel, getClass().getClassLoader()); + assertTrue("Unparceled heterogeneous ParceledListSlice", false); + } catch (IllegalArgumentException e) { + // Success, we're not allowed to process heterogeneous + // elements in a ParceledListSlice. + } + } finally { + parcel.recycle(); + } + } + + /** + * Write a ParcelableListSlice that uses the BaseObject base class as the Creator. + * This is dangerous, as it may affect how the data is unparceled, then later parceled + * by the system, leading to a self-modifying data security vulnerability. + */ + private static <T extends BaseObject> void writeEvilParceledListSlice(Parcel dest, List<T> list) { + final int listCount = list.size(); + + // Number of items. + dest.writeInt(listCount); + + // The type/creator to use when unparceling. Here we use the base class + // to simulate an attack on ParceledListSlice. + dest.writeString(BaseObject.class.getName()); + + for (int i = 0; i < listCount; i++) { + // 1 means the item is present. + dest.writeInt(1); + list.get(i).writeToParcel(dest, 0); + } + } + + public abstract static class BaseObject implements Parcelable { + protected static final int TYPE_SMALL = 0; + protected static final int TYPE_LARGE = 1; + + protected void writeToParcel(Parcel dest, int flags, int type) { + dest.writeInt(type); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * This is *REALLY* bad, but we're doing it in the test to ensure that we handle + * the possible exploit when unparceling an object with the BaseObject written as + * Creator. + */ + public static final Creator<BaseObject> CREATOR = new Creator<BaseObject>() { + @Override + public BaseObject createFromParcel(Parcel source) { + switch (source.readInt()) { + case TYPE_SMALL: + return SmallObject.createFromParcelBody(source); + case TYPE_LARGE: + return LargeObject.createFromParcelBody(source); + default: + throw new IllegalArgumentException("Unknown type"); + } + } + + @Override + public BaseObject[] newArray(int size) { + return new BaseObject[size]; + } + }; + } + + public static class SmallObject extends BaseObject { + public int mFieldA; + public int mFieldB; + + public SmallObject(int a, int b) { + mFieldA = a; + mFieldB = b; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags, TYPE_SMALL); + dest.writeInt(mFieldA); + dest.writeInt(mFieldB); + } + + public static SmallObject createFromParcelBody(Parcel source) { + return new SmallObject(source.readInt(), source.readInt()); + } + + public static final Creator<SmallObject> CREATOR = new Creator<SmallObject>() { + @Override + public SmallObject createFromParcel(Parcel source) { + // Consume the type (as it is always written out). + source.readInt(); + return createFromParcelBody(source); + } + + @Override + public SmallObject[] newArray(int size) { + return new SmallObject[size]; + } + }; + } + + public static class LargeObject extends BaseObject { + public int mFieldA; + public int mFieldB; + public int mFieldC; + public int mFieldD; + public int mFieldE; + + public LargeObject(int a, int b, int c, int d, int e) { + mFieldA = a; + mFieldB = b; + mFieldC = c; + mFieldD = d; + mFieldE = e; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags, TYPE_LARGE); + dest.writeInt(mFieldA); + dest.writeInt(mFieldB); + dest.writeInt(mFieldC); + dest.writeInt(mFieldD); + dest.writeInt(mFieldE); + } + + public static LargeObject createFromParcelBody(Parcel source) { + return new LargeObject( + source.readInt(), + source.readInt(), + source.readInt(), + source.readInt(), + source.readInt() + ); + } + + public static final Creator<LargeObject> CREATOR = new Creator<LargeObject>() { + @Override + public LargeObject createFromParcel(Parcel source) { + // Consume the type (as it is always written out). + source.readInt(); + return createFromParcelBody(source); + } + + @Override + public LargeObject[] newArray(int size) { + return new LargeObject[size]; + } + }; + } +} diff --git a/include/androidfw/ResourceTypes.h b/include/androidfw/ResourceTypes.h index ac5eca08a4ff..07df5ae00252 100644 --- a/include/androidfw/ResourceTypes.h +++ b/include/androidfw/ResourceTypes.h @@ -107,9 +107,9 @@ struct Res_png_9patch yDivsOffset(0), colorsOffset(0) { } int8_t wasDeserialized; - int8_t numXDivs; - int8_t numYDivs; - int8_t numColors; + uint8_t numXDivs; + uint8_t numYDivs; + uint8_t numColors; // The offset (from the start of this structure) to the xDivs & yDivs // array for this 9patch. To get a pointer to this array, call diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java index a2bd6d7840d0..18036927363f 100644 --- a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java @@ -614,15 +614,27 @@ public final class BridgeTypedArray extends TypedArray { int pos = value.indexOf('/'); String idName = value.substring(pos + 1); - - // if this is a framework id - if (mPlatformFile || value.startsWith("@android") || value.startsWith("@+android")) { - // look for idName in the android R classes + boolean create = value.startsWith("@+"); + boolean isFrameworkId = + mPlatformFile || value.startsWith("@android") || value.startsWith("@+android"); + + // Look for the idName in project or android R class depending on isPlatform. + if (create) { + Integer idValue; + if (isFrameworkId) { + idValue = Bridge.getResourceId(ResourceType.ID, idName); + } else { + idValue = mContext.getProjectCallback().getResourceId(ResourceType.ID, idName); + } + return idValue == null ? defValue : idValue; + } + // This calls the same method as in if(create), but doesn't create a dynamic id, if + // one is not found. + if (isFrameworkId) { return mContext.getFrameworkResourceValue(ResourceType.ID, idName, defValue); + } else { + return mContext.getProjectResourceValue(ResourceType.ID, idName, defValue); } - - // look for idName in the project R class. - return mContext.getProjectResourceValue(ResourceType.ID, idName, defValue); } // not a direct id valid reference? resolve it diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java index 8523f1a95dab..2adeb67b9150 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -288,6 +288,11 @@ public final class BridgeContext extends Context { value = mRenderResources.resolveResValue(value); } + if (value == null) { + // unable to find the attribute. + return false; + } + // check if this is a style resource if (value instanceof StyleResourceValue) { // get the id that will represent this style. @@ -295,7 +300,6 @@ public final class BridgeContext extends Context { return true; } - int a; // if this is a framework value. if (value.isFramework()) { diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/AppCompatActionBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/AppCompatActionBar.java new file mode 100644 index 000000000000..e5023b873e23 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/AppCompatActionBar.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2015 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 com.android.layoutlib.bridge.bars; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.SessionParams; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.impl.ResourceHelper; +import com.android.resources.ResourceType; + +import android.graphics.drawable.Drawable; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + + +/** + * Assumes that the AppCompat library is present in the project's classpath and creates an + * actionbar around it. + */ +public class AppCompatActionBar extends BridgeActionBar { + + private Object mWindowDecorActionBar; + private static final String WINDOW_ACTION_BAR_CLASS = "android.support.v7.internal.app.WindowDecorActionBar"; + private Class<?> mWindowActionBarClass; + + /** + * Inflate the action bar and attach it to {@code parentView} + */ + public AppCompatActionBar(@NonNull BridgeContext context, @NonNull SessionParams params, + @NonNull ViewGroup parentView) { + super(context, params, parentView); + int contentRootId = context.getProjectResourceValue(ResourceType.ID, + "action_bar_activity_content", 0); + View contentView = getDecorContent().findViewById(contentRootId); + if (contentView != null) { + assert contentView instanceof FrameLayout; + setContentRoot(((FrameLayout) contentView)); + } else { + // Something went wrong. Create a new FrameLayout in the enclosing layout. + FrameLayout contentRoot = new FrameLayout(context); + setMatchParent(contentRoot); + mEnclosingLayout.addView(contentRoot); + setContentRoot(contentRoot); + } + try { + Class[] constructorParams = {View.class}; + Object[] constructorArgs = {getDecorContent()}; + mWindowDecorActionBar = params.getProjectCallback().loadView(WINDOW_ACTION_BAR_CLASS, + constructorParams, constructorArgs); + + mWindowActionBarClass = mWindowDecorActionBar == null ? null : + mWindowDecorActionBar.getClass(); + setupActionBar(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + protected ResourceValue getLayoutResource(BridgeContext context) { + // We always assume that the app has requested the action bar. + return context.getRenderResources().getProjectResource(ResourceType.LAYOUT, + "abc_screen_toolbar"); + } + + @Override + protected void setTitle(CharSequence title) { + if (title != null && mWindowDecorActionBar != null) { + Method setTitle = getMethod(mWindowActionBarClass, "setTitle", CharSequence.class); + invoke(setTitle, mWindowDecorActionBar, title); + } + } + + @Override + protected void setSubtitle(CharSequence subtitle) { + if (subtitle != null && mWindowDecorActionBar != null) { + Method setSubtitle = getMethod(mWindowActionBarClass, "setSubtitle", CharSequence.class); + invoke(setSubtitle, mWindowDecorActionBar, subtitle); + } + } + + @Override + protected void setIcon(String icon) { + // Do this only if the action bar doesn't already have an icon. + if (icon != null && !icon.isEmpty() && mWindowDecorActionBar != null) { + if (((Boolean) invoke(getMethod(mWindowActionBarClass, "hasIcon"), mWindowDecorActionBar) + )) { + Drawable iconDrawable = getDrawable(icon, false); + if (iconDrawable != null) { + Method setIcon = getMethod(mWindowActionBarClass, "setIcon", Drawable.class); + invoke(setIcon, mWindowDecorActionBar, iconDrawable); + } + } + } + } + + @Override + protected void setHomeAsUp(boolean homeAsUp) { + if (mWindowDecorActionBar != null) { + Method setHomeAsUp = getMethod(mWindowActionBarClass, + "setDefaultDisplayHomeAsUpEnabled", boolean.class); + invoke(setHomeAsUp, mWindowDecorActionBar, homeAsUp); + } + } + + @Override + public void createMenuPopup() { + // it's hard to addd menus to appcompat's actionbar, since it'll use a lot of reflection. + // so we skip it for now. + } + + @Nullable + private static Method getMethod(Class<?> owner, String name, Class<?>... parameterTypes) { + try { + return owner == null ? null : owner.getMethod(name, parameterTypes); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } + return null; + } + + @Nullable + private static Object invoke(Method method, Object owner, Object... args) { + try { + return method == null ? null : method.invoke(owner, args); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + return null; + } + + // TODO: this is duplicated from FrameworkActionBarWrapper$WindowActionBarWrapper + @Nullable + private Drawable getDrawable(@NonNull String name, boolean isFramework) { + RenderResources res = mBridgeContext.getRenderResources(); + ResourceValue value = res.findResValue(name, isFramework); + value = res.resolveResValue(value); + if (value != null) { + return ResourceHelper.getDrawable(value, mBridgeContext); + } + return null; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/BridgeActionBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/BridgeActionBar.java new file mode 100644 index 000000000000..b29d25f78d65 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/BridgeActionBar.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2015 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 com.android.layoutlib.bridge.bars; + +import com.android.annotations.NonNull; +import com.android.ide.common.rendering.api.ActionBarCallback; +import com.android.ide.common.rendering.api.ActionBarCallback.HomeButtonStyle; +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.SessionParams; +import com.android.layoutlib.bridge.android.BridgeContext; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; + +/** + * An abstraction over two implementations of the ActionBar - framework and appcompat. + */ +public abstract class BridgeActionBar { + // Store a reference to the context so that we don't have to cast it repeatedly. + @NonNull protected final BridgeContext mBridgeContext; + @NonNull protected final SessionParams mParams; + // A Layout that contains the inflated action bar. The menu popup is added to this layout. + @NonNull protected final ViewGroup mEnclosingLayout; + + private final View mDecorContent; + private final ActionBarCallback mCallback; + + @NonNull private FrameLayout mContentRoot; + + public BridgeActionBar(@NonNull BridgeContext context, @NonNull SessionParams params, + @NonNull ViewGroup parentView) { + mBridgeContext = context; + mParams = params; + mCallback = params.getProjectCallback().getActionBarCallback(); + ResourceValue layoutName = getLayoutResource(context); + if (layoutName == null) { + throw new RuntimeException("Unable to find the layout for Action Bar."); + } + int layoutId; + if (layoutName.isFramework()) { + layoutId = context.getFrameworkResourceValue(layoutName.getResourceType(), + layoutName.getName(), 0); + } else { + layoutId = context.getProjectResourceValue(layoutName.getResourceType(), + layoutName.getName(), 0); + + } + if (layoutId == 0) { + throw new RuntimeException( + String.format("Unable to resolve attribute \"%1$s\" of type \"%2$s\"", + layoutName.getName(), layoutName.getResourceType())); + } + if (mCallback.isOverflowPopupNeeded()) { + // Create a RelativeLayout around the action bar, to which the overflow popup may be + // added. + mEnclosingLayout = new RelativeLayout(mBridgeContext); + setMatchParent(mEnclosingLayout); + parentView.addView(mEnclosingLayout); + } else { + mEnclosingLayout = parentView; + } + + // Inflate action bar layout. + mDecorContent = LayoutInflater.from(context).inflate(layoutId, mEnclosingLayout, true); + + } + + /** + * Returns the Layout Resource that should be used to inflate the action bar. This layout + * should cover the complete screen, and have a FrameLayout included, where the content will + * be inflated. + */ + protected abstract ResourceValue getLayoutResource(BridgeContext context); + + protected void setContentRoot(FrameLayout contentRoot) { + mContentRoot = contentRoot; + } + + @NonNull + public FrameLayout getContentRoot() { + return mContentRoot; + } + + /** + * Returns the view inflated. This should contain both the ActionBar and the app content in it. + */ + protected View getDecorContent() { + return mDecorContent; + } + + /** Setup things like the title, subtitle, icon etc. */ + protected void setupActionBar() { + setTitle(); + setSutTitle(); + setIcon(); + setHomeAsUp(mCallback.getHomeButtonStyle() == HomeButtonStyle.SHOW_HOME_AS_UP); + } + + protected abstract void setTitle(CharSequence title); + protected abstract void setSubtitle(CharSequence subtitle); + protected abstract void setIcon(String icon); + protected abstract void setHomeAsUp(boolean homeAsUp); + + private void setTitle() { + RenderResources res = mBridgeContext.getRenderResources(); + + String title = mParams.getAppLabel(); + ResourceValue titleValue = res.findResValue(title, false); + if (titleValue != null && titleValue.getValue() != null) { + setTitle(titleValue.getValue()); + } else { + setTitle(title); + } + } + + private void setSutTitle() { + String subTitle = mCallback.getSubTitle(); + if (subTitle != null) { + setSubtitle(subTitle); + } + } + + private void setIcon() { + String appIcon = mParams.getAppIcon(); + if (appIcon != null) { + setIcon(appIcon); + } + } + + public abstract void createMenuPopup(); + + public ActionBarCallback getCallBack() { + return mCallback; + } + + protected static void setMatchParent(View view) { + view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/ActionBarLayout.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FrameworkActionBar.java index 2ff8d3739fa9..a1c90651cbf0 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/ActionBarLayout.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FrameworkActionBar.java @@ -31,7 +31,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.DisplayMetrics; import android.util.TypedValue; -import android.view.LayoutInflater; +import android.view.InflateException; import android.view.View; import android.view.View.MeasureSpec; import android.view.ViewGroup; @@ -44,70 +44,30 @@ import android.widget.RelativeLayout; import java.util.ArrayList; -public class ActionBarLayout { +/** + * Creates the ActionBar as done by the framework. + */ +public class FrameworkActionBar extends BridgeActionBar { private static final String LAYOUT_ATTR_NAME = "windowActionBarFullscreenDecorLayout"; // The Action Bar - @NonNull - private CustomActionBarWrapper mActionBar; - - // Store another reference to the context so that we don't have to cast it repeatedly. - @NonNull - private final BridgeContext mBridgeContext; - - @NonNull - private FrameLayout mContentRoot; + @NonNull private FrameworkActionBarWrapper mActionBar; // A fake parent for measuring views. - @Nullable - private ViewGroup mMeasureParent; - - // A Layout that contains the inflated action bar. The menu popup is added to this layout. - @NonNull - private final RelativeLayout mEnclosingLayout; + @Nullable private ViewGroup mMeasureParent; /** * Inflate the action bar and attach it to {@code parentView} */ - public ActionBarLayout(@NonNull BridgeContext context, @NonNull SessionParams params, + public FrameworkActionBar(@NonNull BridgeContext context, @NonNull SessionParams params, @NonNull ViewGroup parentView) { + super(context, params, parentView); - mBridgeContext = context; - - ResourceValue layoutName = context.getRenderResources() - .findItemInTheme(LAYOUT_ATTR_NAME, true); - if (layoutName != null) { - // We may need to resolve the reference obtained. - layoutName = context.getRenderResources().findResValue(layoutName.getValue(), - layoutName.isFramework()); - } - int layoutId = 0; - String error = null; - if (layoutName == null) { - error = "Unable to find action bar layout (" + LAYOUT_ATTR_NAME - + ") in the current theme."; - } else { - layoutId = context.getFrameworkResourceValue(layoutName.getResourceType(), - layoutName.getName(), 0); - if (layoutId == 0) { - error = String.format("Unable to resolve attribute \"%s\" of type \"%s\"", - layoutName.getName(), layoutName.getResourceType()); - } - } - if (layoutId == 0) { - throw new RuntimeException(error); - } - // Create a RelativeLayout to hold the action bar. The layout is needed so that we may - // add the menu popup to it. - mEnclosingLayout = new RelativeLayout(mBridgeContext); - setMatchParent(mEnclosingLayout); - parentView.addView(mEnclosingLayout); - - // Inflate action bar layout. - View decorContent = LayoutInflater.from(context).inflate(layoutId, mEnclosingLayout, true); + View decorContent = getDecorContent(); - mActionBar = CustomActionBarWrapper.getActionBarWrapper(context, params, decorContent); + mActionBar = FrameworkActionBarWrapper.getActionBarWrapper(context, getCallBack(), + decorContent); FrameLayout contentRoot = (FrameLayout) mEnclosingLayout.findViewById(android.R.id.content); @@ -117,27 +77,62 @@ public class ActionBarLayout { contentRoot = new FrameLayout(context); setMatchParent(contentRoot); mEnclosingLayout.addView(contentRoot); - mContentRoot = contentRoot; + setContentRoot(contentRoot); } else { - mContentRoot = contentRoot; - mActionBar.setupActionBar(); + setContentRoot(contentRoot); + setupActionBar(); mActionBar.inflateMenus(); } } - private void setMatchParent(View view) { - view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); + @Override + protected ResourceValue getLayoutResource(BridgeContext context) { + ResourceValue layoutName = + context.getRenderResources().findItemInTheme(LAYOUT_ATTR_NAME, true); + if (layoutName != null) { + // We may need to resolve the reference obtained. + layoutName = context.getRenderResources().findResValue(layoutName.getValue(), + layoutName.isFramework()); + } + if (layoutName == null) { + throw new InflateException("Unable to find action bar layout (" + LAYOUT_ATTR_NAME + + ") in the current theme."); + } + return layoutName; + } + + @Override + protected void setupActionBar() { + super.setupActionBar(); + mActionBar.setupActionBar(); + } + + @Override + protected void setHomeAsUp(boolean homeAsUp) { + mActionBar.setHomeAsUp(homeAsUp); + } + + @Override + protected void setTitle(CharSequence title) { + mActionBar.setTitle(title); + } + + @Override + protected void setSubtitle(CharSequence subtitle) { + mActionBar.setSubTitle(subtitle); + } + + @Override + protected void setIcon(String icon) { + mActionBar.setIcon(icon); } /** * Creates a Popup and adds it to the content frame. It also adds another {@link FrameLayout} to * the content frame which shall serve as the new content root. */ + @Override public void createMenuPopup() { - assert mEnclosingLayout.getChildCount() == 1 - : "Action Bar Menus have already been created."; - if (!isOverflowPopupNeeded()) { return; } @@ -193,11 +188,6 @@ public class ActionBarLayout { return needed; } - @NonNull - public FrameLayout getContentRoot() { - return mContentRoot; - } - // Copied from com.android.internal.view.menu.MenuPopHelper.measureContentWidth() private int measureContentWidth(@NonNull ListAdapter adapter) { // Menus don't tend to be long, so this is more sane than it looks. diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomActionBarWrapper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FrameworkActionBarWrapper.java index 6db722ec680d..44c2cd85c8cd 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomActionBarWrapper.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FrameworkActionBarWrapper.java @@ -19,10 +19,8 @@ package com.android.layoutlib.bridge.bars; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.ActionBarCallback; -import com.android.ide.common.rendering.api.ActionBarCallback.HomeButtonStyle; import com.android.ide.common.rendering.api.RenderResources; import com.android.ide.common.rendering.api.ResourceValue; -import com.android.ide.common.rendering.api.SessionParams; import com.android.internal.R; import com.android.internal.app.ToolbarActionBar; import com.android.internal.app.WindowDecorActionBar; @@ -54,10 +52,9 @@ import static com.android.resources.ResourceType.MENU; /** * A common API to access {@link ToolbarActionBar} and {@link WindowDecorActionBar}. */ -public abstract class CustomActionBarWrapper { +public abstract class FrameworkActionBarWrapper { @NonNull protected ActionBar mActionBar; - @NonNull protected SessionParams mParams; @NonNull protected ActionBarCallback mCallback; @NonNull protected BridgeContext mContext; @@ -68,49 +65,48 @@ public abstract class CustomActionBarWrapper { * ?attr/windowActionBarFullscreenDecorLayout */ @NonNull - public static CustomActionBarWrapper getActionBarWrapper(@NonNull BridgeContext context, - @NonNull SessionParams params, @NonNull View decorContent) { + public static FrameworkActionBarWrapper getActionBarWrapper(@NonNull BridgeContext context, + @NonNull ActionBarCallback callback, @NonNull View decorContent) { View view = decorContent.findViewById(R.id.action_bar); if (view instanceof Toolbar) { - return new ToolbarWrapper(context, params, ((Toolbar) view)); + return new ToolbarWrapper(context, callback, (Toolbar) view); } else if (view instanceof ActionBarView) { - return new WindowActionBarWrapper(context, params, decorContent, - ((ActionBarView) view)); + return new WindowActionBarWrapper(context, callback, decorContent, + (ActionBarView) view); } else { throw new IllegalStateException("Can't make an action bar out of " + view.getClass().getSimpleName()); } } - CustomActionBarWrapper(@NonNull BridgeContext context, @NonNull SessionParams params, + FrameworkActionBarWrapper(@NonNull BridgeContext context, ActionBarCallback callback, @NonNull ActionBar actionBar) { mActionBar = actionBar; - mParams = params; - mCallback = params.getProjectCallback().getActionBarCallback(); + mCallback = callback; mContext = context; } + /** A call to setup any custom properties. */ protected void setupActionBar() { - // Do the things that are common to all implementations. - RenderResources res = mContext.getRenderResources(); + // Nothing to do here. + } - String title = mParams.getAppLabel(); - ResourceValue titleValue = res.findResValue(title, false); - if (titleValue != null && titleValue.getValue() != null) { - mActionBar.setTitle(titleValue.getValue()); - } else { - mActionBar.setTitle(title); - } + public void setTitle(CharSequence title) { + mActionBar.setTitle(title); + } - String subTitle = mCallback.getSubTitle(); + public void setSubTitle(CharSequence subTitle) { if (subTitle != null) { mActionBar.setSubtitle(subTitle); } + } - // Add show home as up icon. - if (mCallback.getHomeButtonStyle() == HomeButtonStyle.SHOW_HOME_AS_UP) { - mActionBar.setDisplayOptions(0xFF, ActionBar.DISPLAY_HOME_AS_UP); - } + public void setHomeAsUp(boolean homeAsUp) { + mActionBar.setDisplayHomeAsUpEnabled(homeAsUp); + } + + public void setIcon(String icon) { + // Nothing to do. } protected boolean isSplit() { @@ -186,15 +182,14 @@ public abstract class CustomActionBarWrapper { * Material theme uses {@link Toolbar} as the action bar. This wrapper provides access to * Toolbar using a common API. */ - private static class ToolbarWrapper extends CustomActionBarWrapper { + private static class ToolbarWrapper extends FrameworkActionBarWrapper { @NonNull private final Toolbar mToolbar; // This is the view. - ToolbarWrapper(@NonNull BridgeContext context, @NonNull SessionParams params, + ToolbarWrapper(@NonNull BridgeContext context, @NonNull ActionBarCallback callback, @NonNull Toolbar toolbar) { - super(context, params, new ToolbarActionBar(toolbar, "", new WindowCallback()) - ); + super(context, callback, new ToolbarActionBar(toolbar, "", new WindowCallback())); mToolbar = toolbar; } @@ -248,19 +243,17 @@ public abstract class CustomActionBarWrapper { * Holo theme uses {@link WindowDecorActionBar} as the action bar. This wrapper provides * access to it using a common API. */ - private static class WindowActionBarWrapper extends CustomActionBarWrapper { + private static class WindowActionBarWrapper extends FrameworkActionBarWrapper { - @NonNull - private final WindowDecorActionBar mActionBar; - @NonNull - private final ActionBarView mActionBarView; - @NonNull - private final View mDecorContentRoot; + @NonNull private final WindowDecorActionBar mActionBar; + @NonNull private final ActionBarView mActionBarView; + @NonNull private final View mDecorContentRoot; private MenuBuilder mMenuBuilder; - public WindowActionBarWrapper(@NonNull BridgeContext context, @NonNull SessionParams params, - @NonNull View decorContentRoot, @NonNull ActionBarView actionBarView) { - super(context, params, new WindowDecorActionBar(decorContentRoot)); + public WindowActionBarWrapper(@NonNull BridgeContext context, + @NonNull ActionBarCallback callback, @NonNull View decorContentRoot, + @NonNull ActionBarView actionBarView) { + super(context, callback, new WindowDecorActionBar(decorContentRoot)); mActionBarView = actionBarView; mActionBar = ((WindowDecorActionBar) super.mActionBar); mDecorContentRoot = decorContentRoot; @@ -268,7 +261,6 @@ public abstract class CustomActionBarWrapper { @Override protected void setupActionBar() { - super.setupActionBar(); // Set the navigation mode. int navMode = mCallback.getNavigationMode(); @@ -278,16 +270,6 @@ public abstract class CustomActionBarWrapper { setupTabs(3); } - String icon = mParams.getAppIcon(); - // If the action bar style doesn't specify an icon, set the icon obtained from the - // session params. - if (!mActionBar.hasIcon() && icon != null) { - Drawable iconDrawable = getDrawable(icon, false); - if (iconDrawable != null) { - mActionBar.setIcon(iconDrawable); - } - } - // Set action bar to be split, if needed. ViewGroup splitView = (ViewGroup) mDecorContentRoot.findViewById(R.id.split_action_bar); if (splitView != null) { @@ -300,6 +282,17 @@ public abstract class CustomActionBarWrapper { } @Override + public void setIcon(String icon) { + // Set the icon only if the action bar doesn't specify an icon. + if (!mActionBar.hasIcon() && icon != null) { + Drawable iconDrawable = getDrawable(icon, false); + if (iconDrawable != null) { + mActionBar.setIcon(iconDrawable); + } + } + } + + @Override protected void inflateMenus() { super.inflateMenus(); // The super implementation doesn't set the menu on the view. Set it here. @@ -340,7 +333,7 @@ public abstract class CustomActionBarWrapper { @Override int getMenuPopupMargin() { - return -ActionBarLayout.getPixelValue("10dp", mContext.getMetrics()); + return -FrameworkActionBar.getPixelValue("10dp", mContext.getMetrics()); } // TODO: Use an adapter, like List View to set up tabs. diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java index 4637bfd20549..58acab9aa318 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java @@ -51,11 +51,13 @@ import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes; import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; import com.android.layoutlib.bridge.android.SessionParamsFlags; +import com.android.layoutlib.bridge.bars.BridgeActionBar; +import com.android.layoutlib.bridge.bars.AppCompatActionBar; import com.android.layoutlib.bridge.bars.Config; import com.android.layoutlib.bridge.bars.NavigationBar; import com.android.layoutlib.bridge.bars.StatusBar; import com.android.layoutlib.bridge.bars.TitleBar; -import com.android.layoutlib.bridge.bars.ActionBarLayout; +import com.android.layoutlib.bridge.bars.FrameworkActionBar; import com.android.layoutlib.bridge.impl.binding.FakeAdapter; import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter; import com.android.resources.Density; @@ -354,7 +356,7 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { // if the theme says no title/action bar, then the size will be 0 if (mActionBarSize > 0) { - ActionBarLayout actionBar = createActionBar(context, params, backgroundLayout); + BridgeActionBar actionBar = createActionBar(context, params, backgroundLayout); actionBar.createMenuPopup(); mContentRoot = actionBar.getContentRoot(); } else if (mTitleBarSize > 0) { @@ -1190,8 +1192,22 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { // android.support.v7.app.ActionBarActivity, and not care about the theme name at all. if (mIsThemeAppCompat == null) { StyleResourceValue defaultTheme = resources.getDefaultTheme(); - StyleResourceValue val = resources.getStyle("Theme.AppCompat", false); - mIsThemeAppCompat = defaultTheme == val || resources.themeIsParentOf(val, defaultTheme); + // We can't simply check for parent using resources.themeIsParentOf() since the + // inheritance structure isn't really what one would expect. The first common parent + // between Theme.AppCompat.Light and Theme.AppCompat is Theme.Material (for v21). + boolean isThemeAppCompat = false; + for (int i = 0; i < 50; i++) { + // for loop ensures that we don't run into cyclic theme inheritance. + if (defaultTheme.getName().startsWith("Theme.AppCompat")) { + isThemeAppCompat = true; + break; + } + defaultTheme = resources.getParent(defaultTheme); + if (defaultTheme == null) { + break; + } + } + mIsThemeAppCompat = isThemeAppCompat; } return mIsThemeAppCompat; } @@ -1647,9 +1663,13 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { /** * Creates the action bar. Also queries the project callback for missing information. */ - private ActionBarLayout createActionBar(BridgeContext context, SessionParams params, + private BridgeActionBar createActionBar(BridgeContext context, SessionParams params, ViewGroup parentView) { - return new ActionBarLayout(context, params, parentView); + if (mIsThemeAppCompat == Boolean.TRUE) { + return new AppCompatActionBar(context, params, parentView); + } else { + return new FrameworkActionBar(context, params, parentView); + } } public BufferedImage getImage() { |