Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.view;
     18 
     19 import com.android.ide.common.rendering.api.LayoutLog;
     20 import com.android.ide.common.rendering.api.LayoutlibCallback;
     21 import com.android.ide.common.rendering.api.MergeCookie;
     22 import com.android.ide.common.rendering.api.ResourceReference;
     23 import com.android.ide.common.rendering.api.ResourceValue;
     24 import com.android.layoutlib.bridge.Bridge;
     25 import com.android.layoutlib.bridge.BridgeConstants;
     26 import com.android.layoutlib.bridge.MockView;
     27 import com.android.layoutlib.bridge.android.BridgeContext;
     28 import com.android.layoutlib.bridge.android.BridgeXmlBlockParser;
     29 import com.android.layoutlib.bridge.android.support.DrawerLayoutUtil;
     30 import com.android.layoutlib.bridge.android.support.RecyclerViewUtil;
     31 import com.android.layoutlib.bridge.impl.ParserFactory;
     32 import com.android.layoutlib.bridge.util.ReflectionUtils;
     33 import com.android.resources.ResourceType;
     34 import com.android.tools.layoutlib.annotations.NotNull;
     35 import com.android.tools.layoutlib.annotations.Nullable;
     36 import com.android.util.Pair;
     37 
     38 import org.xmlpull.v1.XmlPullParser;
     39 
     40 import android.annotation.NonNull;
     41 import android.content.Context;
     42 import android.content.res.TypedArray;
     43 import android.graphics.drawable.Animatable;
     44 import android.graphics.drawable.Drawable;
     45 import android.util.AttributeSet;
     46 import android.widget.ImageView;
     47 import android.widget.NumberPicker;
     48 
     49 import java.io.File;
     50 import java.lang.reflect.Constructor;
     51 import java.lang.reflect.InvocationTargetException;
     52 import java.lang.reflect.Method;
     53 import java.util.HashMap;
     54 import java.util.Map;
     55 import java.util.function.BiFunction;
     56 
     57 import static com.android.layoutlib.bridge.android.BridgeContext.getBaseContext;
     58 
     59 /**
     60  * Custom implementation of {@link LayoutInflater} to handle custom views.
     61  */
     62 public final class BridgeInflater extends LayoutInflater {
     63 
     64     private final LayoutlibCallback mLayoutlibCallback;
     65 
     66     private boolean mIsInMerge = false;
     67     private ResourceReference mResourceReference;
     68     private Map<View, String> mOpenDrawerLayouts;
     69 
     70     // Keep in sync with the same value in LayoutInflater.
     71     private static final int[] ATTRS_THEME = new int[] {com.android.internal.R.attr.theme };
     72 
     73     /**
     74      * List of class prefixes which are tried first by default.
     75      * <p/>
     76      * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater.
     77      */
     78     private static final String[] sClassPrefixList = {
     79         "android.widget.",
     80         "android.webkit.",
     81         "android.app."
     82     };
     83     private BiFunction<String, AttributeSet, View> mCustomInflater;
     84 
     85     public static String[] getClassPrefixList() {
     86         return sClassPrefixList;
     87     }
     88 
     89     private BridgeInflater(LayoutInflater original, Context newContext) {
     90         super(original, newContext);
     91         newContext = getBaseContext(newContext);
     92         mLayoutlibCallback = (newContext instanceof BridgeContext) ?
     93                 ((BridgeContext) newContext).getLayoutlibCallback() :
     94                 null;
     95     }
     96 
     97     /**
     98      * Instantiate a new BridgeInflater with an {@link LayoutlibCallback} object.
     99      *
    100      * @param context The Android application context.
    101      * @param layoutlibCallback the {@link LayoutlibCallback} object.
    102      */
    103     public BridgeInflater(BridgeContext context, LayoutlibCallback layoutlibCallback) {
    104         super(context);
    105         mLayoutlibCallback = layoutlibCallback;
    106         mConstructorArgs[0] = context;
    107     }
    108 
    109     @Override
    110     public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
    111         View view = createViewFromCustomInflater(name, attrs);
    112 
    113         if (view == null) {
    114             try {
    115                 // First try to find a class using the default Android prefixes
    116                 for (String prefix : sClassPrefixList) {
    117                     try {
    118                         view = createView(name, prefix, attrs);
    119                         if (view != null) {
    120                             break;
    121                         }
    122                     } catch (ClassNotFoundException e) {
    123                         // Ignore. We'll try again using the base class below.
    124                     }
    125                 }
    126 
    127                 // Next try using the parent loader. This will most likely only work for
    128                 // fully-qualified class names.
    129                 try {
    130                     if (view == null) {
    131                         view = super.onCreateView(name, attrs);
    132                     }
    133                 } catch (ClassNotFoundException e) {
    134                     // Ignore. We'll try again using the custom view loader below.
    135                 }
    136 
    137                 // Finally try again using the custom view loader
    138                 if (view == null) {
    139                     view = loadCustomView(name, attrs);
    140                 }
    141             } catch (InflateException e) {
    142                 // Don't catch the InflateException below as that results in hiding the real cause.
    143                 throw e;
    144             } catch (Exception e) {
    145                 // Wrap the real exception in a ClassNotFoundException, so that the calling method
    146                 // can deal with it.
    147                 throw new ClassNotFoundException("onCreateView", e);
    148             }
    149         }
    150 
    151         setupViewInContext(view, attrs);
    152 
    153         return view;
    154     }
    155 
    156     /**
    157      * Finds the createView method in the given customInflaterClass. Since createView is
    158      * currently package protected, it will show in the declared class so we iterate up the
    159      * hierarchy and return the first instance we find.
    160      * The returned method will be accessible.
    161      */
    162     @NotNull
    163     private static Method getCreateViewMethod(Class<?> customInflaterClass) throws NoSuchMethodException {
    164         Class<?> current = customInflaterClass;
    165         do {
    166             try {
    167                 Method method = current.getDeclaredMethod("createView", View.class, String.class,
    168                                 Context.class, AttributeSet.class, boolean.class, boolean.class,
    169                                 boolean.class, boolean.class);
    170                 method.setAccessible(true);
    171                 return method;
    172             } catch (NoSuchMethodException ignore) {
    173             }
    174             current = current.getSuperclass();
    175         } while (current != null && current != Object.class);
    176 
    177         throw new NoSuchMethodException();
    178     }
    179 
    180     /**
    181      * Finds the custom inflater class. If it's defined in the theme, we'll use that one (if the
    182      * class does not exist, null is returned).
    183      * If {@code viewInflaterClass} is not defined in the theme, we'll try to instantiate
    184      * {@code android.support.v7.app.AppCompatViewInflater}
    185      */
    186     @Nullable
    187     private static Class<?> findCustomInflater(@NotNull BridgeContext bc,
    188             @NotNull LayoutlibCallback layoutlibCallback) {
    189         ResourceValue value = bc.getRenderResources().findItemInTheme("viewInflaterClass", false);
    190         String inflaterName = value != null ? value.getValue() : null;
    191 
    192         if (inflaterName != null) {
    193             try {
    194                 return layoutlibCallback.findClass(inflaterName);
    195             } catch (ClassNotFoundException ignore) {
    196             }
    197 
    198             // viewInflaterClass was defined but we couldn't find the class
    199         } else if (bc.isAppCompatTheme()) {
    200             // Older versions of AppCompat do not define the viewInflaterClass so try to get it
    201             // manually
    202             try {
    203                 return layoutlibCallback.findClass("android.support.v7.app.AppCompatViewInflater");
    204             } catch (ClassNotFoundException ignore) {
    205             }
    206         }
    207 
    208         return null;
    209     }
    210 
    211     /**
    212      * Checks if there is a custom inflater and, when present, tries to instantiate the view
    213      * using it.
    214      */
    215     @Nullable
    216     private View createViewFromCustomInflater(@NotNull String name, @NotNull AttributeSet attrs) {
    217         if (mCustomInflater == null) {
    218             Context context = getContext();
    219             context = getBaseContext(context);
    220             if (context instanceof BridgeContext) {
    221                 BridgeContext bc = (BridgeContext) context;
    222                 Class<?> inflaterClass = findCustomInflater(bc, mLayoutlibCallback);
    223 
    224                 if (inflaterClass != null) {
    225                     try {
    226                         Constructor<?> constructor =  inflaterClass.getDeclaredConstructor();
    227                         constructor.setAccessible(true);
    228                         Object inflater = constructor.newInstance();
    229                         Method method = getCreateViewMethod(inflaterClass);
    230                         Context finalContext = context;
    231                         mCustomInflater = (viewName, attributeSet) -> {
    232                             try {
    233                                 return (View) method.invoke(inflater, null, viewName, finalContext,
    234                                         attributeSet,
    235                                         false,
    236                                         false /*readAndroidTheme*/, // No need after L
    237                                         true /*readAppTheme*/,
    238                                         true /*wrapContext*/);
    239                             } catch (IllegalAccessException | InvocationTargetException e) {
    240                                 assert false : "Call to createView failed";
    241                             }
    242                             return null;
    243                         };
    244                     } catch (InvocationTargetException | IllegalAccessException |
    245                             NoSuchMethodException | InstantiationException ignore) {
    246                     }
    247                 }
    248             }
    249 
    250             if (mCustomInflater == null) {
    251                 // There is no custom inflater. We'll create a nop custom inflater to avoid the
    252                 // penalty of trying to instantiate again
    253                 mCustomInflater = (s, attributeSet) -> null;
    254             }
    255         }
    256 
    257         return mCustomInflater.apply(name, attrs);
    258     }
    259 
    260     @Override
    261     public View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
    262             boolean ignoreThemeAttr) {
    263         View view = null;
    264         if (name.equals("view")) {
    265             // This is usually done by the superclass but this allows us catching the error and
    266             // reporting something useful.
    267             name = attrs.getAttributeValue(null, "class");
    268 
    269             if (name == null) {
    270                 Bridge.getLog().error(LayoutLog.TAG_BROKEN, "Unable to inflate view tag without " +
    271                   "class attribute", null);
    272                 // We weren't able to resolve the view so we just pass a mock View to be able to
    273                 // continue rendering.
    274                 view = new MockView(context, attrs);
    275                 ((MockView) view).setText("view");
    276             }
    277         }
    278 
    279         try {
    280             if (view == null) {
    281                 view = super.createViewFromTag(parent, name, context, attrs, ignoreThemeAttr);
    282             }
    283         } catch (InflateException e) {
    284             // Creation of ContextThemeWrapper code is same as in the super method.
    285             // Apply a theme wrapper, if allowed and one is specified.
    286             if (!ignoreThemeAttr) {
    287                 final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
    288                 final int themeResId = ta.getResourceId(0, 0);
    289                 if (themeResId != 0) {
    290                     context = new ContextThemeWrapper(context, themeResId);
    291                 }
    292                 ta.recycle();
    293             }
    294             if (!(e.getCause() instanceof ClassNotFoundException)) {
    295                 // There is some unknown inflation exception in inflating a View that was found.
    296                 view = new MockView(context, attrs);
    297                 ((MockView) view).setText(name);
    298                 Bridge.getLog().error(LayoutLog.TAG_BROKEN, e.getMessage(), e, null);
    299             } else {
    300                 final Object lastContext = mConstructorArgs[0];
    301                 mConstructorArgs[0] = context;
    302                 // try to load the class from using the custom view loader
    303                 try {
    304                     view = loadCustomView(name, attrs);
    305                 } catch (Exception e2) {
    306                     // Wrap the real exception in an InflateException so that the calling
    307                     // method can deal with it.
    308                     InflateException exception = new InflateException();
    309                     if (!e2.getClass().equals(ClassNotFoundException.class)) {
    310                         exception.initCause(e2);
    311                     } else {
    312                         exception.initCause(e);
    313                     }
    314                     throw exception;
    315                 } finally {
    316                     mConstructorArgs[0] = lastContext;
    317                 }
    318             }
    319         }
    320 
    321         setupViewInContext(view, attrs);
    322 
    323         return view;
    324     }
    325 
    326     @Override
    327     public View inflate(int resource, ViewGroup root) {
    328         Context context = getContext();
    329         context = getBaseContext(context);
    330         if (context instanceof BridgeContext) {
    331             BridgeContext bridgeContext = (BridgeContext)context;
    332 
    333             ResourceValue value = null;
    334 
    335             @SuppressWarnings("deprecation")
    336             Pair<ResourceType, String> layoutInfo = Bridge.resolveResourceId(resource);
    337             if (layoutInfo != null) {
    338                 value = bridgeContext.getRenderResources().getFrameworkResource(
    339                         ResourceType.LAYOUT, layoutInfo.getSecond());
    340             } else {
    341                 layoutInfo = mLayoutlibCallback.resolveResourceId(resource);
    342 
    343                 if (layoutInfo != null) {
    344                     value = bridgeContext.getRenderResources().getProjectResource(
    345                             ResourceType.LAYOUT, layoutInfo.getSecond());
    346                 }
    347             }
    348 
    349             if (value != null) {
    350                 File f = new File(value.getValue());
    351                 if (f.isFile()) {
    352                     try {
    353                         XmlPullParser parser = ParserFactory.create(f, true);
    354 
    355                         BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser(
    356                                 parser, bridgeContext, value.isFramework());
    357 
    358                         return inflate(bridgeParser, root);
    359                     } catch (Exception e) {
    360                         Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ,
    361                                 "Failed to parse file " + f.getAbsolutePath(), e, null);
    362 
    363                         return null;
    364                     }
    365                 }
    366             }
    367         }
    368         return null;
    369     }
    370 
    371     /**
    372      * Instantiates the given view name and returns the instance. If the view doesn't exist, a
    373      * MockView or null might be returned.
    374      * @param name the custom view name
    375      * @param attrs the {@link AttributeSet} to be passed to the view constructor
    376      * @param silent if true, errors while loading the view won't be reported and, if the view
    377      * doesn't exist, null will be returned.
    378      */
    379     private View loadCustomView(String name, AttributeSet attrs, boolean silent) throws Exception {
    380         if (mLayoutlibCallback != null) {
    381             // first get the classname in case it's not the node name
    382             if (name.equals("view")) {
    383                 name = attrs.getAttributeValue(null, "class");
    384                 if (name == null) {
    385                     return null;
    386                 }
    387             }
    388 
    389             mConstructorArgs[1] = attrs;
    390 
    391             Object customView = silent ?
    392                     mLayoutlibCallback.loadClass(name, mConstructorSignature, mConstructorArgs)
    393                     : mLayoutlibCallback.loadView(name, mConstructorSignature, mConstructorArgs);
    394 
    395             if (customView instanceof View) {
    396                 return (View)customView;
    397             }
    398         }
    399 
    400         return null;
    401     }
    402 
    403     private View loadCustomView(String name, AttributeSet attrs) throws Exception {
    404         return loadCustomView(name, attrs, false);
    405     }
    406 
    407     private void setupViewInContext(View view, AttributeSet attrs) {
    408         Context context = getContext();
    409         context = getBaseContext(context);
    410         if (context instanceof BridgeContext) {
    411             BridgeContext bc = (BridgeContext) context;
    412             // get the view key
    413             Object viewKey = getViewKeyFromParser(attrs, bc, mResourceReference, mIsInMerge);
    414             if (viewKey != null) {
    415                 bc.addViewKey(view, viewKey);
    416             }
    417             String scrollPosX = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollX");
    418             if (scrollPosX != null && scrollPosX.endsWith("px")) {
    419                 int value = Integer.parseInt(scrollPosX.substring(0, scrollPosX.length() - 2));
    420                 bc.setScrollXPos(view, value);
    421             }
    422             String scrollPosY = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollY");
    423             if (scrollPosY != null && scrollPosY.endsWith("px")) {
    424                 int value = Integer.parseInt(scrollPosY.substring(0, scrollPosY.length() - 2));
    425                 bc.setScrollYPos(view, value);
    426             }
    427             if (ReflectionUtils.isInstanceOf(view, RecyclerViewUtil.CN_RECYCLER_VIEW)) {
    428                 Integer resourceId = null;
    429                 String attrListItemValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI,
    430                         BridgeConstants.ATTR_LIST_ITEM);
    431                 int attrItemCountValue = attrs.getAttributeIntValue(BridgeConstants.NS_TOOLS_URI,
    432                         BridgeConstants.ATTR_ITEM_COUNT, -1);
    433                 if (attrListItemValue != null && !attrListItemValue.isEmpty()) {
    434                     ResourceValue resValue = bc.getRenderResources().findResValue(attrListItemValue, false);
    435                     if (resValue.isFramework()) {
    436                         resourceId = Bridge.getResourceId(resValue.getResourceType(),
    437                                 resValue.getName());
    438                     } else {
    439                         resourceId = mLayoutlibCallback.getResourceId(resValue.getResourceType(),
    440                                 resValue.getName());
    441                     }
    442                 }
    443                 if (resourceId == null) {
    444                     resourceId = 0;
    445                 }
    446                 RecyclerViewUtil.setAdapter(view, bc, mLayoutlibCallback, resourceId, attrItemCountValue);
    447             } else if (ReflectionUtils.isInstanceOf(view, DrawerLayoutUtil.CN_DRAWER_LAYOUT)) {
    448                 String attrVal = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI,
    449                         BridgeConstants.ATTR_OPEN_DRAWER);
    450                 if (attrVal != null) {
    451                     getDrawerLayoutMap().put(view, attrVal);
    452                 }
    453             }
    454             else if (view instanceof NumberPicker) {
    455                 NumberPicker numberPicker = (NumberPicker) view;
    456                 String minValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "minValue");
    457                 if (minValue != null) {
    458                     numberPicker.setMinValue(Integer.parseInt(minValue));
    459                 }
    460                 String maxValue = attrs.getAttributeValue(BridgeConstants.NS_TOOLS_URI, "maxValue");
    461                 if (maxValue != null) {
    462                     numberPicker.setMaxValue(Integer.parseInt(maxValue));
    463                 }
    464             }
    465             else if (view instanceof ImageView) {
    466                 ImageView img = (ImageView) view;
    467                 Drawable drawable = img.getDrawable();
    468                 if (drawable instanceof Animatable) {
    469                     if (!((Animatable) drawable).isRunning()) {
    470                         ((Animatable) drawable).start();
    471                     }
    472                 }
    473             }
    474 
    475         }
    476     }
    477 
    478     public void setIsInMerge(boolean isInMerge) {
    479         mIsInMerge = isInMerge;
    480     }
    481 
    482     public void setResourceReference(ResourceReference reference) {
    483         mResourceReference = reference;
    484     }
    485 
    486     @Override
    487     public LayoutInflater cloneInContext(Context newContext) {
    488         return new BridgeInflater(this, newContext);
    489     }
    490 
    491     /*package*/ static Object getViewKeyFromParser(AttributeSet attrs, BridgeContext bc,
    492             ResourceReference resourceReference, boolean isInMerge) {
    493 
    494         if (!(attrs instanceof BridgeXmlBlockParser)) {
    495             return null;
    496         }
    497         BridgeXmlBlockParser parser = ((BridgeXmlBlockParser) attrs);
    498 
    499         // get the view key
    500         Object viewKey = parser.getViewCookie();
    501 
    502         if (viewKey == null) {
    503             int currentDepth = parser.getDepth();
    504 
    505             // test whether we are in an included file or in a adapter binding view.
    506             BridgeXmlBlockParser previousParser = bc.getPreviousParser();
    507             if (previousParser != null) {
    508                 // looks like we are inside an embedded layout.
    509                 // only apply the cookie of the calling node (<include>) if we are at the
    510                 // top level of the embedded layout. If there is a merge tag, then
    511                 // skip it and look for the 2nd level
    512                 int testDepth = isInMerge ? 2 : 1;
    513                 if (currentDepth == testDepth) {
    514                     viewKey = previousParser.getViewCookie();
    515                     // if we are in a merge, wrap the cookie in a MergeCookie.
    516                     if (viewKey != null && isInMerge) {
    517                         viewKey = new MergeCookie(viewKey);
    518                     }
    519                 }
    520             } else if (resourceReference != null && currentDepth == 1) {
    521                 // else if there's a resource reference, this means we are in an adapter
    522                 // binding case. Set the resource ref as the view cookie only for the top
    523                 // level view.
    524                 viewKey = resourceReference;
    525             }
    526         }
    527 
    528         return viewKey;
    529     }
    530 
    531     public void postInflateProcess(View view) {
    532         if (mOpenDrawerLayouts != null) {
    533             String gravity = mOpenDrawerLayouts.get(view);
    534             if (gravity != null) {
    535                 DrawerLayoutUtil.openDrawer(view, gravity);
    536             }
    537             mOpenDrawerLayouts.remove(view);
    538         }
    539     }
    540 
    541     @NonNull
    542     private Map<View, String> getDrawerLayoutMap() {
    543         if (mOpenDrawerLayouts == null) {
    544             mOpenDrawerLayouts = new HashMap<View, String>(4);
    545         }
    546         return mOpenDrawerLayouts;
    547     }
    548 
    549     public void onDoneInflation() {
    550         if (mOpenDrawerLayouts != null) {
    551             mOpenDrawerLayouts.clear();
    552         }
    553     }
    554 }
    555