/* * 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 android.databinding; import android.app.Activity; import android.app.Application; import android.content.res.AssetManager; import android.content.res.Resources; import android.taobao.atlas.framework.Atlas; import android.taobao.atlas.framework.BundleImpl; import android.text.TextUtils; import android.util.TypedValue; import android.view.InflateException; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import org.osgi.framework.Bundle; import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; /** * Utility class to create {@link ViewDataBinding} from layouts. */ public class AtlasDataBindingUtil { private static HashMap<String, Object> sMappers = new HashMap(); private static DataBindingComponent sDefaultComponent = null; /** * Prevent DataBindingUtil from being instantiated. */ private AtlasDataBindingUtil() {} /** * Set the default {@link DataBindingComponent} to use for data binding. * <p> * <code>bindingComponent</code> may be passed as the first parameter of binding adapters. * <p> * When instance method BindingAdapters are used, the class instance for the binding adapter * is retrieved from the DataBindingComponent. */ public static void setDefaultComponent(DataBindingComponent bindingComponent) { sDefaultComponent = bindingComponent; } /** * Returns the default {@link DataBindingComponent} used in data binding. This can be * <code>null</code> if no default was set in * {@link #setDefaultComponent(DataBindingComponent)}. * * @return the default {@link DataBindingComponent} used in data binding. This can be * <code>null</code> if no default was set in * {@link #setDefaultComponent(DataBindingComponent)}. */ public static DataBindingComponent getDefaultComponent() { return sDefaultComponent; } /** * Inflates a binding layout and returns the newly-created binding for that layout. * This uses the DataBindingComponent set in * {@link #setDefaultComponent(DataBindingComponent)}. * <p> * Use this version only if <code>layoutId</code> is unknown in advance. Otherwise, use * the generated Binding's inflate method to ensure type-safe inflation. * * @param inflater The LayoutInflater used to inflate the binding layout. * @param layoutId The layout resource ID of the layout to inflate. * @param parent Optional view to be the parent of the generated hierarchy * (if attachToParent is true), or else simply an object that provides * a set of LayoutParams values for root of the returned hierarchy * (if attachToParent is false.) * @param attachToParent Whether the inflated hierarchy should be attached to the * parent parameter. If false, parent is only used to create * the correct subclass of LayoutParams for the root view in the XML. * @return The newly-created binding for the inflated layout or <code>null</code> if * the layoutId wasn't for a binding layout. * @throws InflateException When a merge layout was used and attachToParent was false. * @see #setDefaultComponent(DataBindingComponent) */ public static <T extends ViewDataBinding> T inflate(LayoutInflater inflater, int layoutId, ViewGroup parent, boolean attachToParent) { return inflate(inflater, layoutId, parent, attachToParent, sDefaultComponent); } /** * Inflates a binding layout and returns the newly-created binding for that layout. * <p> * Use this version only if <code>layoutId</code> is unknown in advance. Otherwise, use * the generated Binding's inflate method to ensure type-safe inflation. * * @param inflater The LayoutInflater used to inflate the binding layout. * @param layoutId The layout resource ID of the layout to inflate. * @param parent Optional view to be the parent of the generated hierarchy * (if attachToParent is true), or else simply an object that provides * a set of LayoutParams values for root of the returned hierarchy * (if attachToParent is false.) * @param attachToParent Whether the inflated hierarchy should be attached to the * parent parameter. If false, parent is only used to create * the correct subclass of LayoutParams for the root view in the XML. * @param bindingComponent The DataBindingComponent to use in the binding. * @return The newly-created binding for the inflated layout or <code>null</code> if * the layoutId wasn't for a binding layout. * @throws InflateException When a merge layout was used and attachToParent was false. */ public static <T extends ViewDataBinding> T inflate( LayoutInflater inflater, int layoutId, ViewGroup parent, boolean attachToParent, DataBindingComponent bindingComponent) { final boolean useChildren = parent != null && attachToParent; final int startChildren = useChildren ? parent.getChildCount() : 0; final View view = inflater.inflate(layoutId, parent, attachToParent); if (useChildren) { return bindToAddedViews(bindingComponent, parent, startChildren, layoutId); } else { return bind(bindingComponent, view, layoutId); } } /** * Returns the binding for the given layout root or creates a binding if one * does not exist. This uses the DataBindingComponent set in * {@link #setDefaultComponent(DataBindingComponent)}. * <p> * Prefer using the generated Binding's <code>bind</code> method to ensure type-safe inflation * when it is known that <code>root</code> has not yet been bound. * * @param root The root View of the inflated binding layout. * @return A ViewDataBinding for the given root View. If one already exists, the * existing one will be returned. * @throws IllegalArgumentException when root is not from an inflated binding layout. * @see #getBinding(View) */ @SuppressWarnings("unchecked") public static <T extends ViewDataBinding> T bind(View root) { return bind(root, sDefaultComponent); } /** * Returns the binding for the given layout root or creates a binding if one * does not exist. * <p> * Prefer using the generated Binding's <code>bind</code> method to ensure type-safe inflation * when it is known that <code>root</code> has not yet been bound. * * @param root The root View of the inflated binding layout. * @param bindingComponent The DataBindingComponent to use in data binding. * @return A ViewDataBinding for the given root View. If one already exists, the * existing one will be returned. * @throws IllegalArgumentException when root is not from an inflated binding layout. * @see #getBinding(View) */ @SuppressWarnings("unchecked") public static <T extends ViewDataBinding> T bind(View root, DataBindingComponent bindingComponent) { T binding = getBinding(root); if (binding != null) { return binding; } Object tagObj = root.getTag(); if (!(tagObj instanceof String)) { throw new IllegalArgumentException("View is not a binding layout"); } else { String tag = (String) tagObj; int id = root.getId(); if (id == -1) { throw new RuntimeException("must set a valid ID for view " + root); } Object mapper = getDataBinderMapper((Application)root.getContext().getApplicationContext(), root.getResources(), id); try { Method getLayoutIdMethod = mapper.getClass().getDeclaredMethod("getLayoutId", new Class[] { String.class }); getLayoutIdMethod.setAccessible(true); int layoutId = ((Integer)getLayoutIdMethod.invoke(mapper, new Object[] { tag })).intValue(); if (layoutId == 0) { throw new IllegalArgumentException("View is not a binding layout"); } Method getDataBinderMethod = mapper.getClass().getDeclaredMethod("getDataBinder", new Class[] { DataBindingComponent.class, View.class, Integer.TYPE }); getDataBinderMethod.setAccessible(true); return (T)getDataBinderMethod.invoke(bindingComponent, new Object[] { root, Integer.valueOf(layoutId) }); } catch (Throwable e) { throw new RuntimeException(e); } } } @SuppressWarnings("unchecked") static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View[] roots, int layoutId) { // return (T) sMapper.getDataBinder(bindingComponent, roots, layoutId); Object mapper = getDataBinderMapper((Application)roots[0].getContext().getApplicationContext(), roots[0].getResources(), layoutId); try{ Method getDataBinderMethod = mapper.getClass().getDeclaredMethod("getDataBinder", new Class[] { DataBindingComponent.class, View[].class, Integer.TYPE }); getDataBinderMethod.setAccessible(true); return (T)getDataBinderMethod.invoke(mapper, new Object[] { bindingComponent, roots, Integer.valueOf(layoutId) }); } catch (Throwable e) { throw new RuntimeException(e); } } static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root, int layoutId) { // return (T) sMapper.getDataBinder(bindingComponent, root, layoutId); Object mapper = getDataBinderMapper((Application)root.getContext().getApplicationContext(), root.getResources(), layoutId); Method getDataBinderMethod = null; try { getDataBinderMethod = mapper.getClass().getDeclaredMethod("getDataBinder", new Class[] { DataBindingComponent.class, View.class, Integer.TYPE }); getDataBinderMethod.setAccessible(true); return (T)getDataBinderMethod.invoke(mapper, new Object[] { bindingComponent, root, Integer.valueOf(layoutId) }); } catch (Throwable e) { throw new RuntimeException(e); } } /** * Retrieves the binding responsible for the given View. If <code>view</code> is not a * binding layout root, its parents will be searched for the binding. If there is no binding, * <code>null</code> will be returned. * <p> * This differs from {@link #getBinding(View)} in that findBinding takes any view in the * layout and searches for the binding associated with the root. <code>getBinding</code> * takes only the root view. * * @param view A <code>View</code> in the bound layout. * @return The ViewDataBinding associated with the given view or <code>null</code> if * view is not part of a bound layout. */ public static <T extends ViewDataBinding> T findBinding(View view) { while (view != null) { ViewDataBinding binding = ViewDataBinding.getBinding(view); if (binding != null) { return (T) binding; } Object tag = view.getTag(); if (tag instanceof String) { String tagString = (String) tag; if (tagString.startsWith("layout") && tagString.endsWith("_0")) { final char nextChar = tagString.charAt(6); final int slashIndex = tagString.indexOf('/', 7); boolean isUnboundRoot = false; if (nextChar == '/') { // only one slash should exist isUnboundRoot = slashIndex == -1; } else if (nextChar == '-' && slashIndex != -1) { int nextSlashIndex = tagString.indexOf('/', slashIndex + 1); // only one slash should exist isUnboundRoot = nextSlashIndex == -1; } if (isUnboundRoot) { // An inflated, but unbound layout return null; } } } ViewParent viewParent = view.getParent(); if (viewParent instanceof View) { view = (View) viewParent; } else { view = null; } } return null; } /** * Retrieves the binding responsible for the given View layout root. If there is no binding, * <code>null</code> will be returned. This uses the DataBindingComponent set in * {@link #setDefaultComponent(DataBindingComponent)}. * * @param view The root <code>View</code> in the layout with binding. * @return The ViewDataBinding associated with the given view or <code>null</code> if * either the view is not a root View for a layout or view hasn't been bound. */ public static <T extends ViewDataBinding> T getBinding(View view) { return (T) ViewDataBinding.getBinding(view); } /** * Set the Activity's content view to the given layout and return the associated binding. * The given layout resource must not be a merge layout. * * @param activity The Activity whose content View should change. * @param layoutId The resource ID of the layout to be inflated, bound, and set as the * Activity's content. * @return The binding associated with the inflated content view. */ public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId) { return setContentView(activity, layoutId, sDefaultComponent); } /** * Set the Activity's content view to the given layout and return the associated binding. * The given layout resource must not be a merge layout. * * @param bindingComponent The DataBindingComponent to use in data binding. * @param activity The Activity whose content View should change. * @param layoutId The resource ID of the layout to be inflated, bound, and set as the * Activity's content. * @return The binding associated with the inflated content view. */ public static <T extends ViewDataBinding> T setContentView(Activity activity, int layoutId, DataBindingComponent bindingComponent) { activity.setContentView(layoutId); View decorView = activity.getWindow().getDecorView(); ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content); return bindToAddedViews(bindingComponent, contentView, 0, layoutId); } /** * Converts the given BR id to its string representation which might be useful for logging * purposes. * * @param id The integer id, which should be a field from BR class. * @return The name if the BR id or null if id is out of bounds. */ public static String convertBrIdToString(int id) { // return sMapper.convertBrIdToString(id); return ""; } private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component, ViewGroup parent, int startChildren, int layoutId) { final int endChildren = parent.getChildCount(); final int childrenAdded = endChildren - startChildren; if (childrenAdded == 1) { final View childView = parent.getChildAt(endChildren - 1); return bind(component, childView, layoutId); } else { final View[] children = new View[childrenAdded]; for (int i = 0; i < childrenAdded; i++) { children[i] = parent.getChildAt(i + startChildren); } return bind(component, children, layoutId); } } private static Object getDataBinderMapper(Application application, Resources resource, int resourceId) { TypedValue value = new TypedValue(); resource.getValue(resourceId, value, false); int cookie = value.assetCookie; try { String className = null; String bundleLocation = null; String assetsPath = (String)AssetManager.class.getMethod("getCookieName", new Class[] { Integer.TYPE }).invoke(resource.getAssets(), new Object[] { Integer.valueOf(cookie) }); if (assetsPath.endsWith(".zip")) { bundleLocation = substringBetween(assetsPath, "/storage/", "/version."); className = String.format("%s.%s", new Object[] { bundleLocation, "DataBinderMapper" }); } else if (assetsPath.endsWith(".so")) { List<Bundle> bundles = Atlas.getInstance().getBundles(); for (int x = 0; x < bundles.size(); x++) { BundleImpl impl = (BundleImpl)bundles.get(x); if (impl.getArchive().getArchiveFile().getAbsolutePath().equals(assetsPath)) { bundleLocation = impl.getLocation(); className = String.format("%s.%s", new Object[] { bundleLocation, "DataBinderMapper" }); break; } } } else { className = "android.databinding.DataBinderMapper"; } if (TextUtils.isEmpty(className)) { throw new RuntimeException("can not find DatabindMapper : " + assetsPath); } Class clazz = application.getClassLoader().loadClass(className); Object dataMapper = clazz.newInstance(); sMappers.put(bundleLocation, dataMapper); return dataMapper; } catch (Throwable e) { throw new RuntimeException(e); } } public static String substringBetween(String str, String open, String close) { if ((str == null) || (open == null) || (close == null)) { return null; } int start = str.indexOf(open); if (start != -1) { int end = str.indexOf(close, start + open.length()); if (end != -1) { return str.substring(start + open.length(), end); } } return null; } }