Commit 17b2aaaf authored by Robbie McElrath's avatar Robbie McElrath Committed by Commit Bot

[WebLayer] Fix Site Settings Dialogs on WebLayer L,M

This CL works around a LayoutInflater bug in Android <N that caused all
Dialogs in WebLayer's Site Settings UI to crash when opening.
LayoutInflater creates Views by looking up their constructor from the
class's name, and caches this name to constructor mapping as an
optimization. This cache causes issues if a View is loaded multiple
times with difference ClassLoaders. In Site Settings, some AndroidX
classes get loaded early on with the embedding app's ClassLoader, which
causes the constructor from that ClassLoader's version of the class to
be cached. When the WebLayer implementation then tries to load the same
class, it loads the version from the embedding app, rather than the
WebLayer's version.

This issue was fixed in Android N, but to work around it on earlier
versions of Android, this CL registers Site Setting's fake
FragmentActivity as the Factory2 for the implementation Context so it
can intercept View loading, and loads Views manually to bypass the
LayoutInflater cache.

Bug: 1126647
Change-Id: Icaf81b2dd84dc5b82b28a4c8d92114a4b7a37598
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2402188
Commit-Queue: Robbie McElrath <rmcelrath@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Cr-Commit-Position: refs/heads/master@{#805626}
parent d40cf21e
......@@ -9,11 +9,14 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.Window;
import androidx.appcompat.app.AppCompatDelegate;
......@@ -39,6 +42,8 @@ import org.chromium.weblayer_private.interfaces.SiteSettingsFragmentArgs;
import org.chromium.weblayer_private.interfaces.SiteSettingsIntentHelper;
import org.chromium.weblayer_private.interfaces.StrictModeWorkaround;
import java.lang.reflect.Constructor;
/**
* WebLayer's implementation of the client library's SiteSettingsFragment.
*
......@@ -78,11 +83,17 @@ public class SiteSettingsFragmentImpl extends RemoteFragmentImpl {
*/
private static class PassthroughFragmentActivity extends FragmentActivity
implements PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
private static final Class<?>[] VIEW_CONSTRUCTOR_ARGS =
new Class[] {Context.class, AttributeSet.class};
private final SiteSettingsFragmentImpl mFragmentImpl;
private PassthroughFragmentActivity(SiteSettingsFragmentImpl fragmentImpl) {
mFragmentImpl = fragmentImpl;
attachBaseContext(mFragmentImpl.getWebLayerContext());
// Register ourselves as a the LayoutInflater factory so we can handle loading Views.
// See onCreateView for information about why this is needed.
getLayoutInflater().setFactory2(this);
// This class doesn't extend AppCompatActivity, so some appcompat functionality doesn't
// get initialized, which leads to some appcompat widgets (like switches) rendering
// incorrectly. There are some resource issues with having this class extend
......@@ -105,6 +116,54 @@ public class SiteSettingsFragmentImpl extends RemoteFragmentImpl {
Context.LAYOUT_INFLATER_SERVICE);
}
// This method is needed to work around a LayoutInflater bug in Android <N. Before
// LayoutInflater creates an instance of a View, it needs to look up the class by name to
// get a reference to its Constructor. As an optimization, it caches this name to
// Constructor mapping. This cache causes issues if a class gets loaded multiple times with
// different ClassLoaders. In Site Settings, some AndroidX Views get loaded early on with
// the embedding app's ClassLoader, so the Constructor from that ClassLoader's version of
// the class gets cached. When the WebLayer implementation later tries to inflate the same
// class, it instantiates a version from the wrong ClassLoader, which leads to a
// ClassCastException when casting that View to its original class. This was fixed in
// Android N, but to work around it on L & M, we inflate the Views manually here, which
// bypasses LayoutInflater's cache.
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// If the class doesn't have a '.' in its name, it's probably a built-in Android View,
// which are often referenced by just their class names with no package prefix. For
// these classes we can return null to fall back to LayoutInflater's default behavior.
if (name.indexOf('.') == -1) {
return null;
}
Class<? extends View> clazz = null;
try {
clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
LayoutInflater inflater = getLayoutInflater();
if (inflater.getFilter() != null && !inflater.getFilter().onLoadClass(clazz)) {
throw new InflateException(attrs.getPositionDescription()
+ ": Class not allowed to be inflated " + name);
}
Constructor<? extends View> constructor =
clazz.getConstructor(VIEW_CONSTRUCTOR_ARGS);
constructor.setAccessible(true);
View view = constructor.newInstance(new Object[] {context, attrs});
if (view instanceof ViewStub) {
// Use the same Context when inflating ViewStub later.
ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(inflater.cloneInContext(context));
}
return view;
} catch (Exception e) {
InflateException ie = new InflateException(attrs.getPositionDescription()
+ ": Error inflating class "
+ (clazz == null ? "<unknown>" : clazz.getName()));
ie.initCause(e);
throw ie;
}
}
@Override
public Window getWindow() {
return getEmbedderActivity().getWindow();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment