From 027f6aa554b31a47a8cd9a63a4b85d240c02945b Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Sat, 8 Jan 2022 12:31:57 +0530 Subject: [PATCH] Fix: Invalid XML code generated by UI Designer --- .../java/com/itsaky/widgets/WidgetInfo.java | 32 ++++-- .../com/itsaky/widgets/models/Widget.java | 14 +-- .../itsaky/inflater/XMLLayoutInflater.java | 34 +++--- .../android/view/ViewAttrAdapter.java | 1 - .../com/itsaky/inflater/impl/ErrorUiView.java | 100 ++++++++++++++++++ 5 files changed, 147 insertions(+), 34 deletions(-) create mode 100644 xml-inflater/src/main/java/com/itsaky/inflater/impl/ErrorUiView.java diff --git a/sdk-info/src/main/java/com/itsaky/widgets/WidgetInfo.java b/sdk-info/src/main/java/com/itsaky/widgets/WidgetInfo.java index 04ba40102..4b538c4b1 100755 --- a/sdk-info/src/main/java/com/itsaky/widgets/WidgetInfo.java +++ b/sdk-info/src/main/java/com/itsaky/widgets/WidgetInfo.java @@ -21,6 +21,10 @@ package com.itsaky.widgets; import android.content.Context; import android.content.res.Resources; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.itsaky.widgets.models.Widget; import java.io.BufferedReader; import java.io.IOException; @@ -28,26 +32,39 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.Collection; import java.util.SortedMap; +import java.util.SortedSet; import java.util.TreeMap; public class WidgetInfo { private final SortedMap widgets; - public WidgetInfo (final Context ctx) throws IOException { + public WidgetInfo (@NonNull final Context ctx) throws IOException { this.widgets = new TreeMap<>(); readWidgets(ctx.getResources()); } - public Widget getWidgetBySimpleName (String simpleName) { - return this.widgets.getOrDefault(simpleName, null); + public Widget getWidget (String name) { + return this.widgets.getOrDefault(name, null); + } + + @Nullable + public Widget getWidgetBySimpleName (@NonNull final String name) { + for (final var entry : this.widgets.entrySet ()) { + final var key = entry.getKey (); + final var simple = key.substring (key.lastIndexOf (".") + 1); + if (simple.equals (name)) { + return entry.getValue (); + } + } + return null; } public Collection getWidgets() { return this.widgets.values(); } - private void readWidgets(final Resources resources) throws IOException { + private void readWidgets(@NonNull final Resources resources) throws IOException { final InputStream in = resources.openRawResource(com.itsaky.sdkinfo.R.raw.widgets); if(in == null) return; @@ -60,17 +77,12 @@ public class WidgetInfo { final char code = name.charAt(0); final String viewName = name.substring(1); - final String simpleViewName = simpleName(viewName); final boolean isViewGroup = code == 'L'; // L -> Layout, W -> Widget, P -> LayoutParam // Don't add layout params if(code != 'P') { - widgets.put(simpleViewName, new Widget(viewName, simpleViewName, isViewGroup)); + widgets.put(viewName, new Widget(viewName, isViewGroup)); } } } - - private String simpleName(String name) { - return name.substring(name.lastIndexOf(".") + 1); - } } diff --git a/sdk-info/src/main/java/com/itsaky/widgets/models/Widget.java b/sdk-info/src/main/java/com/itsaky/widgets/models/Widget.java index 35dd33e94..0e50034c7 100755 --- a/sdk-info/src/main/java/com/itsaky/widgets/models/Widget.java +++ b/sdk-info/src/main/java/com/itsaky/widgets/models/Widget.java @@ -19,24 +19,26 @@ **************************************************************************************/ package com.itsaky.widgets.models; -public class Widget implements Comparable { +import androidx.annotation.NonNull; + +public class Widget implements Comparable { public String name; public String simpleName; public boolean isViewGroup; - public Widget(String name, String simpleName, boolean isViewGroup) { + public Widget(@NonNull String name, boolean isViewGroup) { this.name = name; - this.simpleName = simpleName; + this.simpleName = name.substring (name.lastIndexOf (".") + 1); this.isViewGroup = isViewGroup; } @Override - public int compareTo(Object p1) { - if(p1 instanceof Widget) { - Widget that = (Widget) p1; + public int compareTo(Widget that) { + if(that != null) { return this.simpleName.compareTo(that.simpleName); } + return -1; } diff --git a/xml-inflater/src/main/java/com/itsaky/inflater/XMLLayoutInflater.java b/xml-inflater/src/main/java/com/itsaky/inflater/XMLLayoutInflater.java index 6031f59bb..c8f476975 100644 --- a/xml-inflater/src/main/java/com/itsaky/inflater/XMLLayoutInflater.java +++ b/xml-inflater/src/main/java/com/itsaky/inflater/XMLLayoutInflater.java @@ -34,6 +34,7 @@ import com.itsaky.attrinfo.AttrInfo; import com.itsaky.inflater.adapters.android.view.ViewAttrAdapter; import com.itsaky.inflater.adapters.android.view.ViewGroupAttrAdapter; import com.itsaky.inflater.impl.BaseView; +import com.itsaky.inflater.impl.ErrorUiView; import com.itsaky.inflater.impl.UiAttribute; import com.itsaky.inflater.impl.UiView; import com.itsaky.inflater.impl.UiViewGroup; @@ -43,6 +44,7 @@ import com.itsaky.widgets.models.Widget; import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; @@ -104,7 +106,7 @@ class XMLLayoutInflater extends BaseLayoutInflater { IView root = onCreateView (doc.child (0), parent); if (root == null) { - root = onCreateErrorView (TextView.class.getName (), "No views added"); + root = onCreateErrorView (TextView.class.getName (), "No views added", parent); } // Notify @@ -267,7 +269,7 @@ class XMLLayoutInflater extends BaseLayoutInflater { final Widget widget = widgetInfo.getWidgetBySimpleName (name); if (widget == null) { LOG.error ("Unable to inflate view. widget == null"); - return onCreateErrorView (name, getString (com.itsaky.inflater.R.string.msg_cannot_create_view, name)); + return onCreateErrorView (name, getString (com.itsaky.inflater.R.string.msg_cannot_create_view, name), parent); } return createFromQualifiedName (widget.name, parent, style); @@ -278,6 +280,13 @@ class XMLLayoutInflater extends BaseLayoutInflater { // TODO Try to load classes directly from .class files if possible try { + + final var androidView = this.widgetInfo.getWidget (name); + if (androidView == null) { + // If this is not an Android view, do not bother to create one + throw new InflateException (String.format ("%s is not supported yet.", name)); + } + final View created = createAndroidViewForName (name); final BaseView view = created instanceof ViewGroup @@ -287,7 +296,7 @@ class XMLLayoutInflater extends BaseLayoutInflater { } catch (Throwable th) { final var msg = getString (R.string.msg_cannot_create_view, name); LOG.error (msg, th); - return onCreateErrorView (name, msg); + return onCreateErrorView (name, msg, parent); } } @@ -295,8 +304,7 @@ class XMLLayoutInflater extends BaseLayoutInflater { protected View createAndroidViewForName (String name) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException { final Class loaded = Class.forName (name).asSubclass (View.class); final Constructor constructor = loaded.getConstructor (Context.class /*, AttributeSet.class, int.class*/); - final View created = constructor.newInstance (contextProvider.getContext ()/*, null, style*/); - return created; + return constructor.newInstance (contextProvider.getContext ()/*, null, style*/); } protected IView applyLayoutParams (@NonNull IView view, ViewGroup parent) { @@ -305,18 +313,9 @@ class XMLLayoutInflater extends BaseLayoutInflater { return view; } - protected IView onCreateErrorView (String name, String msg) throws InflateException { - - final Context ctx = contextProvider.getContext (); - assertNotnull (ctx, "Context is null!"); - - final TextView error = new TextView (ctx); - error.setText (msg); - error.setLayoutParams (new ViewGroup.LayoutParams (-2, -2)); - error.setTextColor (ContextCompat.getColor (ctx, android.R.color.black)); - error.setBackgroundColor (ContextCompat.getColor (ctx, android.R.color.white)); - - return new UiView (name, error, true); + protected IView onCreateErrorView (String name, String msg, final ViewGroup parent) throws InflateException { + final var view = ErrorUiView.create (Objects.requireNonNull (contextProvider.getContext (), "Context is null."), name, msg); + return applyLayoutParams (view, parent); } private String getString (@StringRes int id, Object... format) throws InflateException { @@ -329,6 +328,7 @@ class XMLLayoutInflater extends BaseLayoutInflater { try { final Class clazz = parent.getClass (); Method method = null; + try { method = clazz.getDeclaredMethod ("generateDefaultLayoutParams"); } catch (NoSuchMethodException | SecurityException e) { diff --git a/xml-inflater/src/main/java/com/itsaky/inflater/adapters/android/view/ViewAttrAdapter.java b/xml-inflater/src/main/java/com/itsaky/inflater/adapters/android/view/ViewAttrAdapter.java index df2d84075..b3f837f10 100644 --- a/xml-inflater/src/main/java/com/itsaky/inflater/adapters/android/view/ViewAttrAdapter.java +++ b/xml-inflater/src/main/java/com/itsaky/inflater/adapters/android/view/ViewAttrAdapter.java @@ -72,7 +72,6 @@ public class ViewAttrAdapter extends CommonParseUtils implements IAttributeAdapt final String name = attribute.getAttributeName(); final String value = attribute.getValue(); final ViewGroup.LayoutParams params = view.getLayoutParams(); - final Resources res = view.getResources(); final Context ctx = view.getContext(); if (!canHandleNamespace(namespace)) { diff --git a/xml-inflater/src/main/java/com/itsaky/inflater/impl/ErrorUiView.java b/xml-inflater/src/main/java/com/itsaky/inflater/impl/ErrorUiView.java new file mode 100644 index 000000000..a35f6a4a8 --- /dev/null +++ b/xml-inflater/src/main/java/com/itsaky/inflater/impl/ErrorUiView.java @@ -0,0 +1,100 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.inflater.impl; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import com.itsaky.inflater.IView; + +import org.jetbrains.annotations.Contract; + +/** + * A view which is used to show views that cannot be inflated. + * This view will preserver the attributes and the children + * if the view is a view group; + */ +public class ErrorUiView extends UiViewGroup { + + private ErrorUiView (String qualifiedName, ViewGroup view) { + super (qualifiedName, view); + } + + private ErrorUiView (String qualifiedName, ViewGroup view, boolean isPlaceholder) { + super (qualifiedName, view, isPlaceholder); + } + + /** + * Create a new error view for the given qualified name of the view + * that was not inflated correctly. + * + * We add a text directly in this view. Any other views that will + * be added to this group will have visibility {@link View#GONE} + * by default. + * + * @param context The context that will be used to create the view. + * @param name The qualified name of the view that was not inflated. + * @param message The message that should be shown to the user. + * @return A newly created instance of the error view. + */ + @NonNull + @Contract("_, _, _ -> new") + public static IView create (@NonNull final Context context, @NonNull final String name, @NonNull final String message) { + final var container = new LinearLayout (context); + final var error = new TextView (context); + error.setText (message); + error.setLayoutParams (new ViewGroup.LayoutParams (-2, -2)); + error.setTextColor (ContextCompat.getColor (context, android.R.color.black)); + error.setBackgroundColor (ContextCompat.getColor (context, android.R.color.white)); + + container.setOrientation (LinearLayout.HORIZONTAL); + container.addView (error, new LinearLayout.LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + return new ErrorUiView (name, container, true); + } + + @Override + public void addView (IView view, int index) { + // Adding the view at the given index will result in an error + // This is because the children list is different in this error + // view. Because of TextView added in ErrorView.create (...); + // So we simply add the view at index 0 and make it invisible + super.addView (view, 0); + + view.asView ().setVisibility (View.GONE); + } + + @Override + public String getXmlTag () { + // This might be a custom view or something + // So return the qualified name of the view + return this.qualifiedName; + } + + @NonNull + @Override + public String generateCode (int indentCount) { + return super.generateCode (indentCount); + } +}