package com.afollestad.aesthetic; import android.content.Context; import android.content.res.TypedArray; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RestrictTo; import android.support.v4.view.LayoutInflaterFactory; import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatDelegate; import android.support.v7.view.ContextThemeWrapper; import android.support.v7.widget.CardView; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; import java.lang.reflect.Field; import java.lang.reflect.Method; import io.reactivex.Observable; import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static com.afollestad.aesthetic.Util.resolveResId; /** @author Aidan Follestad (afollestad) */ @RestrictTo(LIBRARY_GROUP) final class InflationInterceptor implements LayoutInflaterFactory { private final Method onCreateViewMethod; private final Method createViewMethod; private final Field constructorArgsField; private final AppCompatActivity keyContext; @NonNull private final LayoutInflater layoutInflater; @Nullable private final AppCompatDelegate delegate; private int[] ATTRS_THEME; InflationInterceptor( @Nullable AppCompatActivity keyContext, @NonNull LayoutInflater li, @Nullable AppCompatDelegate delegate) { this.keyContext = keyContext; layoutInflater = li; this.delegate = delegate; try { onCreateViewMethod = LayoutInflater.class.getDeclaredMethod( "onCreateView", View.class, String.class, AttributeSet.class); } catch (NoSuchMethodException e) { throw new IllegalStateException("Failed to retrieve the onCreateView method.", e); } try { createViewMethod = LayoutInflater.class.getDeclaredMethod( "createView", String.class, String.class, AttributeSet.class); } catch (NoSuchMethodException e) { throw new IllegalStateException("Failed to retrieve the createView method.", e); } try { constructorArgsField = LayoutInflater.class.getDeclaredField("mConstructorArgs"); } catch (NoSuchFieldException e) { throw new IllegalStateException("Failed to retrieve the mConstructorArgs field.", e); } try { final Field attrsThemeField = LayoutInflater.class.getDeclaredField("ATTRS_THEME"); attrsThemeField.setAccessible(true); ATTRS_THEME = (int[]) attrsThemeField.get(null); } catch (Throwable t) { t.printStackTrace(); Log.d( "InflationInterceptor", "Failed to get the value of static field ATTRS_THEME: " + t.getMessage()); } onCreateViewMethod.setAccessible(true); createViewMethod.setAccessible(true); constructorArgsField.setAccessible(true); } private static void log(String msg, Object... args) { //noinspection PointlessBooleanExpression if (args != null) { Log.d("InflationInterceptor", String.format(msg, args)); } else { Log.d("InflationInterceptor", msg); } } private boolean isBlackListedForApply(String name) { return "android.support.design.internal.NavigationMenuItemView".equals(name) || "ViewStub".equals(name) || "fragment".equals(name) || "include".equals(name); } @Override public View onCreateView(View parent, final String name, Context context, AttributeSet attrs) { View view = null; final int viewId = resolveResId(context, attrs, android.R.attr.id); switch (name) { case "ImageView": case "android.support.v7.widget.AppCompatImageView": view = new AestheticImageView(context, attrs); break; case "ImageButton": case "android.support.v7.widget.AppCompatImageButton": view = new AestheticImageButton(context, attrs); break; case "android.support.v4.widget.DrawerLayout": view = new AestheticDrawerLayout(context, attrs); break; case "Toolbar": case "android.support.v7.widget.Toolbar": view = new AestheticToolbar(context, attrs); break; case "android.support.v7.widget.AppCompatTextView": case "TextView": if (viewId == R.id.snackbar_text) { view = new AestheticSnackBarTextView(context, attrs); } else { view = new AestheticTextView(context, attrs); if (parent instanceof LinearLayout && view.getId() == android.R.id.message) { // This is for a toast message view = null; } } break; case "Button": case "android.support.v7.widget.AppCompatButton": if (viewId == R.id.snackbar_action) { view = new AestheticSnackBarButton(context, attrs); } else { view = new AestheticButton(context, attrs); } break; case "android.support.v7.widget.AppCompatCheckBox": case "CheckBox": view = new AestheticCheckBox(context, attrs); break; case "android.support.v7.widget.AppCompatRadioButton": case "RadioButton": view = new AestheticRadioButton(context, attrs); break; case "android.support.v7.widget.AppCompatEditText": case "EditText": view = new AestheticEditText(context, attrs); break; case "Switch": view = new AestheticSwitch(context, attrs); break; case "android.support.v7.widget.SwitchCompat": view = new AestheticSwitchCompat(context, attrs); break; case "android.support.v7.widget.AppCompatSeekBar": case "SeekBar": view = new AestheticSeekBar(context, attrs); break; case "ProgressBar": view = new AestheticProgressBar(context, attrs); break; case "android.support.v7.view.menu.ActionMenuItemView": view = new AestheticActionMenuItemView(context, attrs); break; case "android.support.v7.widget.RecyclerView": view = new AestheticRecyclerView(context, attrs); break; case "android.support.v4.widget.NestedScrollView": view = new AestheticNestedScrollView(context, attrs); break; case "ListView": view = new AestheticListView(context, attrs); break; case "ScrollView": view = new AestheticScrollView(context, attrs); break; case "android.support.v4.view.ViewPager": view = new AestheticViewPager(context, attrs); break; case "Spinner": case "android.support.v7.widget.AppCompatSpinner": view = new AestheticSpinner(context, attrs); break; case "android.support.design.widget.TextInputLayout": view = new AestheticTextInputLayout(context, attrs); break; case "android.support.design.widget.TextInputEditText": view = new AestheticTextInputEditText(context, attrs); break; case "android.support.design.widget.TabLayout": view = new AestheticTabLayout(context, attrs); break; case "android.support.design.widget.NavigationView": view = new AestheticNavigationView(context, attrs); break; case "android.support.design.widget.BottomNavigationView": view = new AestheticBottomNavigationView(context, attrs); break; case "android.support.design.widget.FloatingActionButton": view = new AestheticFab(context, attrs); break; case "android.support.design.widget.CoordinatorLayout": view = new AestheticCoordinatorLayout(context, attrs); break; } int viewBackgroundRes = 0; if (view != null && view.getTag() != null && ":aesthetic_ignore".equals(view.getTag())) { // Set view back to null so we can let AppCompat handle this view instead. view = null; } else if (attrs != null) { viewBackgroundRes = resolveResId(context, attrs, android.R.attr.background); } if (view == null) { // First, check if the AppCompatDelegate will give us a view, usually (maybe always) null. if (delegate != null) { view = delegate.createView(parent, name, context, attrs); if (view == null) { view = keyContext.onCreateView(parent, name, context, attrs); } else { view = null; } } else { view = null; } if (isBlackListedForApply(name)) { return view; } // Mimic code of LayoutInflater using reflection tricks (this would normally be run when this factory returns null). // We need to intercept the default behavior rather than allowing the LayoutInflater to handle it after this method returns. if (view == null) { try { Context viewContext = layoutInflater.getContext(); // Apply a theme wrapper, if requested. if (ATTRS_THEME != null) { final TypedArray ta = viewContext.obtainStyledAttributes(attrs, ATTRS_THEME); final int themeResId = ta.getResourceId(0, 0); if (themeResId != 0) { //noinspection RestrictedApi viewContext = new ContextThemeWrapper(viewContext, themeResId); } ta.recycle(); } Object[] constructorArgs; try { constructorArgs = (Object[]) constructorArgsField.get(layoutInflater); } catch (IllegalAccessException e) { throw new IllegalStateException( "Failed to retrieve the mConstructorArgsField field.", e); } final Object lastContext = constructorArgs[0]; constructorArgs[0] = viewContext; try { if (-1 == name.indexOf('.')) { view = (View) onCreateViewMethod.invoke(layoutInflater, parent, name, attrs); } else { view = (View) createViewMethod.invoke(layoutInflater, name, null, attrs); } } catch (Exception e) { log("Failed to inflate %s: %s", name, e.getMessage()); e.printStackTrace(); } finally { constructorArgs[0] = lastContext; } } catch (Throwable t) { throw new RuntimeException( String.format("An error occurred while inflating View %s: %s", name, t.getMessage()), t); } } } if (view != null) { if (view instanceof CardView) { viewBackgroundRes = resolveResId(context, attrs, R.attr.cardBackgroundColor); } if (viewBackgroundRes != 0) { Observable<Integer> fallback = null; if (view instanceof CardView) { fallback = Aesthetic.get().colorCardViewBackground(); } Observable<Integer> obs; obs = ViewUtil.getObservableForResId(view.getContext(), viewBackgroundRes, fallback); if (obs != null) { Aesthetic.get().addBackgroundSubscriber(view, obs); } } String idName = ""; try { idName = context.getResources().getResourceName(view.getId()) + " "; } catch (Throwable ignored) { } log("Inflated -> %s%s", idName, view.getClass().getName()); } return view; } }