From d66d65dae633cb83d6bc87ea77fc7f0a4296cdeb Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 29 Mar 2022 09:07:01 +0530 Subject: [PATCH] Recreate layout inflater to avoid NPE after activity recreation Wait for SDKInfo to be initialized before inflating layout --- .idea/google-java-format.xml | 1 - .../itsaky/androidide/DesignerActivity.java | 723 +++++++++--------- .../com/itsaky/androidide/EditorActivity.java | 26 +- .../com/itsaky/androidide/app/StudioApp.java | 196 ++--- .../androidide/models/XMLAttribute.java | 254 +++--- .../com/itsaky/inflater/ILayoutInflater.java | 263 +++---- .../itsaky/inflater/XMLLayoutInflater.java | 2 +- 7 files changed, 737 insertions(+), 728 deletions(-) diff --git a/.idea/google-java-format.xml b/.idea/google-java-format.xml index 05946a82a..8b57f4527 100644 --- a/.idea/google-java-format.xml +++ b/.idea/google-java-format.xml @@ -2,6 +2,5 @@ \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/DesignerActivity.java b/app/src/main/java/com/itsaky/androidide/DesignerActivity.java index a5c05999a..098fd5cb2 100755 --- a/app/src/main/java/com/itsaky/androidide/DesignerActivity.java +++ b/app/src/main/java/com/itsaky/androidide/DesignerActivity.java @@ -18,10 +18,13 @@ package com.itsaky.androidide; import android.annotation.SuppressLint; +import android.app.ProgressDialog; import android.content.ClipData; +import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; +import android.text.TextUtils; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -50,6 +53,7 @@ import androidx.transition.TransitionManager; import androidx.transition.TransitionSet; import com.blankj.utilcode.util.DeviceUtils; +import com.blankj.utilcode.util.ThreadUtils; import com.blankj.utilcode.util.ThrowableUtils; import com.itsaky.androidide.adapters.WidgetGroupItemAdapter; import com.itsaky.androidide.adapters.WidgetItemAdapter; @@ -70,6 +74,7 @@ import com.itsaky.inflater.IInflateListener; import com.itsaky.inflater.ILayoutInflater; import com.itsaky.inflater.IView; import com.itsaky.inflater.IViewGroup; +import com.itsaky.inflater.LayoutInflaterConfiguration; import com.itsaky.inflater.impl.BaseView; import org.jetbrains.annotations.Contract; @@ -77,403 +82,409 @@ import org.jetbrains.annotations.Contract; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; public class DesignerActivity extends StudioActivity - implements WidgetItemAdapter.OnDragStartListener, - AttrEditorSheet.OnViewDeletionFailedListener { + implements WidgetItemAdapter.OnDragStartListener, + AttrEditorSheet.OnViewDeletionFailedListener, + ILayoutInflater.ContextProvider { - public static final String KEY_LAYOUT_PATH = "designer_layoutPath"; - public static final String KEY_GENERATED_CODE = "designer_xmlCode"; - public static final String DRAGGING_WIDGET_TAG = "DRAGGING_WIDGET"; - public static final String DRAGGING_WIDGET_MIME = "application/ide_widget"; - private static final Logger LOG = Logger.instance("DesignerActivity"); - private final boolean isTablet = DeviceUtils.isTablet(); - private final List widgetGroups = new ArrayList<>(); - private ActivityDesignerBinding mBinding; - private UIWidgetGroup checkedWidgetCategory; - private IViewGroup inflatedRoot; - private File layout; - private boolean inflationFailed = false; - private AttrEditorSheet mEditorSheet; - // This will make sure to apply listeners, background and data to view that are inflated from - // XML. - private final IInflateListener mInflateListener = - new IInflateListener() { + public static final String KEY_LAYOUT_PATH = "designer_layoutPath"; + public static final String KEY_RES_DIRS = "designer_resDirs"; + public static final String KEY_GENERATED_CODE = "designer_xmlCode"; + public static final String DRAGGING_WIDGET_TAG = "DRAGGING_WIDGET"; + public static final String DRAGGING_WIDGET_MIME = "application/ide_widget"; + private static final Logger LOG = Logger.instance("DesignerActivity"); + private final boolean isTablet = DeviceUtils.isTablet(); + private final List widgetGroups = new ArrayList<>(); + private ActivityDesignerBinding mBinding; + private UIWidgetGroup checkedWidgetCategory; + private ILayoutInflater layoutInflater; + private IViewGroup inflatedRoot; + private File layout; + private boolean inflationFailed = false; + private AttrEditorSheet mEditorSheet; + // This will make sure to apply listeners, background and data to view that are inflated from + // XML. + private final IInflateListener mInflateListener = + new IInflateListener() { - @Override - public void onBeginInflate() {} + @Override + public void onBeginInflate() {} - @Override - public void onInflateView(IView view, IViewGroup parent) { - if (view instanceof IViewGroup) { - View v = view.asView(); - v.setBackgroundResource(R.drawable.bg_design_container_background); - v.setOnDragListener(getOnDragListener((IViewGroup) view)); - } + @Override + public void onInflateView(IView view, IViewGroup parent) { + if (view instanceof IViewGroup) { + View v = view.asView(); + v.setBackgroundResource(R.drawable.bg_design_container_background); + v.setOnDragListener(getOnDragListener((IViewGroup) view)); + } - setupInflatedView(view); - setDragDataToInflatedView(view); - } + setupInflatedView(view); + setDragDataToInflatedView(view); + } - @Override - public void onApplyAttribute(IAttribute attr, IView view) {} + @Override + public void onApplyAttribute(IAttribute attr, IView view) {} - @Override - public void onFinishInflate(IView rootView) { - if (rootView instanceof IViewGroup) { - inflatedRoot = (IViewGroup) rootView; - } - } - }; + @Override + public void onFinishInflate(IView rootView) { + if (rootView instanceof IViewGroup) { + inflatedRoot = (IViewGroup) rootView; + } + } + }; - @Override - protected View bindLayout() { - this.mBinding = ActivityDesignerBinding.inflate(getLayoutInflater()); - return mBinding.getRoot(); - } + @Override + protected View bindLayout() { + this.mBinding = ActivityDesignerBinding.inflate(getLayoutInflater()); + return mBinding.getRoot(); + } - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); - setSupportActionBar(mBinding.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setSupportActionBar(mBinding.toolbar); + Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); - final var toggle = - new ActionBarDrawerToggle( - this, - mBinding.getRoot(), - mBinding.toolbar, - R.string.app_name, - R.string.app_name) { + final var toggle = + new ActionBarDrawerToggle( + this, mBinding.getRoot(), mBinding.toolbar, R.string.app_name, R.string.app_name) { - @Override - public void onDrawerSlide(View drawerView, float slideOffset) { - super.onDrawerSlide(drawerView, slideOffset); - if (isTablet) { - mBinding.mainContainer.setTranslationX( - drawerView.getWidth() * slideOffset); - } - } - }; - mBinding.getRoot().addDrawerListener(toggle); - toggle.syncState(); - - final var extras = getIntent().getExtras(); - final var path = extras.getString(KEY_LAYOUT_PATH, null); - final var name = path.substring(path.lastIndexOf(File.separator) + 1); - getSupportActionBar().setTitle(name); - - try { - this.layout = new File(path); - - final ILayoutInflater inflater = getApp().getLayoutInflater(); - inflater.resetContextProvider(newContextProvider()); - inflater.registerInflateListener(this.mInflateListener); - final IView view = inflater.inflatePath(path, mBinding.layoutContainer); - - if (this.inflatedRoot != null) { - this.inflatedRoot.asView().setOnDragListener(getOnDragListener(this.inflatedRoot)); + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + super.onDrawerSlide(drawerView, slideOffset); + if (isTablet) { + mBinding.mainContainer.setTranslationX(drawerView.getWidth() * slideOffset); } + } + }; + mBinding.getRoot().addDrawerListener(toggle); + toggle.syncState(); - mBinding.layoutContainer.addView(view.asView()); - mBinding.layoutContainer.setOnClickListener( - v -> mBinding.getRoot().openDrawer(GravityCompat.START)); - } catch (Throwable th) { - mBinding.layoutContainer.removeAllViews(); - mBinding.layoutContainer.addView(createErrorText(th)); - LOG.error(getString(R.string.err_cannot_inflate_layout), th); - inflationFailed = true; - } + final var extras = getIntent().getExtras(); + final var path = extras.getString(KEY_LAYOUT_PATH, null); + final var name = path.substring(path.lastIndexOf(File.separator) + 1); + final var resDirs = extras.getStringArrayList(KEY_RES_DIRS); + getSupportActionBar().setTitle(name); - setupWidgets(); + try { + this.layout = new File(path); + LOG.debug("Inflating layout file:", this.layout); + LOG.debug("Resource directories:", TextUtils.join(",\n", resDirs)); - // Always open the drawer if this device is a tablet. - if (isTablet) { - mBinding.getRoot().setScrimColor(Color.TRANSPARENT); - mBinding.getRoot().openDrawer(GravityCompat.START); - mBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN); - } + //noinspection deprecation + final var pd = ProgressDialog.show(this, null, getString(R.string.please_wait), true, false); + getApp() + .createInflaterConfig(this, resDirs.stream().map(File::new).collect(Collectors.toSet())) + .whenComplete( + (config, throwable) -> { + if (config == null || throwable != null) { + LOG.error("Unable to create inflater configuration", throwable); + return; + } + + ThreadUtils.runOnUiThread( + () -> { + pd.dismiss(); + inflatePath(path, config); + }); + }); + } catch (Throwable th) { + mBinding.layoutContainer.removeAllViews(); + mBinding.layoutContainer.addView(createErrorText(th)); + LOG.error(getString(R.string.err_cannot_inflate_layout), th); + inflationFailed = true; } - @Override - public void onBackPressed() { + setupWidgets(); - if (inflationFailed) { - super.onBackPressed(); + // Always open the drawer if this device is a tablet. + if (isTablet) { + mBinding.getRoot().setScrimColor(Color.TRANSPARENT); + mBinding.getRoot().openDrawer(GravityCompat.START); + mBinding.getRoot().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN); + } + } + + private void inflatePath(String path, LayoutInflaterConfiguration config) { + this.layoutInflater = ILayoutInflater.newInstance(config); + this.layoutInflater.resetContextProvider(this); + this.layoutInflater.registerInflateListener(this.mInflateListener); + final IView view = this.layoutInflater.inflatePath(path, mBinding.layoutContainer); + + if (this.inflatedRoot != null) { + this.inflatedRoot.asView().setOnDragListener(getOnDragListener(this.inflatedRoot)); + } + + mBinding.layoutContainer.addView(view.asView()); + mBinding.layoutContainer.setOnClickListener( + v -> mBinding.getRoot().openDrawer(GravityCompat.START)); + } + + @Override + public void onBackPressed() { + + if (inflationFailed) { + super.onBackPressed(); + return; + } + + // When the user presses the back button, set the activity result and finish this activity + + final var progress = new ProgressSheet(); + progress.setSubMessageEnabled(true); + progress.setShowTitle(false); + progress.setMessage(getString(R.string.title_generating_xml)); + progress.setSubMessage(getString(R.string.please_wait)); + progress.show(getSupportFragmentManager(), "generate_code_progress"); + + final var future = + CompletableFuture.supplyAsync( + () -> BaseView.XML_HEADER.concat("\n").concat(inflatedRoot.generateCode())); + future.whenComplete( + (s, throwable) -> { + progress.dismiss(); + + if (throwable != null) { + notifyXmlGenerationFailed(throwable); return; - } + } - // When the user presses the back button, set the activity result and finish this activity + try { + finishWithResult(future.get()); + } catch (ExecutionException | InterruptedException e) { + notifyXmlGenerationFailed(e); + } + }); + } - final var progress = new ProgressSheet(); - progress.setSubMessageEnabled(true); - progress.setShowTitle(false); - progress.setMessage(getString(R.string.title_generating_xml)); - progress.setSubMessage(getString(R.string.please_wait)); - progress.show(getSupportFragmentManager(), "generate_code_progress"); + private void notifyXmlGenerationFailed(Throwable error) { + final var errorMessage = getString(R.string.msg_generate_xml_failed); + LOG.error(errorMessage, error); - final var future = - CompletableFuture.supplyAsync( - () -> BaseView.XML_HEADER.concat("\n").concat(inflatedRoot.generateCode())); - future.whenComplete( - (s, throwable) -> { - progress.dismiss(); + DialogUtils.newYesNoDialog( + this, + getString(R.string.title_code_generation_failed), + getString(R.string.msg_code_generation_failed), + (dialog1, which) -> finishWithError(), + (dialog1, which) -> dialog1.dismiss()) + .show(); + } - if (throwable != null) { - notifyXmlGenerationFailed(throwable); - return; - } + private void finishWithResult(String result) { + final var intent = new Intent(); + intent.putExtra(KEY_GENERATED_CODE, result); + setResult(RESULT_OK, intent); + finish(); + } - try { - finishWithResult(future.get()); - } catch (ExecutionException | InterruptedException e) { - notifyXmlGenerationFailed(e); - } - }); + private void finishWithError() { + final var intent = new Intent(); + setResult(-123, intent); + finish(); + } + + @Override + protected void onDestroy() { + LOG.logThis(); + if (isAttrEditorShowing()) { + getAttrEditorSheet().dismiss(); } - private void notifyXmlGenerationFailed(Throwable error) { - final var errorMessage = getString(R.string.msg_generate_xml_failed); - LOG.error(errorMessage, error); + this.mEditorSheet = null; - DialogUtils.newYesNoDialog( - this, - getString(R.string.title_code_generation_failed), - getString(R.string.msg_code_generation_failed), - (dialog1, which) -> finishWithError(), - (dialog1, which) -> dialog1.dismiss()) - .show(); + // Release the reference to inflate listener and context from the layout inflater + // Failing to do so will lead getSupportFragmentManager() to return a destroyed fragment + // manager + try { + if (layoutInflater == null) { + LOG.debug("Skipping layout inflater context reference release. Inflater is null"); + return; + } + + this.layoutInflater.unregisterListener(mInflateListener); + this.layoutInflater.resetContextProvider(null); + } catch (Throwable e) { + LOG.error("Unable to release activity references from ILayoutInflater", e); } - private void finishWithResult(String result) { - final var intent = new Intent(); - intent.putExtra(KEY_GENERATED_CODE, result); - setResult(RESULT_OK, intent); - finish(); + super.onDestroy(); + } + + private void setupInflatedView(@NonNull IView view) { + final var listener = + new WidgetTouchListener(view, this::onLayoutViewClick, this::onLayoutViewLongClick); + view.asView().setOnTouchListener(listener); + + setDragDataToInflatedView(view); + } + + private boolean onLayoutViewLongClick(@NonNull IView view) { + final var shadow = new WidgetDragShadowBuilder(view.asView()); + final var item = new ClipData.Item(DesignerActivity.DRAGGING_WIDGET_TAG); + final var clip = + new ClipData( + DesignerActivity.DRAGGING_WIDGET_TAG, + new String[] {DesignerActivity.DRAGGING_WIDGET_MIME}, + item); + final var data = view.getExtraData(); + + view.asView().startDragAndDrop(clip, shadow, data, 0); + view.removeFromParent(); + + return true; + } + + private void onLayoutViewClick(@NonNull IView view) { + if (isAttrEditorShowing()) { + getAttrEditorSheet().dismiss(); } - private void finishWithError() { - final var intent = new Intent(); - setResult(-123, intent); - finish(); + getAttrEditorSheet().setSelectedView(view); + + if (!getSupportFragmentManager().isDestroyed()) { + getAttrEditorSheet().show(getSupportFragmentManager(), "attribute_editor_dialog"); } + } - @Override - protected void onDestroy() { + private boolean isAttrEditorShowing() { + return getAttrEditorSheet().getDialog() != null && getAttrEditorSheet().getDialog().isShowing(); + } - if (isAttrEditorShowing()) { - getAttrEditorSheet().dismiss(); - } + private void setupWidgets() { + final var android = new UIWidgetGroup(getString(R.string.widget_group_android)); + android.addChild( + new UIWidget(getString(R.string.widget_button), R.drawable.ic_widget_button, Button.class)); + android.addChild( + new UIWidget( + getString(R.string.widget_checkbox), R.drawable.ic_widget_checkbox, CheckBox.class)); + android.addChild( + new UIWidget( + getString(R.string.widget_checked_textview), + R.drawable.ic_widget_checked_textview, + CheckedTextView.class)); + android.addChild( + new UIWidget( + getString(R.string.widget_edittext), R.drawable.ic_widget_edittext, EditText.class)); + android.addChild( + new UIWidget( + getString(R.string.widget_image_button), + R.drawable.ic_widget_image, + ImageButton.class)); + android.addChild( + new UIWidget( + getString(R.string.widget_image_view), R.drawable.ic_widget_image, ImageView.class)); + android.addChild( + new UIWidget( + getString(R.string.widget_progressbar), + R.drawable.ic_widget_progress, + ProgressBar.class)); + android.addChild( + new UIWidget( + getString(R.string.widget_radio_button), + R.drawable.ic_widget_radio_button, + RadioButton.class)); + android.addChild( + new UIWidget( + getString(R.string.widget_seekbar), R.drawable.ic_widget_seekbar, SeekBar.class)); + android.addChild( + new UIWidget( + getString(R.string.widget_spinner), R.drawable.ic_widget_spinner, Spinner.class)); + android.addChild( + new UIWidget( + getString(R.string.widget_textview), R.drawable.ic_widget_textview, TextView.class)); - this.mEditorSheet = null; + // IMPORTANT: Do not select any other groups here + android.setSelected(true); + this.checkedWidgetCategory = android; + widgetGroups.add(android); - // Release the reference to inflate listener and context from the layout inflater - // Failing to do so will lead getSupportFragmentManager() to return a destroyed fragment - // manager - try { - final var layoutInflater = getApp().getLayoutInflater(); - layoutInflater.unregisterListener(mInflateListener); - layoutInflater.resetContextProvider(null); - } catch (Throwable e) { - e.printStackTrace(); - } + final var layouts = new UIWidgetGroup(getString(R.string.widget_group_layouts)); + layouts.addChild( + new UIWidget( + getString(R.string.layout_linear), R.drawable.ic_widget_linear, LinearLayout.class)); + layouts.addChild( + new UIWidget( + getString(R.string.layout_relative), + R.drawable.ic_widget_relative, + RelativeLayout.class)); - super.onDestroy(); + widgetGroups.add(layouts); + + final var adapter = new WidgetGroupItemAdapter(widgetGroups, this::showWidgetGroup); + mBinding.groupItems.setAdapter(adapter); + + showWidgetGroup(android); + } + + @SuppressLint("NotifyDataSetChanged") + private void showWidgetGroup(@NonNull UIWidgetGroup group) { + final var children = group.getChildren(); + this.mBinding.widgetItems.setAdapter(new WidgetItemAdapter(children, this)); + this.checkedWidgetCategory.setSelected(false); + this.checkedWidgetCategory = group; + this.checkedWidgetCategory.setSelected(true); + + TransitionManager.beginDelayedTransition( + mBinding.navigation, + new TransitionSet().addTransition(new Slide(Gravity.END)).addTransition(new Fade())); + + //noinspection ConstantConditions + this.mBinding.groupItems.getAdapter().notifyDataSetChanged(); + } + + @NonNull + private TextView createErrorText(Throwable th) { + final TextView error = new TextView(this); + error.setText(ThrowableUtils.getFullStackTrace(th)); + error.setLayoutParams(new ViewGroup.LayoutParams(-2, -2)); + error.setTextColor(ContextCompat.getColor(this, android.R.color.black)); + error.setBackgroundColor(ContextCompat.getColor(this, android.R.color.white)); + return error; + } + + @Override + public void onDragStarted(View view) { + mBinding.getRoot().closeDrawer(GravityCompat.START); + } + + @NonNull + @Contract("_ -> new") + private View.OnDragListener getOnDragListener(IViewGroup group) { + // WidgetDragListener cannot be reused + // This is because they keep a reference to the view group in which dragged view will be + // added + + return new WidgetDragListener(this, group, this::setupInflatedView); + } + + private void setDragDataToInflatedView(@NonNull IView view) { + view.setExtraData(new WidgetDragData(true, view, null)); + } + + private AttrEditorSheet getAttrEditorSheet() { + return this.mEditorSheet == null + ? mEditorSheet = new AttrEditorSheet().setLayout(this.layout) + : mEditorSheet; + } + + @Override + public boolean onDeletionFailed(@NonNull IView view) { + final var v = view.asView(); + if (mBinding.layoutContainer.indexOfChild(v) >= 0) { + mBinding.layoutContainer.removeView(v); + return true; } + return false; + } - private void setupInflatedView(@NonNull IView view) { - final var listener = - new WidgetTouchListener(view, this::onLayoutViewClick, this::onLayoutViewLongClick); - view.asView().setOnTouchListener(listener); - - setDragDataToInflatedView(view); - } - - private boolean onLayoutViewLongClick(@NonNull IView view) { - final var shadow = new WidgetDragShadowBuilder(view.asView()); - final var item = new ClipData.Item(DesignerActivity.DRAGGING_WIDGET_TAG); - final var clip = - new ClipData( - DesignerActivity.DRAGGING_WIDGET_TAG, - new String[] {DesignerActivity.DRAGGING_WIDGET_MIME}, - item); - final var data = view.getExtraData(); - - view.asView().startDragAndDrop(clip, shadow, data, 0); - view.removeFromParent(); - - return true; - } - - private void onLayoutViewClick(@NonNull IView view) { - if (isAttrEditorShowing()) { - getAttrEditorSheet().dismiss(); - } - - getAttrEditorSheet().setSelectedView(view); - - if (!getSupportFragmentManager().isDestroyed()) { - getAttrEditorSheet().show(getSupportFragmentManager(), "attribute_editor_dialog"); - } - } - - private boolean isAttrEditorShowing() { - return getAttrEditorSheet().getDialog() != null - && getAttrEditorSheet().getDialog().isShowing(); - } - - private void setupWidgets() { - final var android = new UIWidgetGroup(getString(R.string.widget_group_android)); - android.addChild( - new UIWidget( - getString(R.string.widget_button), - R.drawable.ic_widget_button, - Button.class)); - android.addChild( - new UIWidget( - getString(R.string.widget_checkbox), - R.drawable.ic_widget_checkbox, - CheckBox.class)); - android.addChild( - new UIWidget( - getString(R.string.widget_checked_textview), - R.drawable.ic_widget_checked_textview, - CheckedTextView.class)); - android.addChild( - new UIWidget( - getString(R.string.widget_edittext), - R.drawable.ic_widget_edittext, - EditText.class)); - android.addChild( - new UIWidget( - getString(R.string.widget_image_button), - R.drawable.ic_widget_image, - ImageButton.class)); - android.addChild( - new UIWidget( - getString(R.string.widget_image_view), - R.drawable.ic_widget_image, - ImageView.class)); - android.addChild( - new UIWidget( - getString(R.string.widget_progressbar), - R.drawable.ic_widget_progress, - ProgressBar.class)); - android.addChild( - new UIWidget( - getString(R.string.widget_radio_button), - R.drawable.ic_widget_radio_button, - RadioButton.class)); - android.addChild( - new UIWidget( - getString(R.string.widget_seekbar), - R.drawable.ic_widget_seekbar, - SeekBar.class)); - android.addChild( - new UIWidget( - getString(R.string.widget_spinner), - R.drawable.ic_widget_spinner, - Spinner.class)); - android.addChild( - new UIWidget( - getString(R.string.widget_textview), - R.drawable.ic_widget_textview, - TextView.class)); - - // IMPORTANT: Do not select any other groups here - android.setSelected(true); - this.checkedWidgetCategory = android; - widgetGroups.add(android); - - final var layouts = new UIWidgetGroup(getString(R.string.widget_group_layouts)); - layouts.addChild( - new UIWidget( - getString(R.string.layout_linear), - R.drawable.ic_widget_linear, - LinearLayout.class)); - layouts.addChild( - new UIWidget( - getString(R.string.layout_relative), - R.drawable.ic_widget_relative, - RelativeLayout.class)); - - widgetGroups.add(layouts); - - final var adapter = new WidgetGroupItemAdapter(widgetGroups, this::showWidgetGroup); - mBinding.groupItems.setAdapter(adapter); - - showWidgetGroup(android); - } - - @SuppressLint("NotifyDataSetChanged") - private void showWidgetGroup(@NonNull UIWidgetGroup group) { - final var children = group.getChildren(); - this.mBinding.widgetItems.setAdapter(new WidgetItemAdapter(children, this)); - this.checkedWidgetCategory.setSelected(false); - this.checkedWidgetCategory = group; - this.checkedWidgetCategory.setSelected(true); - - TransitionManager.beginDelayedTransition( - mBinding.navigation, - new TransitionSet() - .addTransition(new Slide(Gravity.END)) - .addTransition(new Fade())); - - //noinspection ConstantConditions - this.mBinding.groupItems.getAdapter().notifyDataSetChanged(); - } - - @NonNull - @Contract(pure = true) - private ILayoutInflater.ContextProvider newContextProvider() { - return () -> DesignerActivity.this; - } - - @NonNull - private TextView createErrorText(Throwable th) { - final TextView error = new TextView(this); - error.setText(ThrowableUtils.getFullStackTrace(th)); - error.setLayoutParams(new ViewGroup.LayoutParams(-2, -2)); - error.setTextColor(ContextCompat.getColor(this, android.R.color.black)); - error.setBackgroundColor(ContextCompat.getColor(this, android.R.color.white)); - return error; - } - - @Override - public void onDragStarted(View view) { - mBinding.getRoot().closeDrawer(GravityCompat.START); - } - - @NonNull - @Contract("_ -> new") - private View.OnDragListener getOnDragListener(IViewGroup group) { - // WidgetDragListener cannot be reused - // This is because they keep a reference to the view group in which dragged view will be - // added - - return new WidgetDragListener(this, group, this::setupInflatedView); - } - - private void setDragDataToInflatedView(@NonNull IView view) { - view.setExtraData(new WidgetDragData(true, view, null)); - } - - private AttrEditorSheet getAttrEditorSheet() { - return this.mEditorSheet == null - ? mEditorSheet = new AttrEditorSheet().setLayout(this.layout) - : mEditorSheet; - } - - @Override - public boolean onDeletionFailed(@NonNull IView view) { - final var v = view.asView(); - if (mBinding.layoutContainer.indexOfChild(v) >= 0) { - mBinding.layoutContainer.removeView(v); - return true; - } - return false; - } + @Override + public Context getContext() { + return this; + } } diff --git a/app/src/main/java/com/itsaky/androidide/EditorActivity.java b/app/src/main/java/com/itsaky/androidide/EditorActivity.java index 90b449ff2..b8492c76e 100755 --- a/app/src/main/java/com/itsaky/androidide/EditorActivity.java +++ b/app/src/main/java/com/itsaky/androidide/EditorActivity.java @@ -108,7 +108,6 @@ import com.itsaky.androidide.viewmodel.EditorViewModel; import com.itsaky.androidide.views.MaterialBanner; import com.itsaky.androidide.views.SymbolInputView; import com.itsaky.androidide.views.editor.CodeEditorView; -import com.itsaky.inflater.ILayoutInflater; import com.itsaky.inflater.values.ValuesTableFactory; import com.itsaky.lsp.java.models.JavaServerConfiguration; import com.itsaky.lsp.java.models.JavaServerSettings; @@ -129,7 +128,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.regex.Pattern; @@ -1338,18 +1336,12 @@ public class EditorActivity extends StudioActivity } saveAll(false); - if (getApp().getLayoutInflater() == null) { - LOG.info("Creating layout inflater instance..."); - getApp().createInflater( - getApp().createInflaterConfig( - getContextProvider(), getResourceDirectories())); - } final Intent intent = new Intent(this, DesignerActivity.class); intent.putExtra( DesignerActivity.KEY_LAYOUT_PATH, getCurrentEditor().getFile().getAbsolutePath()); - + intent.putStringArrayListExtra(DesignerActivity.KEY_RES_DIRS, getResourceDirectories()); LOG.info("Launching UI Designer..."); mUIDesignerLauncher.launch(intent); } catch (Throwable th) { @@ -1359,14 +1351,8 @@ public class EditorActivity extends StudioActivity } @NonNull - @Contract(" -> new") - private ILayoutInflater.ContextProvider getContextProvider() { - return this::provide; - } - - @NonNull - private Set getResourceDirectories() { - final Set dirs = new HashSet<>(); + private ArrayList getResourceDirectories() { + final var dirs = new ArrayList(); if (getAndroidProject() != null && getAndroidProject().getModulePaths() != null && !getAndroidProject().getModulePaths().isEmpty()) { @@ -1374,7 +1360,7 @@ public class EditorActivity extends StudioActivity if (path != null && new File(path).exists()) { File res = new File(path, "src/main/res"); if (res.exists()) { - dirs.add(res); + dirs.add(res.getAbsolutePath()); } } } @@ -1412,7 +1398,9 @@ public class EditorActivity extends StudioActivity CompletableFuture.runAsync( () -> ValuesTableFactory.setupWithResDirectories( - getResourceDirectories().toArray(new File[0]))); + getResourceDirectories().stream() + .map(File::new) + .toArray(File[]::new))); } @Nullable diff --git a/app/src/main/java/com/itsaky/androidide/app/StudioApp.java b/app/src/main/java/com/itsaky/androidide/app/StudioApp.java index 9b2509e10..b947893b8 100755 --- a/app/src/main/java/com/itsaky/androidide/app/StudioApp.java +++ b/app/src/main/java/com/itsaky/androidide/app/StudioApp.java @@ -18,7 +18,9 @@ package com.itsaky.androidide.app; import android.content.Intent; + import androidx.annotation.NonNull; + import com.blankj.utilcode.util.ThrowableUtils; import com.google.firebase.messaging.FirebaseMessaging; import com.itsaky.androidide.CrashHandlerActivity; @@ -36,118 +38,122 @@ import com.itsaky.lsp.java.JavaLanguageServer; import com.itsaky.lsp.xml.XMLLanguageServer; import com.itsaky.sdk.SDKInfo; import com.itsaky.widgets.WidgetInfo; + import java.io.File; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; public class StudioApp extends BaseApplication { - private static final Logger LOG = Logger.instance("StudioApp"); - private static StudioApp instance; - private static SDKInfo sdkInfo; - private final ILanguageServer mJavaLanguageServer = new JavaLanguageServer(); - private final ILanguageServer mXMLLanguageServer = new XMLLanguageServer(); - private IResourceTable mResTable; - private ILayoutInflater mLayoutInflater; - private Thread.UncaughtExceptionHandler uncaughtExceptionHandler; + private static final Logger LOG = Logger.instance("StudioApp"); + private static StudioApp instance; + private static SDKInfo sdkInfo; + private final ILanguageServer mJavaLanguageServer = new JavaLanguageServer(); + private final ILanguageServer mXMLLanguageServer = new XMLLanguageServer(); + private IResourceTable mResTable; + private Thread.UncaughtExceptionHandler uncaughtExceptionHandler; - public static StudioApp getInstance() { - return instance; + public static StudioApp getInstance() { + return instance; + } + + @Override + public void onCreate() { + instance = this; + this.uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(this::handleCrash); + super.onCreate(); + + FirebaseMessaging.getInstance().subscribeToTopic(MessagingService.TOPIC_UPDATE); + FirebaseMessaging.getInstance().subscribeToTopic(MessagingService.TOPIC_DEV_MSGS); + + getApiInformation(); + } + + @NonNull + public ILanguageServer getJavaLanguageServer() { + return mJavaLanguageServer; + } + + @NonNull + public ILanguageServer getXMLLanguageServer() { + return mXMLLanguageServer; + } + + private void handleCrash(Thread thread, Throwable th) { + writeException(th); + + try { + final var intent = new Intent(); + intent.setAction(CrashHandlerActivity.REPORT_ACTION); + intent.putExtra(CrashHandlerActivity.TRACE_KEY, ThrowableUtils.getFullStackTrace(th)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + + if (this.uncaughtExceptionHandler != null) { + this.uncaughtExceptionHandler.uncaughtException(thread, th); + } + System.exit(1); + } catch (Throwable error) { + LOG.error("Unable to show crash handler activity", error); } + } - @Override - public void onCreate() { - instance = this; - this.uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); - Thread.setDefaultUncaughtExceptionHandler(this::handleCrash); - super.onCreate(); + public CompletableFuture createInflaterConfig( + ILayoutInflater.ContextProvider contextProvider, Set resDirs) { - FirebaseMessaging.getInstance().subscribeToTopic(MessagingService.TOPIC_UPDATE); - FirebaseMessaging.getInstance().subscribeToTopic(MessagingService.TOPIC_DEV_MSGS); - - initializeApiInformation(); - } - - @NonNull - public ILanguageServer getJavaLanguageServer() { - return mJavaLanguageServer; - } - - @NonNull - public ILanguageServer getXMLLanguageServer() { - return mXMLLanguageServer; - } - - private void handleCrash(Thread thread, Throwable th) { - writeException(th); - - try { - final var intent = new Intent(); - intent.setAction(CrashHandlerActivity.REPORT_ACTION); - intent.putExtra(CrashHandlerActivity.TRACE_KEY, ThrowableUtils.getFullStackTrace(th)); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - - if (this.uncaughtExceptionHandler != null) { - this.uncaughtExceptionHandler.uncaughtException(thread, th); - } - System.exit(1); - } catch (Throwable error) { - LOG.error("Unable to show crash handler activity", error); - } - } - - public void createInflater(LayoutInflaterConfiguration config) { - this.mLayoutInflater = ILayoutInflater.newInstance(config); - } - - public LayoutInflaterConfiguration createInflaterConfig( - ILayoutInflater.ContextProvider contextProvider, Set resDirs) { - return new LayoutInflaterConfiguration.Builder() - .setAttrInfo(this.attrInfo()) - .setWidgetInfo(this.widgetInfo()) - .setResourceFinder( - mResTable == null ? mResTable = new ProjectResourceTable() : mResTable) + return CompletableFuture.supplyAsync( + () -> { + try { + final var sdkInfo = getApiInformation().get(); + return new LayoutInflaterConfiguration.Builder() + .setAttrInfo(sdkInfo.getAttrInfo()) + .setWidgetInfo(sdkInfo.getWidgetInfo()) + .setResourceFinder(getResourceTable()) .setResourceDirectories(resDirs) .setContextProvider(contextProvider) .create(); - } + } catch (Throwable e) { + throw new CompletionException(e); + } + }); + } - public ILayoutInflater getLayoutInflater() { - return mLayoutInflater; - } + public IResourceTable getResourceTable() { + return mResTable == null ? mResTable = new ProjectResourceTable() : mResTable; + } - public IResourceTable getResourceTable() { - return this.mResTable; - } + /** Reads API version information from api-versions.xml */ + public CompletableFuture getApiInformation() { + return CompletableFuture.supplyAsync( + () -> { + if (sdkInfo == null) { + try { + sdkInfo = new SDKInfo(StudioApp.this); + ((XMLLanguageServer) mXMLLanguageServer).setupSDK(sdkInfo); + } catch (Throwable th) { + LOG.error(getString(R.string.err_init_sdkinfo), th); + } + } - /** Reads API version information from api-versions.xml */ - public void initializeApiInformation() { - new Thread( - () -> { - try { - sdkInfo = new SDKInfo(StudioApp.this); - ((XMLLanguageServer) mXMLLanguageServer).setupSDK(sdkInfo); - } catch (Throwable th) { - LOG.error(getString(R.string.err_init_sdkinfo), th); - } - }, - "SDKInfo Loader") - .start(); - } + return sdkInfo; + }); + } - public void stopAllDaemons() { - newShell(null).bgAppend("gradle --stop"); - } + public void stopAllDaemons() { + newShell(null).bgAppend("gradle --stop"); + } - public ApiInfo apiInfo() { - return sdkInfo.getApiInfo(); - } + public ApiInfo apiInfo() { + return sdkInfo.getApiInfo(); + } - public AttrInfo attrInfo() { - return sdkInfo.getAttrInfo(); - } + public AttrInfo attrInfo() { + return sdkInfo.getAttrInfo(); + } - public WidgetInfo widgetInfo() { - return sdkInfo.getWidgetInfo(); - } + public WidgetInfo widgetInfo() { + return sdkInfo.getWidgetInfo(); + } } diff --git a/app/src/main/java/com/itsaky/androidide/models/XMLAttribute.java b/app/src/main/java/com/itsaky/androidide/models/XMLAttribute.java index a978f07b3..c2163f4a1 100644 --- a/app/src/main/java/com/itsaky/androidide/models/XMLAttribute.java +++ b/app/src/main/java/com/itsaky/androidide/models/XMLAttribute.java @@ -19,15 +19,19 @@ package com.itsaky.androidide.models; import android.os.Parcel; import android.os.Parcelable; + import androidx.annotation.NonNull; + import com.itsaky.androidide.app.StudioApp; import com.itsaky.attrinfo.models.Attr; import com.itsaky.inflater.IAttribute; import com.itsaky.inflater.impl.UiAttribute; import com.itsaky.xml.INamespace; -import java.util.Objects; + import org.jetbrains.annotations.Contract; +import java.util.Objects; + /** * A model that is used to show attributes list of a view. * @@ -35,142 +39,140 @@ import org.jetbrains.annotations.Contract; */ public class XMLAttribute extends UiAttribute implements Parcelable { - public static final Creator CREATOR = - new Creator<>() { - @NonNull - @Contract("_ -> new") - @Override - public XMLAttribute createFromParcel(Parcel in) { - return new XMLAttribute(in); - } - - @NonNull - @Contract(value = "_ -> new", pure = true) - @Override - public XMLAttribute[] newArray(int size) { - return new XMLAttribute[size]; - } - }; - public Attr attr; - /** Is this attribute applied to any view? */ - private boolean isApplied; - /** - * Format of this attribute. May be stored here so we don't have to look in {@link - * com.itsaky.attrinfo.AttrInfo AttrInfo} again and again. - */ - private int format = -1; - - public XMLAttribute(IAttribute attribute) { - this(attribute, true); - } - - public XMLAttribute(@NonNull IAttribute attribute, boolean isApplied) { - this( - attribute.getNamespace(), - attribute.getAttributeName(), - attribute.getValue(), - isApplied); - } - - public XMLAttribute(INamespace namespace, String name, String value, boolean isApplied) { - super(namespace, name, value); - this.isApplied = isApplied; - } - - private XMLAttribute(@NonNull Parcel in) { - this( - in.readParcelable(INamespace.class.getClassLoader()), - in.readString(), - in.readString(), - in.readByte() != 0); - this.attr = in.readParcelable(Attr.class.getClassLoader()); - } - - /** - * Create a new attribute instance with the android namespace. {@link #isApplied()} will return - * true by default. - * - * @param name The name of the attribute. - * @param value The value of the attribute. - * @return A new instance of this class with the provided data. - */ - @NonNull - @Contract("_, _ -> new") - public static XMLAttribute newAndroidAttribute(String name, String value) { - return new XMLAttribute(INamespace.ANDROID, name, value, true); - } - - /** - * Create a new attribute instance with the android namespace. The {@link #isApplied()} will - * return {@code false} by default. - * - * @param name The name of the attribute. - * @return The new attribute instance. - */ - @NonNull - @Contract("_ -> new") - public static XMLAttribute newAndroidAttribute(String name) { - return new XMLAttribute(INamespace.ANDROID, name, "", false); - } - - public int getFormat() { - return format; - } - - public void setFormat(int format) { - this.format = format; - } - - public int findFormat() { - - if (getFormat() > 0) { - return getFormat(); + public static final Creator CREATOR = + new Creator<>() { + @NonNull + @Contract("_ -> new") + @Override + public XMLAttribute createFromParcel(Parcel in) { + return new XMLAttribute(in); } - final var attrs = StudioApp.getInstance().attrInfo(); - this.attr = attrs.getAttribute(getAttributeName()); - if (this.attr != null) { - this.format = this.attr.format; + @NonNull + @Contract(value = "_ -> new", pure = true) + @Override + public XMLAttribute[] newArray(int size) { + return new XMLAttribute[size]; } + }; + public Attr attr; + /** Is this attribute applied to any view? */ + private boolean isApplied; + /** + * Format of this attribute. May be stored here so we don't have to look in {@link + * com.itsaky.attrinfo.AttrInfo AttrInfo} again and again. + */ + private int format = -1; - return getFormat(); + public XMLAttribute(IAttribute attribute) { + this(attribute, true); + } + + public XMLAttribute(@NonNull IAttribute attribute, boolean isApplied) { + this(attribute.getNamespace(), attribute.getAttributeName(), attribute.getValue(), isApplied); + } + + public XMLAttribute(INamespace namespace, String name, String value, boolean isApplied) { + super(namespace, name, value); + this.isApplied = isApplied; + } + + private XMLAttribute(@NonNull Parcel in) { + this( + in.readParcelable(INamespace.class.getClassLoader()), + in.readString(), + in.readString(), + in.readByte() != 0); + this.attr = in.readParcelable(Attr.class.getClassLoader()); + this.format = in.readInt(); + } + + /** + * Create a new attribute instance with the android namespace. {@link #isApplied()} will return + * true by default. + * + * @param name The name of the attribute. + * @param value The value of the attribute. + * @return A new instance of this class with the provided data. + */ + @NonNull + @Contract("_, _ -> new") + public static XMLAttribute newAndroidAttribute(String name, String value) { + return new XMLAttribute(INamespace.ANDROID, name, value, true); + } + + /** + * Create a new attribute instance with the android namespace. The {@link #isApplied()} will + * return {@code false} by default. + * + * @param name The name of the attribute. + * @return The new attribute instance. + */ + @NonNull + @Contract("_ -> new") + public static XMLAttribute newAndroidAttribute(String name) { + return new XMLAttribute(INamespace.ANDROID, name, "", false); + } + + public int getFormat() { + return format; + } + + public void setFormat(int format) { + this.format = format; + } + + public int findFormat() { + + if (getFormat() > 0) { + return getFormat(); } - public boolean hasFormat(int format) { - return (this.format & format) != 0; + final var attrs = StudioApp.getInstance().attrInfo(); + this.attr = attrs.getAttribute(getAttributeName()); + if (this.attr != null) { + this.format = this.attr.format; } - public Attr getAttr() { - return attr; - } + return getFormat(); + } - public void setAttr(Attr attr) { - Objects.requireNonNull(attr); - this.attr = attr; - setFormat(attr.format); - } + public boolean hasFormat(int format) { + return (this.format & format) != 0; + } - @Override - public void apply(String value) { - super.apply(value); - isApplied = true; - } + public Attr getAttr() { + return attr; + } - public boolean isApplied() { - return isApplied; - } + public void setAttr(Attr attr) { + Objects.requireNonNull(attr); + this.attr = attr; + setFormat(attr.format); + } - @Override - public int describeContents() { - return 0; - } + @Override + public void apply(String value) { + super.apply(value); + isApplied = true; + } - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeParcelable(getNamespace(), flags); - dest.writeString(getAttributeName()); - dest.writeString(getValue()); - dest.writeByte((byte) (isApplied ? 1 : 0)); - dest.writeParcelable(getAttr(), flags); - } + public boolean isApplied() { + return isApplied; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(getNamespace(), flags); + dest.writeString(getAttributeName()); + dest.writeString(getValue()); + dest.writeByte((byte) (isApplied ? 1 : 0)); + dest.writeParcelable(getAttr(), flags); + dest.writeInt(this.format); + } } diff --git a/xml-inflater/src/main/java/com/itsaky/inflater/ILayoutInflater.java b/xml-inflater/src/main/java/com/itsaky/inflater/ILayoutInflater.java index 44242b5c8..4f75a2870 100644 --- a/xml-inflater/src/main/java/com/itsaky/inflater/ILayoutInflater.java +++ b/xml-inflater/src/main/java/com/itsaky/inflater/ILayoutInflater.java @@ -19,11 +19,14 @@ package com.itsaky.inflater; import android.content.Context; import android.view.ViewGroup; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import com.blankj.utilcode.util.FileIOUtils; import com.blankj.utilcode.util.FileUtils; import com.itsaky.inflater.util.Preconditions; + import java.io.File; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -35,148 +38,148 @@ import java.util.regex.Pattern; */ public abstract class ILayoutInflater { - private static final Pattern XML_DECL = Pattern.compile("<\\?xml.*\\?>"); + private static final Pattern XML_DECL = Pattern.compile("<\\?xml.*\\?>"); - /** The file whose content is currently being inflated. */ - private File currentlyInflatingFile; + /** The file whose content is currently being inflated. */ + private File currentlyInflatingFile; - /** - * Acatual implementation of the layout inflation - * - * @param layout The layout to inflate - * @return The inflated view - * @throws InflateException when there was an error inflating the layout - */ - @NonNull - protected abstract IView doInflate(String layout, ViewGroup parent) throws InflateException; + /** + * Creates an instance of {@code ILayoutInflater} + * + * @param config The configuration of the LayoutInflater + * @return A new layout inflater instance + */ + @NonNull + public static ILayoutInflater newInstance(LayoutInflaterConfiguration config) { + return new XMLLayoutInflater(config); + } - /** - * Reset the {@link ContextProvider} of this inflater - * - * @param provider The new {@link ContextProvider} - */ - public abstract void resetContextProvider(ContextProvider provider); + /** + * Acatual implementation of the layout inflation + * + * @param layout The layout to inflate + * @return The inflated view + * @throws InflateException when there was an error inflating the layout + */ + @NonNull + protected abstract IView doInflate(String layout, ViewGroup parent) throws InflateException; - /** - * Register the inflate listener to this inflater. - * - *

NOTE: Do not register too much listeners as they may affect the inflation time. - * - * @param listener The listener to register. - */ - public abstract void registerInflateListener(IInflateListener listener); + /** + * Reset the {@link ContextProvider} of this inflater + * + * @param provider The new {@link ContextProvider} + */ + public abstract void resetContextProvider(ContextProvider provider); - /** - * Unregister the inflate listener if it is registered. - * - * @param listener The listener to unregister - */ - public abstract void unregisterListener(IInflateListener listener); + /** + * Register the inflate listener to this inflater. + * + *

NOTE: Do not register too much listeners as they may affect the inflation time. + * + * @param listener The listener to register. + */ + public abstract void registerInflateListener(IInflateListener listener); - /** - * Get the resource finder attached to this layout inflater. - * - * @return The attached resource finder. - */ - @NonNull - protected abstract IResourceTable requireResourceFinder(); + /** + * Unregister the inflate listener if it is registered. + * + * @param listener The listener to unregister + */ + public abstract void unregisterListener(IInflateListener listener); - /** - * Inflate the layout from the given file path - * - * @param path The file path to inflate layout from - * @return The inflated layout - * @throws InflateException when there was an error inflating the layout - */ - @NonNull - public IView inflatePath(String path, ViewGroup parent) throws InflateException { - Preconditions.assertNotBlank(path, "Layout file path is blank!"); - return inflate(new File(path), parent); + /** + * Get the resource finder attached to this layout inflater. + * + * @return The attached resource finder. + */ + @NonNull + protected abstract IResourceTable requireResourceTable(); + + /** + * Inflate the layout from the given file path + * + * @param path The file path to inflate layout from + * @return The inflated layout + * @throws InflateException when there was an error inflating the layout + */ + @NonNull + public IView inflatePath(String path, ViewGroup parent) throws InflateException { + Preconditions.assertNotBlank(path, "Layout file path is blank!"); + return inflate(new File(path), parent); + } + + /** + * Inflate the layout from the given file + * + * @param file The file to inflate layout from + * @return The inflated layout + * @throws InflateException when there was an error inflating the layout + */ + @NonNull + public IView inflate(File file, ViewGroup parent) throws InflateException { + + Preconditions.assertNotnull(file, "Cannot inflate null file"); + + // How can we inflate a non existent file? + if (!file.exists()) { + throw new InflateException("File does not exist!"); } - /** - * Inflate the layout from the given file - * - * @param file The file to inflate layout from - * @return The inflated layout - * @throws InflateException when there was an error inflating the layout - */ - @NonNull - public IView inflate(File file, ViewGroup parent) throws InflateException { - - Preconditions.assertNotnull(file, "Cannot inflate null file"); - - // How can we inflate a non existent file? - if (!file.exists()) { - throw new InflateException("File does not exist!"); - } - - if (!file.canRead()) { - throw new InflateException("Cannot read file. Permission denied!"); - } - - // Cannot inflate a non UTF-8 too. This will also be true if the file is empty. - if (!FileUtils.isUtf8(file)) { - throw new InflateException("File is not UTF-8 or is empty!"); - } - - requireResourceFinder().setInflatingFile(file); - this.currentlyInflatingFile = file; - return inflate(FileIOUtils.readFile2String(file), parent); + if (!file.canRead()) { + throw new InflateException("Cannot read file. Permission denied!"); } + // Cannot inflate a non UTF-8 too. This will also be true if the file is empty. + if (!FileUtils.isUtf8(file)) { + throw new InflateException("File is not UTF-8 or is empty!"); + } + + requireResourceTable().setInflatingFile(file); + this.currentlyInflatingFile = file; + return inflate(FileIOUtils.readFile2String(file), parent); + } + + /** + * Inflate the layout from the provided XML layout code + * + * @param layout The XML layout code + * @return The inflated layout + * @throws InflateException when there was an error inflating the layout + */ + @NonNull + protected IView inflate(String layout, ViewGroup parent) throws InflateException { + + Preconditions.assertNotBlank(layout, "Layout is blank!"); + + // Trim the layout code + layout = layout.trim(); + + final Matcher matcher = XML_DECL.matcher(layout); + if (matcher.find()) { + layout = layout.substring(matcher.end()).trim(); + } + + return doInflate(layout, parent); + } + + /** + * Get the currently inflating file. + * + * @return The file. + */ + @Nullable + protected File getFile() { + return currentlyInflatingFile; + } + + /** Provides a reference to {@link Context}. This will be used to create original view. */ + public interface ContextProvider { + /** - * Inflate the layout from the provided XML layout code + * Provide the context to this inflater. * - * @param layout The XML layout code - * @return The inflated layout - * @throws InflateException when there was an error inflating the layout + * @return The context */ - @NonNull - protected IView inflate(String layout, ViewGroup parent) throws InflateException { - - Preconditions.assertNotBlank(layout, "Layout is blank!"); - - // Trim the layout code - layout = layout.trim(); - - final Matcher matcher = XML_DECL.matcher(layout); - if (matcher.find()) { - layout = layout.substring(matcher.end()).trim(); - } - - return doInflate(layout, parent); - } - - /** - * Creates an instance of {@code ILayoutInflater} - * - * @param config The configuration of the LayoutInflater - * @return A new layout inflater instance - */ - @NonNull - public static ILayoutInflater newInstance(LayoutInflaterConfiguration config) { - return new XMLLayoutInflater(config); - } - - /** - * Get the currently inflating file. - * - * @return The file. - */ - @Nullable - protected File getFile() { - return currentlyInflatingFile; - } - - /** Provides a reference to {@link Context}. This will be used to create original view. */ - public static interface ContextProvider { - - /** - * Provide the context to this inflater. - * - * @return The context - */ - Context getContext(); - } + Context getContext(); + } } 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 a1ffc5a1c..a4913ebc3 100644 --- a/xml-inflater/src/main/java/com/itsaky/inflater/XMLLayoutInflater.java +++ b/xml-inflater/src/main/java/com/itsaky/inflater/XMLLayoutInflater.java @@ -130,7 +130,7 @@ class XMLLayoutInflater extends BaseLayoutInflater { @NonNull @Override - protected IResourceTable requireResourceFinder() { + protected IResourceTable requireResourceTable () { return this.resFinder; }