From 58849efff2c5bf4488cf597ee3915aab2f682f17 Mon Sep 17 00:00:00 2001 From: James Barr Date: Thu, 19 Jan 2017 23:00:35 -0800 Subject: [PATCH 1/2] Initial checkin of the ViewPump lib --- CHANGELOG.md | 4 + gradle.properties | 16 + settings.gradle | 1 + viewpump/build.gradle | 31 ++ viewpump/consumer-proguard-rules.pro | 2 + viewpump/gradle.properties | 9 + viewpump/src/main/AndroidManifest.xml | 4 + .../FallbackViewCreationInterceptor.java | 20 + .../viewpump/FallbackViewCreator.java | 12 + .../inflationx/viewpump/InflateRequest.java | 126 +++++ .../inflationx/viewpump/InflateResult.java | 112 +++++ .../inflationx/viewpump/Interceptor.java | 17 + .../inflationx/viewpump/InterceptorChain.java | 46 ++ .../inflationx/viewpump/ReflectionUtils.java | 57 +++ .../github/inflationx/viewpump/ViewPump.java | 150 ++++++ .../viewpump/ViewPumpActivityFactory.java | 32 ++ .../viewpump/ViewPumpContextWrapper.java | 94 ++++ .../viewpump/ViewPumpLayoutInflater.java | 436 ++++++++++++++++++ viewpump/src/main/res/values/ids.xml | 4 + .../viewpump/test/ViewPumpTest.java | 217 +++++++++ .../viewpump/util/AnotherTestView.java | 19 + ...TestViewNewingPreInflationInterceptor.java | 23 + .../NameChangingPreInflationInterceptor.java | 16 + .../util/TestFallbackViewCreator.java | 23 + .../inflationx/viewpump/util/TestView.java | 19 + 25 files changed, 1490 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 gradle.properties create mode 100644 settings.gradle create mode 100644 viewpump/build.gradle create mode 100644 viewpump/consumer-proguard-rules.pro create mode 100644 viewpump/gradle.properties create mode 100644 viewpump/src/main/AndroidManifest.xml create mode 100644 viewpump/src/main/java/io/github/inflationx/viewpump/FallbackViewCreationInterceptor.java create mode 100644 viewpump/src/main/java/io/github/inflationx/viewpump/FallbackViewCreator.java create mode 100644 viewpump/src/main/java/io/github/inflationx/viewpump/InflateRequest.java create mode 100644 viewpump/src/main/java/io/github/inflationx/viewpump/InflateResult.java create mode 100644 viewpump/src/main/java/io/github/inflationx/viewpump/Interceptor.java create mode 100644 viewpump/src/main/java/io/github/inflationx/viewpump/InterceptorChain.java create mode 100644 viewpump/src/main/java/io/github/inflationx/viewpump/ReflectionUtils.java create mode 100644 viewpump/src/main/java/io/github/inflationx/viewpump/ViewPump.java create mode 100644 viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpActivityFactory.java create mode 100644 viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpContextWrapper.java create mode 100644 viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpLayoutInflater.java create mode 100644 viewpump/src/main/res/values/ids.xml create mode 100644 viewpump/src/test/java/io/github/inflationx/viewpump/test/ViewPumpTest.java create mode 100644 viewpump/src/test/java/io/github/inflationx/viewpump/util/AnotherTestView.java create mode 100644 viewpump/src/test/java/io/github/inflationx/viewpump/util/AnotherTestViewNewingPreInflationInterceptor.java create mode 100644 viewpump/src/test/java/io/github/inflationx/viewpump/util/NameChangingPreInflationInterceptor.java create mode 100644 viewpump/src/test/java/io/github/inflationx/viewpump/util/TestFallbackViewCreator.java create mode 100644 viewpump/src/test/java/io/github/inflationx/viewpump/util/TestView.java diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b8026a2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +#Changelog + +#0.1.0 +- Initial Release diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..fa76fa4 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,16 @@ +GROUP=io.github.inflationx +VERSION_NAME=0.1.0 +VERSION_CODE=1 + +POM_PACKAGING=aar +POM_URL=https://github.com/InflationX/ViewPump +POM_DESCRIPTION=View inflation with an pre/post-inflation interceptors +POM_SCM_URL=https://github.com/InflationX/ViewPump +POM_SCM_CONNECTION=scm:git@github.com:InflationX/ViewPump.git +POM_SCM_DEV_CONNECTION=scm:git@github.com:InflationX/ViewPump.git +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENCE_DIST=repo +POM_DEVELOPER_ID=InflationX +POM_DEVELOPER_NAME=InflationX +POM_DEVELOPER_EMAIL= diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..9fbe60f --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':viewpump' diff --git a/viewpump/build.gradle b/viewpump/build.gradle new file mode 100644 index 0000000..aed8e8e --- /dev/null +++ b/viewpump/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 24 + buildToolsVersion "24.0.0" + + defaultConfig { + minSdkVersion 7 + targetSdkVersion 24 + versionCode project.ext.versionCodeInt + versionName version + consumerProguardFiles 'consumer-proguard-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'consumer-proguard-rules.pro' + } + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:24.0.0' + + testCompile 'org.assertj:assertj-core:1.7.1' + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:2.0.42-beta' +} + +apply from: rootProject.file('gradle/deploy.gradle') diff --git a/viewpump/consumer-proguard-rules.pro b/viewpump/consumer-proguard-rules.pro new file mode 100644 index 0000000..f5ca621 --- /dev/null +++ b/viewpump/consumer-proguard-rules.pro @@ -0,0 +1,2 @@ +-keep class io.github.inflationx.viewpump.* { *; } +-keep class io.github.inflationx.viewpump.*$* { *; } diff --git a/viewpump/gradle.properties b/viewpump/gradle.properties new file mode 100644 index 0000000..078e07e --- /dev/null +++ b/viewpump/gradle.properties @@ -0,0 +1,9 @@ +# SubProject Library Gradle Properties +# See parent properties for global properties + +GROUP=io.github.inflationx +POM_NAME=ViewPump +POM_ARTIFACT_ID=viewpump +POM_PACKAGING=aar + +VERSION_NAME=0.1.0 \ No newline at end of file diff --git a/viewpump/src/main/AndroidManifest.xml b/viewpump/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3325321 --- /dev/null +++ b/viewpump/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/FallbackViewCreationInterceptor.java b/viewpump/src/main/java/io/github/inflationx/viewpump/FallbackViewCreationInterceptor.java new file mode 100644 index 0000000..720615b --- /dev/null +++ b/viewpump/src/main/java/io/github/inflationx/viewpump/FallbackViewCreationInterceptor.java @@ -0,0 +1,20 @@ +package io.github.inflationx.viewpump; + +import android.view.View; + +class FallbackViewCreationInterceptor implements Interceptor { + + @Override + public InflateResult intercept(Chain chain) { + InflateRequest request = chain.request(); + FallbackViewCreator viewCreator = request.fallbackViewCreator(); + View fallbackView = viewCreator.onCreateView(request.parent(), request.name(), request.context(), request.attrs()); + + return InflateResult.builder() + .view(fallbackView) + .name(fallbackView != null ? fallbackView.getClass().getName() : request.name()) + .context(request.context()) + .attrs(request.attrs()) + .build(); + } +} diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/FallbackViewCreator.java b/viewpump/src/main/java/io/github/inflationx/viewpump/FallbackViewCreator.java new file mode 100644 index 0000000..6a03991 --- /dev/null +++ b/viewpump/src/main/java/io/github/inflationx/viewpump/FallbackViewCreator.java @@ -0,0 +1,12 @@ +package io.github.inflationx.viewpump; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; + +public interface FallbackViewCreator { + @Nullable + View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @Nullable AttributeSet attrs); +} diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/InflateRequest.java b/viewpump/src/main/java/io/github/inflationx/viewpump/InflateRequest.java new file mode 100644 index 0000000..2a765c0 --- /dev/null +++ b/viewpump/src/main/java/io/github/inflationx/viewpump/InflateRequest.java @@ -0,0 +1,126 @@ +package io.github.inflationx.viewpump; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; + +public class InflateRequest { + private final String name; + private final Context context; + private final AttributeSet attrs; + private final View parent; + private final FallbackViewCreator fallbackViewCreator; + + private InflateRequest(Builder builder) { + name = builder.name; + context = builder.context; + attrs = builder.attrs; + parent = builder.parent; + fallbackViewCreator = builder.fallbackViewCreator; + } + + @NonNull + public String name() { + return name; + } + + @NonNull + public Context context() { + return context; + } + + @Nullable + public AttributeSet attrs() { + return attrs; + } + + @Nullable + public View parent() { + return parent; + } + + @NonNull + public FallbackViewCreator fallbackViewCreator() { + return fallbackViewCreator; + } + + @NonNull + public static Builder builder() { + return new Builder(); + } + + @NonNull + public Builder toBuilder() { + return new Builder(this); + } + + @NonNull + @Override + public String toString() { + return "InflateRequest{" + + "name='" + name + '\'' + + ", context=" + context + + ", attrs=" + attrs + + ", parent=" + parent + + ", fallbackViewCreator=" + fallbackViewCreator + + '}'; + } + + public static final class Builder { + private String name; + private Context context; + private AttributeSet attrs; + private View parent; + private FallbackViewCreator fallbackViewCreator; + + private Builder() { } + + private Builder(InflateRequest request) { + this.name = request.name; + this.context = request.context; + this.attrs = request.attrs; + this.parent = request.parent; + this.fallbackViewCreator = request.fallbackViewCreator; + } + + public Builder name(@NonNull String name) { + this.name = name; + return this; + } + + public Builder context(@NonNull Context context) { + this.context = context; + return this; + } + + public Builder attrs(@Nullable AttributeSet attrs) { + this.attrs = attrs; + return this; + } + + public Builder parent(@Nullable View parent) { + this.parent = parent; + return this; + } + + public Builder fallbackViewCreator(@NonNull FallbackViewCreator fallbackViewCreator) { + this.fallbackViewCreator = fallbackViewCreator; + return this; + } + + public InflateRequest build() { + if (name == null) { + throw new IllegalStateException("name == null"); + } + if (context == null) { + throw new IllegalStateException("context == null"); + } + if (fallbackViewCreator == null) { + throw new IllegalStateException("fallbackViewCreator == null"); + } + return new InflateRequest(this); + } + } +} diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/InflateResult.java b/viewpump/src/main/java/io/github/inflationx/viewpump/InflateResult.java new file mode 100644 index 0000000..02cd5ce --- /dev/null +++ b/viewpump/src/main/java/io/github/inflationx/viewpump/InflateResult.java @@ -0,0 +1,112 @@ +package io.github.inflationx.viewpump; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; + +public class InflateResult { + private final View view; + private final String name; + private final Context context; + private final AttributeSet attrs; + + private InflateResult(Builder builder) { + view = builder.view; + name = builder.name; + context = builder.context; + attrs = builder.attrs; + } + + @Nullable + public View view() { + return view; + } + + @NonNull + public String name() { + return name; + } + + @NonNull + public Context context() { + return context; + } + + @Nullable + public AttributeSet attrs() { + return attrs; + } + + @NonNull + public static Builder builder() { + return new Builder(); + } + + @NonNull + public Builder toBuilder() { + return new Builder(this); + } + + @NonNull + @Override + public String toString() { + return "InflateResult{" + + "view=" + view + + ", name=" + name + + ", context=" + context + + ", attrs=" + attrs + + '}'; + } + + public static final class Builder { + private View view; + private String name; + private Context context; + private AttributeSet attrs; + + private Builder() { } + + private Builder(InflateResult result) { + this.view = result.view; + this.name = result.name; + this.context = result.context; + this.attrs = result.attrs; + } + + public Builder view(@Nullable View view) { + this.view = view; + return this; + } + + public Builder name(@NonNull String name) { + this.name = name; + return this; + } + + public Builder context(@NonNull Context context) { + this.context = context; + return this; + } + + public Builder attrs(@Nullable AttributeSet attrs) { + this.attrs = attrs; + return this; + } + + public InflateResult build() { + if (name == null) { + throw new IllegalStateException("name == null"); + } + if (context == null) { + throw new IllegalStateException("context == null"); + } + if (view != null && !name.equals(view.getClass().getName())) { + throw new IllegalStateException("name (" + name + ") " + + "must be the view's fully qualified name (" + view.getClass().getName() + ")"); + } + return new InflateResult(this); + } + } +} diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/Interceptor.java b/viewpump/src/main/java/io/github/inflationx/viewpump/Interceptor.java new file mode 100644 index 0000000..8ec59a1 --- /dev/null +++ b/viewpump/src/main/java/io/github/inflationx/viewpump/Interceptor.java @@ -0,0 +1,17 @@ +package io.github.inflationx.viewpump; + +/** + * Observes, modifies, and potentially short-circuits inflation requests going out and the + * corresponding views that are inflated or returned. Typically interceptors change the name + * of the view to be inflated, return a programmatically instantiated view, or perform actions + * on a view after it is inflated based on its Context or AttributeSet. + */ +public interface Interceptor { + InflateResult intercept(Chain chain); + + interface Chain { + InflateRequest request(); + + InflateResult proceed(InflateRequest request); + } +} diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/InterceptorChain.java b/viewpump/src/main/java/io/github/inflationx/viewpump/InterceptorChain.java new file mode 100644 index 0000000..68b9172 --- /dev/null +++ b/viewpump/src/main/java/io/github/inflationx/viewpump/InterceptorChain.java @@ -0,0 +1,46 @@ +package io.github.inflationx.viewpump; + +import android.support.annotation.NonNull; + +import java.util.List; + +/** + * A concrete interceptor chain that carries the entire interceptor chain. + */ +class InterceptorChain implements Interceptor.Chain { + private final List interceptors; + private final int index; + private final InflateRequest request; + + InterceptorChain(@NonNull List interceptors, int index, @NonNull InflateRequest request) { + this.interceptors = interceptors; + this.index = index; + this.request = request; + } + + @NonNull + @Override + public InflateRequest request() { + return request; + } + + @NonNull + @Override + public InflateResult proceed(@NonNull InflateRequest request) { + if (index >= interceptors.size()) { + throw new AssertionError("no interceptors added to the chain"); + } + + // Call the next interceptor in the chain. + InterceptorChain next = new InterceptorChain(interceptors, index + 1, request); + Interceptor interceptor = interceptors.get(index); + InflateResult result = interceptor.intercept(next); + + // Confirm that the intercepted response isn't null. + if (result == null) { + throw new NullPointerException("interceptor " + interceptor + " returned null"); + } + + return result; + } +} diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/ReflectionUtils.java b/viewpump/src/main/java/io/github/inflationx/viewpump/ReflectionUtils.java new file mode 100644 index 0000000..f826cd2 --- /dev/null +++ b/viewpump/src/main/java/io/github/inflationx/viewpump/ReflectionUtils.java @@ -0,0 +1,57 @@ +package io.github.inflationx.viewpump; + +import android.util.Log; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class ReflectionUtils { + + private static final String TAG = ReflectionUtils.class.getSimpleName(); + + static Field getField(Class clazz, String fieldName) { + try { + final Field f = clazz.getDeclaredField(fieldName); + f.setAccessible(true); + return f; + } catch (NoSuchFieldException ignored) { + } + return null; + } + + static Object getValue(Field field, Object obj) { + try { + return field.get(obj); + } catch (IllegalAccessException ignored) { + } + return null; + } + + static void setValue(Field field, Object obj, Object value) { + try { + field.set(obj, value); + } catch (IllegalAccessException ignored) { + } + } + + public static Method getMethod(Class clazz, String methodName) { + final Method[] methods = clazz.getMethods(); + for (Method method : methods) { + if (method.getName().equals(methodName)) { + method.setAccessible(true); + return method; + } + } + return null; + } + + public static void invokeMethod(Object object, Method method, Object... args) { + try { + if (method == null) return; + method.invoke(object, args); + } catch (IllegalAccessException | InvocationTargetException e) { + Log.d(TAG, "Can't invoke method using reflection", e); + } + } +} diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPump.java b/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPump.java new file mode 100644 index 0000000..0b4b80b --- /dev/null +++ b/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPump.java @@ -0,0 +1,150 @@ +package io.github.inflationx.viewpump; + +import android.os.Build; +import android.support.annotation.MainThread; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +public final class ViewPump { + + private static ViewPump INSTANCE; + + /** List of interceptors. */ + private final List interceptors; + + /** List that gets cleared and reused as it holds interceptors with the fallback added. */ + private final List mInterceptorsWithFallback; + + /** Use Reflection to inject the private factory. */ + private final boolean mReflection; + + /** Use Reflection to intercept CustomView inflation with the correct Context. */ + private final boolean mCustomViewCreation; + + /** The single instance of the FallbackViewCreationInterceptor. */ + private final FallbackViewCreationInterceptor mFallbackViewCreationInterceptor; + + private ViewPump(Builder builder) { + mInterceptorsWithFallback = new ArrayList<>(); + mFallbackViewCreationInterceptor = new FallbackViewCreationInterceptor(); + + interceptors = builder.interceptors; + mReflection = builder.reflection; + mCustomViewCreation = builder.customViewCreation; + } + + public static void init(ViewPump viewPump) { + INSTANCE = viewPump; + } + + @MainThread + public static ViewPump get() { + if (INSTANCE == null) { + INSTANCE = builder().build(); + } + return INSTANCE; + } + + public InflateResult inflate(InflateRequest originalRequest) { + mInterceptorsWithFallback.clear(); + mInterceptorsWithFallback.addAll(interceptors()); + mInterceptorsWithFallback.add(mFallbackViewCreationInterceptor); + Interceptor.Chain chain = new InterceptorChain(mInterceptorsWithFallback, 0, originalRequest); + return chain.proceed(originalRequest); + } + + public List interceptors() { + return interceptors; + } + + public boolean isReflection() { + return mReflection; + } + + public boolean isCustomViewCreation() { + return mCustomViewCreation; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + + /** List of interceptors. */ + private final List interceptors = new ArrayList<>(); + + /** Use Reflection to inject the private factory. Doesn't exist pre HC. so defaults to false. */ + private boolean reflection = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; + + /** Use Reflection to intercept CustomView inflation with the correct Context. */ + private boolean customViewCreation = true; + + private Builder() { } + + public Builder addInterceptor(Interceptor interceptor) { + interceptors.add(interceptor); + return this; + } + + /** + *

Turn of the use of Reflection to inject the private factory. + * This has operational consequences! Please read and understand before disabling. + * This is already disabled on pre Honeycomb devices. (API 11)

+ * + *

If you disable this you will need to override your {@link android.app.Activity#onCreateView(View, String, android.content.Context, android.util.AttributeSet)} + * as this is set as the {@link android.view.LayoutInflater} private factory.

+ *
+ * Use the following code in the Activity if you disable FactoryInjection: + *

+         * {@literal @}Override
+         * {@literal @}TargetApi(Build.VERSION_CODES.HONEYCOMB)
+         * public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
+         *   return ViewPumpContextWrapper.onActivityCreateView(this, parent, super.onCreateView(parent, name, context, attrs), name, context, attrs);
+         * }
+         * 
+ * + * @param enabled True if private factory inject is allowed; otherwise, false. + */ + public Builder setPrivateFactoryInjectionEnabled(boolean enabled) { + this.reflection = enabled; + return this; + } + + /** + * Due to the poor inflation order where custom views are created and never returned inside an + * {@code onCreateView(...)} method. We have to create CustomView's at the latest point in the + * overrideable injection flow. + * + * On HoneyComb+ this is inside the {@link android.app.Activity#onCreateView(View, String, android.content.Context, android.util.AttributeSet)} + * Pre HoneyComb this is in the {@link android.view.LayoutInflater.Factory#onCreateView(String, android.util.AttributeSet)} + * + * We wrap base implementations, so if you LayoutInflater/Factory/Activity creates the + * custom view before we get to this point, your view is used. (Such is the case with the + * TintEditText etc) + * + * The problem is, the native methods pass there parents context to the constructor in a really + * specific place. We have to mimic this in {@link ViewPumpLayoutInflater#createCustomViewInternal(View, View, String, android.content.Context, android.util.AttributeSet)} + * To mimic this we have to use reflection as the Class constructor args are hidden to us. + * + * We have discussed other means of doing this but this is the only semi-clean way of doing it. + * (Without having to do proxy classes etc). + * + * Calling this will of course speed up inflation by turning off reflection, but not by much, + * But if you want ViewPump to inject the correct typeface then you will need to make sure your CustomView's + * are created before reaching the LayoutInflater onViewCreated. + * + * @param enabled True if custom view inflated is allowed; otherwise, false. + */ + public Builder setCustomViewInflationEnabled(boolean enabled) { + this.customViewCreation = enabled; + return this; + } + + public ViewPump build() { + return new ViewPump(this); + } + } +} diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpActivityFactory.java b/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpActivityFactory.java new file mode 100644 index 0000000..e982e13 --- /dev/null +++ b/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpActivityFactory.java @@ -0,0 +1,32 @@ +package io.github.inflationx.viewpump; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; + +interface ViewPumpActivityFactory { + + /** + * Used to Wrap the Activity onCreateView method. + * + * You implement this method like so in you base activity. + *
+     * {@code
+     * public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
+     *   return ViewPumpContextWrapper.get(getBaseContext()).onActivityCreateView(super.onCreateView(parent, name,context, attrs), attrs);
+     * }
+     * }
+     * 
+ * + * @param parent parent view, can be null. + * @param view result of {@code super.onCreateView(parent, name, context, attrs)}, this might be null, which is fine. + * @param name Name of View we are trying to inflate + * @param context current context (normally the Activity's) + * @param attrs see {@link android.view.LayoutInflater.Factory2#onCreateView(View, String, Context, AttributeSet)} @return the result from the activities {@code onCreateView()} + * @return The view passed in, or null if nothing was passed in. + * @see android.view.LayoutInflater.Factory2 + */ + @Nullable + View onActivityCreateView(View parent, View view, String name, Context context, AttributeSet attrs); +} diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpContextWrapper.java b/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpContextWrapper.java new file mode 100644 index 0000000..b7d9983 --- /dev/null +++ b/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpContextWrapper.java @@ -0,0 +1,94 @@ +package io.github.inflationx.viewpump; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; + +public final class ViewPumpContextWrapper extends ContextWrapper { + + private ViewPumpLayoutInflater mInflater; + + /** + * Uses the default configuration from {@link ViewPump} + * + * Remember if you are defining default in the {@link ViewPump} make sure this + * is initialised before the activity is created. + * + * @param base ContextBase to Wrap. + * @return ContextWrapper to pass back to the activity. + */ + public static ContextWrapper wrap(@NonNull Context base) { + return new ViewPumpContextWrapper(base); + } + + /** + * You only need to call this IF you disable + * {@link ViewPump.Builder#setPrivateFactoryInjectionEnabled(boolean)} + * This will need to be called from the + * {@link Activity#onCreateView(View, String, Context, AttributeSet)} + * method to enable view font injection if the view is created inside the activity onCreateView. + * + * You would implement this method like so in you base activity. + *
+     * {@code
+     * public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
+     *   return ViewPumpContextWrapper.onActivityCreateView(this, parent, super.onCreateView(parent, name, context, attrs), name, context, attrs);
+     * }
+     * }
+     * 
+ * + * @param activity The activity the original that the ContextWrapper was attached too. + * @param parent Parent view from onCreateView + * @param view The View Created inside onCreateView or from super.onCreateView + * @param name The View name from onCreateView + * @param context The context from onCreateView + * @param attr The AttributeSet from onCreateView + * @return The same view passed in, or null if null passed in. + */ + @Nullable + public static View onActivityCreateView(Activity activity, View parent, View view, String name, Context context, AttributeSet attr) { + return get(activity).onActivityCreateView(parent, view, name, context, attr); + } + + /** + * Get the ViewPump Activity Fragment Instance to allow callbacks for when views are created. + * + * @param activity The activity the original that the ContextWrapper was attached too. + * @return Interface allowing you to call onActivityViewCreated + */ + static ViewPumpActivityFactory get(@NonNull Activity activity) { + if (!(activity.getLayoutInflater() instanceof ViewPumpLayoutInflater)) { + throw new RuntimeException("This activity does not wrap the Base Context! See ViewPumpContextWrapper.wrap(Context)"); + } + return (ViewPumpActivityFactory) activity.getLayoutInflater(); + } + + /** + * Uses the default configuration from {@link ViewPump} + * + * Remember if you are defining default in the + * {@link ViewPump} make sure this is initialised before + * the activity is created. + * + * @param base ContextBase to Wrap + */ + private ViewPumpContextWrapper(Context base) { + super(base); + } + + @Override + public Object getSystemService(String name) { + if (LAYOUT_INFLATER_SERVICE.equals(name)) { + if (mInflater == null) { + mInflater = new ViewPumpLayoutInflater(LayoutInflater.from(getBaseContext()), this, false); + } + return mInflater; + } + return super.getSystemService(name); + } +} diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpLayoutInflater.java b/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpLayoutInflater.java new file mode 100644 index 0000000..9e7e6d3 --- /dev/null +++ b/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpLayoutInflater.java @@ -0,0 +1,436 @@ +package io.github.inflationx.viewpump; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.xmlpull.v1.XmlPullParser; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +class ViewPumpLayoutInflater extends LayoutInflater implements ViewPumpActivityFactory { + + private static final String[] sClassPrefixList = { + "android.widget.", + "android.webkit." + }; + + private FallbackViewCreator nameAndAttrsViewCreator; + private FallbackViewCreator parentAndNameAndAttrsViewCreator; + + // Reflection Hax + private boolean mSetPrivateFactory = false; + private Field mConstructorArgs = null; + + protected ViewPumpLayoutInflater(Context context) { + super(context); + nameAndAttrsViewCreator = new NameAndAttrsViewCreator(this); + parentAndNameAndAttrsViewCreator = new ParentAndNameAndAttrsViewCreator(this); + setUpLayoutFactories(false); + } + + protected ViewPumpLayoutInflater(LayoutInflater original, Context newContext, final boolean cloned) { + super(original, newContext); + nameAndAttrsViewCreator = new NameAndAttrsViewCreator(this); + parentAndNameAndAttrsViewCreator = new ParentAndNameAndAttrsViewCreator(this); + setUpLayoutFactories(cloned); + } + + @Override + public LayoutInflater cloneInContext(Context newContext) { + return new ViewPumpLayoutInflater(this, newContext, true); + } + + // === + // Wrapping goodies + // === + + + @Override + public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { + setPrivateFactoryInternal(); + return super.inflate(parser, root, attachToRoot); + } + + /** + * We don't want to unnecessary create/set our factories if there are none there. We try to be + * as lazy as possible. + */ + private void setUpLayoutFactories(boolean cloned) { + if (cloned) return; + // If we are HC+ we get and set Factory2 otherwise we just wrap Factory1 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + if (getFactory2() != null && !(getFactory2() instanceof WrapperFactory2)) { + // Sets both Factory/Factory2 + setFactory2(getFactory2()); + } + } + // We can do this as setFactory2 is used for both methods. + if (getFactory() != null && !(getFactory() instanceof WrapperFactory)) { + setFactory(getFactory()); + } + } + + @Override + public void setFactory(Factory factory) { + // Only set our factory and wrap calls to the Factory trying to be set! + if (!(factory instanceof WrapperFactory)) { + super.setFactory(new WrapperFactory(factory, this)); + } else { + super.setFactory(factory); + } + } + + @Override + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void setFactory2(Factory2 factory2) { + // Only set our factory and wrap calls to the Factory2 trying to be set! + if (!(factory2 instanceof WrapperFactory2)) { +// LayoutInflaterCompat.setFactory(this, new WrapperFactory2(factory2, mViewPumpFactory)); + super.setFactory2(new WrapperFactory2(factory2)); + } else { + super.setFactory2(factory2); + } + } + + private void setPrivateFactoryInternal() { + // Already tried to set the factory. + if (mSetPrivateFactory) return; + // Reflection (Or Old Device) skip. + if (!ViewPump.get().isReflection()) return; + // Skip if not attached to an activity. + if (!(getContext() instanceof Factory2)) { + mSetPrivateFactory = true; + return; + } + + final Method setPrivateFactoryMethod = ReflectionUtils.getMethod(LayoutInflater.class, "setPrivateFactory"); + + if (setPrivateFactoryMethod != null) { + ReflectionUtils.invokeMethod(this, + setPrivateFactoryMethod, + new PrivateWrapperFactory2((Factory2) getContext(), this)); + } + mSetPrivateFactory = true; + } + + // === + // LayoutInflater ViewCreators + // Works in order of inflation + // === + + /** + * The Activity onCreateView (PrivateFactory) is the third port of call for LayoutInflation. + * We opted to manual injection over aggressive reflection, this should be less fragile. + */ + @Override + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public View onActivityCreateView(View parent, View view, String name, Context context, AttributeSet attrs) { + return ViewPump.get().inflate(InflateRequest.builder() + .name(name) + .context(context) + .attrs(attrs) + .parent(parent) + .fallbackViewCreator(new ActivityViewCreator(this, view)) + .build()).view(); + } + + /** + * The LayoutInflater onCreateView is the fourth port of call for LayoutInflation. + * BUT only for none CustomViews. + */ + @Override + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + protected View onCreateView(View parent, String name, AttributeSet attrs) throws ClassNotFoundException { + return ViewPump.get().inflate(InflateRequest.builder() + .name(name) + .context(getContext()) + .attrs(attrs) + .parent(parent) + .fallbackViewCreator(parentAndNameAndAttrsViewCreator) + .build()).view(); + } + + /** + * The LayoutInflater onCreateView is the fourth port of call for LayoutInflation. + * BUT only for none CustomViews. + * Basically if this method doesn't inflate the View nothing probably will. + */ + @Override + protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { + return ViewPump.get().inflate(InflateRequest.builder() + .name(name) + .context(getContext()) // TODO: is this OK? before was fallbackView.getContext() + .attrs(attrs) + .fallbackViewCreator(nameAndAttrsViewCreator) + .build()).view(); + } + + /** + * Nasty method to inflate custom layouts that haven't been handled else where. If this fails it + * will fall back through to the PhoneLayoutInflater method of inflating custom views where + * ViewPump will NOT have a hook into. + * + * @param parent parent view + * @param view view if it has been inflated by this point, if this is not null this method + * just returns this value. + * @param name name of the thing to inflate. + * @param viewContext Context to inflate by if parent is null + * @param attrs Attr for this view which we can steal fontPath from too. + * @return view or the View we inflate in here. + */ + private View createCustomViewInternal(View parent, View view, String name, Context viewContext, AttributeSet attrs) { + // I by no means advise anyone to do this normally, but Google have locked down access to + // the createView() method, so we never get a callback with attributes at the end of the + // createViewFromTag chain (which would solve all this unnecessary rubbish). + // We at the very least try to optimise this as much as possible. + // We only call for customViews (As they are the ones that never go through onCreateView(...)). + // We also maintain the Field reference and make it accessible which will make a pretty + // significant difference to performance on Android 4.0+. + + // If CustomViewCreation is off skip this. + if (!ViewPump.get().isCustomViewCreation()) return view; + if (view == null && name.indexOf('.') > -1) { + if (mConstructorArgs == null) + mConstructorArgs = ReflectionUtils.getField(LayoutInflater.class, "mConstructorArgs"); + + final Object[] mConstructorArgsArr = (Object[]) ReflectionUtils.getValue(mConstructorArgs, this); + final Object lastContext = mConstructorArgsArr[0]; + // The LayoutInflater actually finds out the correct context to use. We just need to set + // it on the mConstructor for the internal method. + // Set the constructor ars up for the createView, not sure why we can't pass these in. + mConstructorArgsArr[0] = viewContext; + ReflectionUtils.setValue(mConstructorArgs, this, mConstructorArgsArr); + try { + view = createView(name, null, attrs); + } catch (ClassNotFoundException ignored) { + } finally { + mConstructorArgsArr[0] = lastContext; + ReflectionUtils.setValue(mConstructorArgs, this, mConstructorArgsArr); + } + } + return view; + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private View superOnCreateView(View parent, String name, AttributeSet attrs) { + try { + return super.onCreateView(parent, name, attrs); + } catch (ClassNotFoundException e) { + return null; + } + } + + private View superOnCreateView(String name, AttributeSet attrs) { + try { + return super.onCreateView(name, attrs); + } catch (ClassNotFoundException e) { + return null; + } + } + + // === + // View creators + // === + + private static class ActivityViewCreator implements FallbackViewCreator { + private final ViewPumpLayoutInflater inflater; + private final View view; + + public ActivityViewCreator(ViewPumpLayoutInflater inflater, View view) { + this.inflater = inflater; + this.view = view; + } + + @Override + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + return inflater.createCustomViewInternal(parent, view, name, context, attrs); + } + } + + private static class ParentAndNameAndAttrsViewCreator implements FallbackViewCreator { + private final ViewPumpLayoutInflater inflater; + + public ParentAndNameAndAttrsViewCreator(ViewPumpLayoutInflater inflater) { + this.inflater = inflater; + } + + @Override + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + return inflater.superOnCreateView(parent, name, attrs); + } + } + + private static class NameAndAttrsViewCreator implements FallbackViewCreator { + private final ViewPumpLayoutInflater inflater; + + public NameAndAttrsViewCreator(ViewPumpLayoutInflater inflater) { + this.inflater = inflater; + } + + @Override + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + // This mimics the {@code PhoneLayoutInflater} in the way it tries to inflate the base + // classes, if this fails its pretty certain the app will fail at this point. + View view = null; + for (String prefix : sClassPrefixList) { + try { + view = inflater.createView(name, prefix, attrs); + } catch (ClassNotFoundException ignored) { + } + } + // In this case we want to let the base class take a crack + // at it. + if (view == null) view = inflater.superOnCreateView(name, attrs); + return view; + } + } + + // === + // Wrapper Factories for Pre/Post HC + // === + + /** + * Factory 1 is the first port of call for LayoutInflation + */ + private static class WrapperFactory implements Factory { + + private final FallbackViewCreator mViewCreator; + + public WrapperFactory(Factory factory, ViewPumpLayoutInflater inflater) { + mViewCreator = Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB + ? new PreHcWrapperFactoryViewCreator(factory, inflater) + : new WrapperFactoryViewCreator(factory); + } + + @Override + public View onCreateView(String name, Context context, AttributeSet attrs) { + return ViewPump.get().inflate(InflateRequest.builder() + .name(name) + .context(context) + .attrs(attrs) + .fallbackViewCreator(mViewCreator) + .build()).view(); + } + } + + private static class WrapperFactoryViewCreator implements FallbackViewCreator { + protected final Factory mFactory; + + public WrapperFactoryViewCreator(Factory factory) { + this.mFactory = factory; + } + + @Override + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + return mFactory.onCreateView(name, context, attrs); + } + } + + private static class PreHcWrapperFactoryViewCreator extends WrapperFactoryViewCreator implements FallbackViewCreator { + protected final ViewPumpLayoutInflater mInflater; + + public PreHcWrapperFactoryViewCreator(Factory factory, ViewPumpLayoutInflater inflater) { + super(factory); + mInflater = inflater; + } + + @Override + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + return mInflater.createCustomViewInternal( + null, mFactory.onCreateView(name, context, attrs), name, context, attrs); + } + } + + /** + * Factory 2 is the second port of call for LayoutInflation + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private static class WrapperFactory2 implements Factory2 { + protected final Factory2 mFactory2; + private final WrapperFactory2ViewCreator mViewCreator; + + public WrapperFactory2(Factory2 factory2) { + mFactory2 = factory2; + mViewCreator = new WrapperFactory2ViewCreator(factory2); + } + + @Override + public View onCreateView(String name, Context context, AttributeSet attrs) { + return onCreateView(null, name, context, attrs); + } + + @Override + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + return ViewPump.get().inflate(InflateRequest.builder() + .name(name) + .context(context) + .attrs(attrs) + .parent(parent) + .fallbackViewCreator(mViewCreator) + .build()).view(); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private static class WrapperFactory2ViewCreator implements FallbackViewCreator { + protected final Factory2 mFactory2; + + public WrapperFactory2ViewCreator(Factory2 factory2) { + this.mFactory2 = factory2; + } + + @Override + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + return mFactory2.onCreateView(parent, name, context, attrs); + } + } + + /** + * Private factory is step three for Activity Inflation, this is what is attached to the + * Activity on HC+ devices. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private static class PrivateWrapperFactory2 extends WrapperFactory2 { + + private final PrivateWrapperFactory2ViewCreator mViewCreator; + + public PrivateWrapperFactory2(Factory2 factory2, ViewPumpLayoutInflater inflater) { + super(factory2); + mViewCreator = new PrivateWrapperFactory2ViewCreator(factory2, inflater); + } + + @Override + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + return ViewPump.get().inflate(InflateRequest.builder() + .name(name) + .context(context) + .attrs(attrs) + .parent(parent) + .fallbackViewCreator(mViewCreator) + .build()).view(); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private static class PrivateWrapperFactory2ViewCreator extends WrapperFactory2ViewCreator implements FallbackViewCreator { + private final ViewPumpLayoutInflater mInflater; + + public PrivateWrapperFactory2ViewCreator(Factory2 factory2, ViewPumpLayoutInflater mInflater) { + super(factory2); + this.mInflater = mInflater; + } + + @Override + public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { + return mInflater.createCustomViewInternal(parent, + mFactory2.onCreateView(parent, name, context, attrs), name, context, attrs); + } + } + +} diff --git a/viewpump/src/main/res/values/ids.xml b/viewpump/src/main/res/values/ids.xml new file mode 100644 index 0000000..8866d8d --- /dev/null +++ b/viewpump/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/viewpump/src/test/java/io/github/inflationx/viewpump/test/ViewPumpTest.java b/viewpump/src/test/java/io/github/inflationx/viewpump/test/ViewPumpTest.java new file mode 100644 index 0000000..5a335d5 --- /dev/null +++ b/viewpump/src/test/java/io/github/inflationx/viewpump/test/ViewPumpTest.java @@ -0,0 +1,217 @@ +package io.github.inflationx.viewpump.test; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +import io.github.inflationx.viewpump.FallbackViewCreator; +import io.github.inflationx.viewpump.InflateRequest; +import io.github.inflationx.viewpump.InflateResult; +import io.github.inflationx.viewpump.Interceptor; +import io.github.inflationx.viewpump.ViewPump; +import io.github.inflationx.viewpump.util.AnotherTestView; +import io.github.inflationx.viewpump.util.TestFallbackViewCreator; +import io.github.inflationx.viewpump.util.NameChangingPreInflationInterceptor; +import io.github.inflationx.viewpump.util.TestView; +import io.github.inflationx.viewpump.util.AnotherTestViewNewingPreInflationInterceptor; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +public class ViewPumpTest { + + @Mock Context mockContext; + @Mock AttributeSet mockAttrs; + @Mock View mockParentView; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + ViewPump.init(null); + } + + @Test + public void uninitViewPump_shouldProvideDefaultInstance() { + assertThat(ViewPump.get()).isNotNull(); + } + + @Test + public void initViewPump_shouldProvideConfiguredInstance() { + ViewPump viewPump = ViewPump.builder().build(); + ViewPump.init(viewPump); + + assertThat(ViewPump.get()) + .isNotNull() + .isSameAs(viewPump); + } + + @Test + public void request_withRequiredParams_shouldReturnView() { + InflateResult result = ViewPump.get().inflate(InflateRequest.builder() + .name(TestView.NAME) + .context(mockContext) + .fallbackViewCreator(new TestFallbackViewCreator()) + .build()); + + assertThat(result).isNotNull(); + assertThat(result.name()).isEqualTo(TestView.NAME); + assertThat(result.context()).isSameAs(mockContext); + assertThat(result.view()) + .isNotNull() + .isInstanceOf(TestView.class); + } + + @Test + public void request_withAdditionalParams_shouldReturnView() { + InflateResult result = ViewPump.get().inflate(InflateRequest.builder() + .name(TestView.NAME) + .context(mockContext) + .attrs(mockAttrs) + .parent(mockParentView) + .fallbackViewCreator(new TestFallbackViewCreator()) + .build()); + + assertThat(result).isNotNull(); + assertThat(result.name()).isEqualTo(TestView.NAME); + assertThat(result.context()).isSameAs(mockContext); + assertThat(result.view()) + .isNotNull() + .isInstanceOf(TestView.class); + } + + @Test + public void request_withInflatedNameChangeInterceptor_shouldReturnViewWithNewName() { + ViewPump.init(ViewPump.builder() + .addInterceptor(new NameChangingPreInflationInterceptor()) + .build()); + + InflateResult result = ViewPump.get().inflate(InflateRequest.builder() + .name(TestView.NAME) + .context(mockContext) + .fallbackViewCreator(new TestFallbackViewCreator()) + .build()); + + assertThat(result).isNotNull(); + assertThat(result.name()).isEqualTo(AnotherTestView.NAME); + assertThat(result.view()) + .isNotNull() + .isInstanceOf(AnotherTestView.class); + } + + @Test + public void request_withViewNewingInterceptor_shouldReturnViewWithoutFallingBack() { + ViewPump.init(ViewPump.builder() + .addInterceptor(new AnotherTestViewNewingPreInflationInterceptor()) + .build()); + + FallbackViewCreator mockFallbackViewCreator = mock(FallbackViewCreator.class); + + InflateResult result = ViewPump.get().inflate(InflateRequest.builder() + .name(AnotherTestView.NAME) + .context(mockContext) + .fallbackViewCreator(mockFallbackViewCreator) + .build()); + + verifyZeroInteractions(mockFallbackViewCreator); + + assertThat(result).isNotNull(); + assertThat(result.name()).isEqualTo(AnotherTestView.NAME); + assertThat(result.view()) + .isNotNull() + .isInstanceOf(AnotherTestView.class); + } + + @Test + public void request_withViewNewingInterceptor_shouldShortcircuitDownstreamInterceptorsAndFallback() { + Interceptor starvedInterceptor = mock(Interceptor.class); + + ViewPump.init(ViewPump.builder() + .addInterceptor(new AnotherTestViewNewingPreInflationInterceptor()) + .addInterceptor(starvedInterceptor) + .build()); + + FallbackViewCreator mockFallbackViewCreator = mock(FallbackViewCreator.class); + + InflateResult result = ViewPump.get().inflate(InflateRequest.builder() + .name(AnotherTestView.NAME) + .context(mockContext) + .fallbackViewCreator(mockFallbackViewCreator) + .build()); + + verifyZeroInteractions(starvedInterceptor); + verifyZeroInteractions(mockFallbackViewCreator); + + assertThat(result).isNotNull(); + assertThat(result.name()).isEqualTo(AnotherTestView.NAME); + assertThat(result.view()) + .isNotNull() + .isInstanceOf(AnotherTestView.class); + } + + @Test + public void request_withNameChangingAndViewNewingInterceptorInOrder_shouldReturnViewWithNewNameWithoutFallback() { + ViewPump.init(ViewPump.builder() + .addInterceptor(new NameChangingPreInflationInterceptor()) + .addInterceptor(new AnotherTestViewNewingPreInflationInterceptor()) + .build()); + + FallbackViewCreator mockFallbackViewCreator = mock(FallbackViewCreator.class); + + InflateResult result = ViewPump.get().inflate(InflateRequest.builder() + .name(TestView.NAME) + .context(mockContext) + .fallbackViewCreator(mockFallbackViewCreator) + .build()); + + verifyZeroInteractions(mockFallbackViewCreator); + + assertThat(result).isNotNull(); + assertThat(result.name()).isEqualTo(AnotherTestView.NAME); + assertThat(result.view()) + .isNotNull() + .isInstanceOf(AnotherTestView.class); + } + + @Test + public void request_withNameChangingAndViewNewingInterceptorWrongOrder_shouldReturnViewWithNewNameWithFallback() { + ViewPump.init(ViewPump.builder() + .addInterceptor(new AnotherTestViewNewingPreInflationInterceptor()) + .addInterceptor(new NameChangingPreInflationInterceptor()) + .build()); + + View fallbackView = new AnotherTestView(mockContext); + FallbackViewCreator mockFallbackViewCreator = mock(FallbackViewCreator.class); + when(mockFallbackViewCreator.onCreateView( + any(View.class), + eq(AnotherTestView.NAME), + any(Context.class), + any(AttributeSet.class))) + .thenReturn(fallbackView); + + InflateResult result = ViewPump.get().inflate(InflateRequest.builder() + .name(TestView.NAME) + .context(mockContext) + .fallbackViewCreator(mockFallbackViewCreator) + .build()); + + verify(mockFallbackViewCreator) + .onCreateView(any(View.class), eq(AnotherTestView.NAME), eq(mockContext), any(AttributeSet.class)); + + assertThat(result).isNotNull(); + assertThat(result.name()).isEqualTo(AnotherTestView.NAME); + assertThat(result.view()) + .isNotNull() + .isInstanceOf(AnotherTestView.class) + .isSameAs(fallbackView); + } +} diff --git a/viewpump/src/test/java/io/github/inflationx/viewpump/util/AnotherTestView.java b/viewpump/src/test/java/io/github/inflationx/viewpump/util/AnotherTestView.java new file mode 100644 index 0000000..ef93924 --- /dev/null +++ b/viewpump/src/test/java/io/github/inflationx/viewpump/util/AnotherTestView.java @@ -0,0 +1,19 @@ +package io.github.inflationx.viewpump.util; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; + +public class AnotherTestView extends View { + + public static final String NAME = AnotherTestView.class.getName(); + + public AnotherTestView(Context context) { + super(context); + } + + public AnotherTestView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } +} \ No newline at end of file diff --git a/viewpump/src/test/java/io/github/inflationx/viewpump/util/AnotherTestViewNewingPreInflationInterceptor.java b/viewpump/src/test/java/io/github/inflationx/viewpump/util/AnotherTestViewNewingPreInflationInterceptor.java new file mode 100644 index 0000000..3f3eb97 --- /dev/null +++ b/viewpump/src/test/java/io/github/inflationx/viewpump/util/AnotherTestViewNewingPreInflationInterceptor.java @@ -0,0 +1,23 @@ +package io.github.inflationx.viewpump.util; + +import io.github.inflationx.viewpump.InflateRequest; +import io.github.inflationx.viewpump.InflateResult; +import io.github.inflationx.viewpump.Interceptor; + +public class AnotherTestViewNewingPreInflationInterceptor implements Interceptor { + + @Override + public InflateResult intercept(Chain chain) { + InflateRequest request = chain.request(); + if (AnotherTestView.NAME.equals(request.name())) { + return InflateResult.builder() + .view(new AnotherTestView(request.context())) + .name(AnotherTestView.NAME) + .context(request.context()) + .attrs(request.attrs()) + .build(); + } else { + return chain.proceed(request); + } + } +} diff --git a/viewpump/src/test/java/io/github/inflationx/viewpump/util/NameChangingPreInflationInterceptor.java b/viewpump/src/test/java/io/github/inflationx/viewpump/util/NameChangingPreInflationInterceptor.java new file mode 100644 index 0000000..f57ea96 --- /dev/null +++ b/viewpump/src/test/java/io/github/inflationx/viewpump/util/NameChangingPreInflationInterceptor.java @@ -0,0 +1,16 @@ +package io.github.inflationx.viewpump.util; + +import io.github.inflationx.viewpump.InflateResult; +import io.github.inflationx.viewpump.Interceptor; + +public class NameChangingPreInflationInterceptor implements Interceptor { + + @Override + public InflateResult intercept(Chain chain) { + return chain.proceed( + chain.request() + .toBuilder() + .name(AnotherTestView.NAME) + .build()); + } +} diff --git a/viewpump/src/test/java/io/github/inflationx/viewpump/util/TestFallbackViewCreator.java b/viewpump/src/test/java/io/github/inflationx/viewpump/util/TestFallbackViewCreator.java new file mode 100644 index 0000000..3ad70ee --- /dev/null +++ b/viewpump/src/test/java/io/github/inflationx/viewpump/util/TestFallbackViewCreator.java @@ -0,0 +1,23 @@ +package io.github.inflationx.viewpump.util; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; + +import io.github.inflationx.viewpump.FallbackViewCreator; + +public class TestFallbackViewCreator implements FallbackViewCreator { + + @Nullable + @Override + public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @Nullable AttributeSet attrs) { + if (TestView.NAME.equals(name)) { + return new TestView(context, attrs); + } else if (AnotherTestView.NAME.equals(name)) { + return new AnotherTestView(context, attrs); + } + return null; + } +} \ No newline at end of file diff --git a/viewpump/src/test/java/io/github/inflationx/viewpump/util/TestView.java b/viewpump/src/test/java/io/github/inflationx/viewpump/util/TestView.java new file mode 100644 index 0000000..69bd56a --- /dev/null +++ b/viewpump/src/test/java/io/github/inflationx/viewpump/util/TestView.java @@ -0,0 +1,19 @@ +package io.github.inflationx.viewpump.util; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; + +public class TestView extends View { + + public static final String NAME = TestView.class.getName(); + + public TestView(Context context) { + super(context); + } + + public TestView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } +} \ No newline at end of file From cf2a227257baa238f41ac8d126110330d96d2932 Mon Sep 17 00:00:00 2001 From: James Barr Date: Mon, 30 Jan 2017 23:30:04 -0800 Subject: [PATCH 2/2] Drop Honeycomb support and minor cleanup --- viewpump/build.gradle | 4 +- .../github/inflationx/viewpump/ViewPump.java | 10 ++-- .../viewpump/ViewPumpLayoutInflater.java | 52 +++++-------------- viewpump/src/main/res/values/ids.xml | 2 +- 4 files changed, 19 insertions(+), 49 deletions(-) diff --git a/viewpump/build.gradle b/viewpump/build.gradle index aed8e8e..240ebed 100644 --- a/viewpump/build.gradle +++ b/viewpump/build.gradle @@ -5,7 +5,7 @@ android { buildToolsVersion "24.0.0" defaultConfig { - minSdkVersion 7 + minSdkVersion 14 targetSdkVersion 24 versionCode project.ext.versionCodeInt versionName version @@ -21,7 +21,7 @@ android { } dependencies { - compile 'com.android.support:appcompat-v7:24.0.0' + provided 'com.android.support:appcompat-v7:24.0.0' testCompile 'org.assertj:assertj-core:1.7.1' testCompile 'junit:junit:4.12' diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPump.java b/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPump.java index 0b4b80b..bd64f4f 100644 --- a/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPump.java +++ b/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPump.java @@ -1,6 +1,5 @@ package io.github.inflationx.viewpump; -import android.os.Build; import android.support.annotation.MainThread; import android.view.View; @@ -76,8 +75,8 @@ public static final class Builder { /** List of interceptors. */ private final List interceptors = new ArrayList<>(); - /** Use Reflection to inject the private factory. Doesn't exist pre HC. so defaults to false. */ - private boolean reflection = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; + /** Use Reflection to inject the private factory. Defaults to true. */ + private boolean reflection = true; /** Use Reflection to intercept CustomView inflation with the correct Context. */ private boolean customViewCreation = true; @@ -91,8 +90,7 @@ public Builder addInterceptor(Interceptor interceptor) { /** *

Turn of the use of Reflection to inject the private factory. - * This has operational consequences! Please read and understand before disabling. - * This is already disabled on pre Honeycomb devices. (API 11)

+ * This has operational consequences! Please read and understand before disabling.

* *

If you disable this you will need to override your {@link android.app.Activity#onCreateView(View, String, android.content.Context, android.util.AttributeSet)} * as this is set as the {@link android.view.LayoutInflater} private factory.

@@ -100,7 +98,6 @@ public Builder addInterceptor(Interceptor interceptor) { * Use the following code in the Activity if you disable FactoryInjection: *

          * {@literal @}Override
-         * {@literal @}TargetApi(Build.VERSION_CODES.HONEYCOMB)
          * public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
          *   return ViewPumpContextWrapper.onActivityCreateView(this, parent, super.onCreateView(parent, name, context, attrs), name, context, attrs);
          * }
@@ -119,7 +116,6 @@ public Builder setPrivateFactoryInjectionEnabled(boolean enabled) {
          * overrideable injection flow.
          *
          * On HoneyComb+ this is inside the {@link android.app.Activity#onCreateView(View, String, android.content.Context, android.util.AttributeSet)}
-         * Pre HoneyComb this is in the {@link android.view.LayoutInflater.Factory#onCreateView(String, android.util.AttributeSet)}
          *
          * We wrap base implementations, so if you LayoutInflater/Factory/Activity creates the
          * custom view before we get to this point, your view is used. (Such is the case with the
diff --git a/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpLayoutInflater.java b/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpLayoutInflater.java
index 9e7e6d3..6f9bf38 100644
--- a/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpLayoutInflater.java
+++ b/viewpump/src/main/java/io/github/inflationx/viewpump/ViewPumpLayoutInflater.java
@@ -1,8 +1,6 @@
 package io.github.inflationx.viewpump;
 
-import android.annotation.TargetApi;
 import android.content.Context;
-import android.os.Build;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -64,11 +62,9 @@ public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)
     private void setUpLayoutFactories(boolean cloned) {
         if (cloned) return;
         // If we are HC+ we get and set Factory2 otherwise we just wrap Factory1
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
-            if (getFactory2() != null && !(getFactory2() instanceof WrapperFactory2)) {
-                // Sets both Factory/Factory2
-                setFactory2(getFactory2());
-            }
+        if (getFactory2() != null && !(getFactory2() instanceof WrapperFactory2)) {
+            // Sets both Factory/Factory2
+            setFactory2(getFactory2());
         }
         // We can do this as setFactory2 is used for both methods.
         if (getFactory() != null && !(getFactory() instanceof WrapperFactory)) {
@@ -80,14 +76,13 @@ private void setUpLayoutFactories(boolean cloned) {
     public void setFactory(Factory factory) {
         // Only set our factory and wrap calls to the Factory trying to be set!
         if (!(factory instanceof WrapperFactory)) {
-            super.setFactory(new WrapperFactory(factory, this));
+            super.setFactory(new WrapperFactory(factory));
         } else {
             super.setFactory(factory);
         }
     }
 
     @Override
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
     public void setFactory2(Factory2 factory2) {
         // Only set our factory and wrap calls to the Factory2 trying to be set!
         if (!(factory2 instanceof WrapperFactory2)) {
@@ -109,6 +104,7 @@ private void setPrivateFactoryInternal() {
             return;
         }
 
+        // TODO: we need to get this and wrap it if something has already set this
         final Method setPrivateFactoryMethod = ReflectionUtils.getMethod(LayoutInflater.class, "setPrivateFactory");
 
         if (setPrivateFactoryMethod != null) {
@@ -129,7 +125,6 @@ private void setPrivateFactoryInternal() {
      * We opted to manual injection over aggressive reflection, this should be less fragile.
      */
     @Override
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
     public View onActivityCreateView(View parent, View view, String name, Context context, AttributeSet attrs) {
         return ViewPump.get().inflate(InflateRequest.builder()
                 .name(name)
@@ -145,7 +140,6 @@ public View onActivityCreateView(View parent, View view, String name, Context co
      * BUT only for none CustomViews.
      */
     @Override
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
     protected View onCreateView(View parent, String name, AttributeSet attrs) throws ClassNotFoundException {
         return ViewPump.get().inflate(InflateRequest.builder()
                 .name(name)
@@ -165,7 +159,7 @@ protected View onCreateView(View parent, String name, AttributeSet attrs) throws
     protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
         return ViewPump.get().inflate(InflateRequest.builder()
                 .name(name)
-                .context(getContext()) // TODO: is this OK? before was fallbackView.getContext()
+                .context(getContext())
                 .attrs(attrs)
                 .fallbackViewCreator(nameAndAttrsViewCreator)
                 .build()).view();
@@ -217,7 +211,6 @@ private View createCustomViewInternal(View parent, View view, String name, Conte
         return view;
     }
 
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
     private View superOnCreateView(View parent, String name, AttributeSet attrs) {
         try {
             return super.onCreateView(parent, name, attrs);
@@ -281,6 +274,9 @@ public View onCreateView(View parent, String name, Context context, AttributeSet
             for (String prefix : sClassPrefixList) {
                 try {
                     view = inflater.createView(name, prefix, attrs);
+                    if (view != null) {
+                        break;
+                    }
                 } catch (ClassNotFoundException ignored) {
                 }
             }
@@ -292,7 +288,7 @@ public View onCreateView(View parent, String name, Context context, AttributeSet
     }
 
     // ===
-    // Wrapper Factories for Pre/Post HC
+    // Wrapper Factories
     // ===
 
     /**
@@ -302,10 +298,8 @@ private static class WrapperFactory implements Factory {
 
         private final FallbackViewCreator mViewCreator;
 
-        public WrapperFactory(Factory factory, ViewPumpLayoutInflater inflater) {
-            mViewCreator = Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
-                    ? new PreHcWrapperFactoryViewCreator(factory, inflater)
-                    : new WrapperFactoryViewCreator(factory);
+        public WrapperFactory(Factory factory) {
+            mViewCreator = new WrapperFactoryViewCreator(factory);
         }
 
         @Override
@@ -332,25 +326,9 @@ public View onCreateView(View parent, String name, Context context, AttributeSet
         }
     }
 
-    private static class PreHcWrapperFactoryViewCreator extends WrapperFactoryViewCreator implements FallbackViewCreator {
-        protected final ViewPumpLayoutInflater mInflater;
-
-        public PreHcWrapperFactoryViewCreator(Factory factory, ViewPumpLayoutInflater inflater) {
-            super(factory);
-            mInflater = inflater;
-        }
-
-        @Override
-        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
-            return mInflater.createCustomViewInternal(
-                    null, mFactory.onCreateView(name, context, attrs), name, context, attrs);
-        }
-    }
-
     /**
      * Factory 2 is the second port of call for LayoutInflation
      */
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
     private static class WrapperFactory2 implements Factory2 {
         protected final Factory2 mFactory2;
         private final WrapperFactory2ViewCreator mViewCreator;
@@ -377,7 +355,6 @@ public View onCreateView(View parent, String name, Context context, AttributeSet
         }
     }
 
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
     private static class WrapperFactory2ViewCreator implements FallbackViewCreator {
         protected final Factory2 mFactory2;
 
@@ -392,10 +369,8 @@ public View onCreateView(View parent, String name, Context context, AttributeSet
     }
 
     /**
-     * Private factory is step three for Activity Inflation, this is what is attached to the
-     * Activity on HC+ devices.
+     * Private factory is step three for Activity Inflation, this is what is attached to the Activity
      */
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
     private static class PrivateWrapperFactory2 extends WrapperFactory2 {
 
         private final PrivateWrapperFactory2ViewCreator mViewCreator;
@@ -417,7 +392,6 @@ public View onCreateView(View parent, String name, Context context, AttributeSet
         }
     }
 
-    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
     private static class PrivateWrapperFactory2ViewCreator extends WrapperFactory2ViewCreator implements FallbackViewCreator {
         private final ViewPumpLayoutInflater mInflater;
 
diff --git a/viewpump/src/main/res/values/ids.xml b/viewpump/src/main/res/values/ids.xml
index 8866d8d..4d0b9c5 100644
--- a/viewpump/src/main/res/values/ids.xml
+++ b/viewpump/src/main/res/values/ids.xml
@@ -1,4 +1,4 @@
 
 
     
-
\ No newline at end of file
+