content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   *
   *   http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing,
   * software distributed under the License is distributed on an
   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   * KIND, either express or implied.  See the License for the
   * specific language governing permissions and limitations
   * under the License.
   */
  package org.apache.weex.ui.component;
  
  import android.app.Activity;
  import android.content.Context;
  import android.graphics.Rect;
  import android.support.annotation.NonNull;
  import android.text.Editable;
  import android.text.InputFilter;
  import android.text.InputType;
  import android.text.TextPaint;
  import android.text.TextUtils;
  import android.text.TextWatcher;
  import android.text.method.PasswordTransformationMethod;
  import android.util.Log;
  import android.util.TypedValue;
  import android.view.Gravity;
  import android.view.KeyEvent;
  import android.view.View;
  import android.view.inputmethod.EditorInfo;
  import android.view.inputmethod.InputMethodManager;
  import android.widget.EditText;
  import android.widget.TextView;
  
  import com.alibaba.fastjson.JSONObject;
  import org.apache.weex.WXSDKInstance;
  import org.apache.weex.WXSDKManager;
  import org.apache.weex.annotation.JSMethod;
  import org.apache.weex.bridge.WXBridgeManager;
  import org.apache.weex.common.Constants;
  import org.apache.weex.common.WXThread;
  import org.apache.weex.dom.CSSConstants;
  import org.apache.weex.dom.WXStyle;
  import org.apache.weex.layout.ContentBoxMeasurement;
  import org.apache.weex.layout.MeasureMode;
  import org.apache.weex.ui.action.BasicComponentData;
  import org.apache.weex.ui.component.helper.SoftKeyboardDetector;
  import org.apache.weex.ui.component.helper.WXTimeInputHelper;
  import org.apache.weex.ui.view.WXEditText;
  import org.apache.weex.utils.TypefaceUtil;
  import org.apache.weex.utils.WXLogUtils;
  import org.apache.weex.utils.WXResourceUtils;
  import org.apache.weex.utils.WXUtils;
  import org.apache.weex.utils.WXViewUtils;
  
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.regex.Pattern;
  import java.util.regex.PatternSyntaxException;
  
  import static org.apache.weex.dom.WXStyle.UNSET;
  
  /**
   * Created by sospartan on 7/11/16.
   */
  public abstract class AbstractEditComponent extends WXComponent<WXEditText> {
  
    private final InputMethodManager mInputMethodManager;
    private String mBeforeText = "";
    private boolean mAutoFocus;
    private String mType = "text";
    private String mMax = null;
    private String mMin = null;
    private String mLastValue = "";
    private int mEditorAction = EditorInfo.IME_ACTION_DONE;
    private String mReturnKeyType = null;
    private List<TextView.OnEditorActionListener> mEditorActionListeners;
    private boolean mListeningKeyboard = false;
    private SoftKeyboardDetector.Unregister mUnregister;
    private boolean mIgnoreNextOnInputEvent = false;
    private boolean mKeepSelectionIndex = false;
    private TextFormatter mFormatter = null;
    private List<TextWatcher> mTextChangedListeners;
    private TextWatcher mTextChangedEventDispatcher;
    private int mFormatRepeatCount = 0;
    private static final int MAX_TEXT_FORMAT_REPEAT = 3;
  
    private TextPaint mPaint = new TextPaint();
    private int mLineHeight = UNSET;
  
    public AbstractEditComponent(WXSDKInstance instance, WXVContainer parent, boolean isLazy, BasicComponentData basicComponentData) {
      super(instance, parent, isLazy, basicComponentData);
      mInputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
      setContentBoxMeasurement(new ContentBoxMeasurement() {
        /** uiThread = false **/
        @Override
        public void measureInternal(float width, float height, int widthMeasureMode, int heightMeasureMode) {
          if (CSSConstants.isUndefined(width) || widthMeasureMode == MeasureMode.UNSPECIFIED) {
            width = 0;
          }
          mMeasureWidth = width;
          mMeasureHeight = getMeasureHeight();
        }
  
        /** uiThread = false **/
        @Override
        public void layoutBefore() {
          updateStyleAndAttrs();
        }
  
        /** uiThread = false **/
        @Override
        public void layoutAfter(float computedWidth, float computedHeight) {
  
        }
      });
    }
  
    @Override
    protected void layoutDirectionDidChanged(boolean isRTL) {
      String alignStr = (String) getStyles().get(Constants.Name.TEXT_ALIGN);
      int textAlign = getTextAlign(alignStr);
      if (textAlign <= 0) {
        textAlign = Gravity.START;
      }
      if (getHostView() instanceof WXEditText) {
        getHostView().setGravity(textAlign | getVerticalGravity());
      }
    }
  
    protected final float getMeasuredLineHeight() {
      return mLineHeight != UNSET && mLineHeight > 0 ? mLineHeight : mPaint.getFontMetrics(null);
    }
  
    protected float getMeasureHeight() {
      return getMeasuredLineHeight();
    }
  
    protected void updateStyleAndAttrs() {
      if (getStyles().size() > 0) {
        int fontSize = UNSET, fontStyle = UNSET, fontWeight = UNSET;
        String fontFamily = null;
        if (getStyles().containsKey(Constants.Name.FONT_SIZE)) {
          fontSize = WXStyle.getFontSize(getStyles(),getViewPortWidth());
        }
  
        if (getStyles().containsKey(Constants.Name.FONT_FAMILY)) {
          fontFamily = WXStyle.getFontFamily(getStyles());
        }
  
        if (getStyles().containsKey(Constants.Name.FONT_STYLE)) {
          fontStyle = WXStyle.getFontStyle(getStyles());
        }
  
        if (getStyles().containsKey(Constants.Name.FONT_WEIGHT)) {
          fontWeight = WXStyle.getFontWeight(getStyles());
        }
  
        int lineHeight = WXStyle.getLineHeight(getStyles(),getViewPortWidth());
        if (lineHeight != UNSET)
          mLineHeight = lineHeight;
  
        if (fontSize != UNSET)
          mPaint.setTextSize(fontSize);
  
        if (fontFamily != null) {
          TypefaceUtil.applyFontStyle(mPaint, fontStyle, fontWeight, fontFamily);
        }
      }
    }
  
    @Override
    protected WXEditText initComponentHostView(@NonNull Context context) {
      final WXEditText inputView = new WXEditText(context);
      appleStyleAfterCreated(inputView);
      return inputView;
    }
  
    @Override
    protected void onHostViewInitialized(WXEditText host) {
      super.onHostViewInitialized(host);
      addFocusChangeListener(new OnFocusChangeListener() {
        @Override
        public void onFocusChange(boolean hasFocus) {
          if (!hasFocus) {
            decideSoftKeyboard();
          }
          setPseudoClassStatus(Constants.PSEUDO.FOCUS,hasFocus);
        }
      });
  
      addKeyboardListener(host);
    }
  
    @Override
    protected boolean isConsumeTouch() {
      //EditText always consume touch event except disabled.
      return !isDisabled();
    }
  
    private OnClickListener mOnClickListener = new OnClickListener() {
      @Override
      public void onHostViewClick() {
        switch (mType) {
          case Constants.Value.DATE:
            hideSoftKeyboard();
            if (getParent() != null) {
              getParent().interceptFocus();
            }
            WXTimeInputHelper.pickDate(mMax, mMin, AbstractEditComponent.this);
            break;
          case Constants.Value.TIME:
            hideSoftKeyboard();
            if (getParent() != null) {
              getParent().interceptFocus();
            }
            WXTimeInputHelper.pickTime(AbstractEditComponent.this);
            break;
        }
      }
    };
  
    private void applyOnClickListener() {
      addClickListener(mOnClickListener);
    }
  
  
    protected int getVerticalGravity(){
      return Gravity.CENTER_VERTICAL;
    }
  
    /**
     * Process view after created.
     *
     * @param editText
     */
    protected void appleStyleAfterCreated(final WXEditText editText) {
      String alignStr = (String) getStyles().get(Constants.Name.TEXT_ALIGN);
      int textAlign = getTextAlign(alignStr);
      if (textAlign <= 0) {
        textAlign = Gravity.START;
      }
      editText.setGravity(textAlign | getVerticalGravity());
      final int colorInt = WXResourceUtils.getColor("#999999");
      if (colorInt != Integer.MIN_VALUE) {
        editText.setHintTextColor(colorInt);
      }
  
      mTextChangedEventDispatcher = new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
          if (mTextChangedListeners != null) {
            for (TextWatcher watcher : mTextChangedListeners) {
              watcher.beforeTextChanged(s, start, count, after);
            }
          }
        }
  
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
          if (mFormatter != null) {
            String raw = mFormatter.recover(s.toString());
            String result = mFormatter.format(raw);
            // prevent infinite loop caused by bad format and recover regexp
            if (!result.equals(s.toString()) && mFormatRepeatCount < MAX_TEXT_FORMAT_REPEAT) {
              mFormatRepeatCount = mFormatRepeatCount + 1;
              int index = editText.getSelectionStart();
              int cursorIndex = mFormatter.format(mFormatter.recover(s.subSequence(0, index).toString())).length();
              editText.setText(result);
              editText.setSelection(cursorIndex);
              return;
            }
  
            mFormatRepeatCount = 0;
          }
  
          if (mTextChangedListeners != null) {
            for (TextWatcher watcher : mTextChangedListeners) {
              watcher.onTextChanged(s, start, before, count);
            }
          }
        }
  
        @Override
        public void afterTextChanged(Editable s) {
          if (mTextChangedListeners != null) {
            for (TextWatcher watcher : mTextChangedListeners) {
              watcher.afterTextChanged(s);
            }
          }
        }
      };
      editText.addTextChangedListener(mTextChangedEventDispatcher);
  
      editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, WXStyle.getFontSize(getStyles(), getInstance().getInstanceViewPortWidth()));
    }
  
  
    @Override
    public void addEvent(final String type) {
      super.addEvent(type);
      if (getHostView() == null || TextUtils.isEmpty(type)) {
        return;
      }
      final TextView text = getHostView();
  
      if (type.equals(Constants.Event.CHANGE)) {
        addFocusChangeListener(new OnFocusChangeListener() {
          @Override
          public void onFocusChange(boolean hasFocus) {
            if (hasFocus) {
              mLastValue = text.getText().toString();
            } else {
              CharSequence newValue = text.getText();
              newValue = newValue == null ? "" : newValue;
              if (!newValue.toString().equals(mLastValue)) {
                fireEvent(Constants.Event.CHANGE, newValue.toString());
                mLastValue = text.getText().toString();
              }
            }
          }
        });
  
        addEditorActionListener(new TextView.OnEditorActionListener() {
          @Override
          public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
            if (actionId == mEditorAction) {
              CharSequence newValue = text.getText();
              newValue = newValue == null ? "" : newValue;
              if (!newValue.toString().equals(mLastValue)) {
                fireEvent(Constants.Event.CHANGE, newValue.toString());
                mLastValue = text.getText().toString();
              }
              if (getParent() != null) {
                getParent().interceptFocus();
              }
              hideSoftKeyboard();
              return true;
            }
            return false;
          }
        });
      } else if (type.equals(Constants.Event.INPUT)) {
        addTextChangedListener(new TextWatcher() {
          @Override
          public void beforeTextChanged(CharSequence s, int start, int count, int after) {
  
          }
  
          @Override
          public void onTextChanged(CharSequence s, int start, int before, int count) {
            if (mIgnoreNextOnInputEvent) {
              mIgnoreNextOnInputEvent = false;
              mBeforeText = s.toString();
              return;
            }
  
            if (mBeforeText.equals(s.toString())) {
              return;
            }
  
            mBeforeText = s.toString();
  
            fireEvent(Constants.Event.INPUT, s.toString());
          }
  
          @Override
          public void afterTextChanged(Editable s) {
  
          }
        });
      }
  
      if (Constants.Event.RETURN.equals(type)) {
        addEditorActionListener(new TextView.OnEditorActionListener() {
          @Override
          public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
            if (actionId == mEditorAction) {
              Map<String, Object> ret = new HashMap<>(2);
              ret.put("returnKeyType", mReturnKeyType);
              ret.put("value", v.getText().toString());
              fireEvent(Constants.Event.RETURN, ret);
              return true;
            }
            return false;
          }
        });
      }
  
      if (Constants.Event.KEYBOARD.equals(type)) {
        mListeningKeyboard = true;
      }
    }
  
    private void fireEvent(String event, String value) {
      if (event != null) {
        Map<String, Object> params = new HashMap<>(2);
        params.put("value", value);
        params.put("timeStamp", System.currentTimeMillis());
  
        Map<String, Object> domChanges = new HashMap<>();
        Map<String, Object> attrsChanges = new HashMap<>();
        attrsChanges.put("value", value);
        domChanges.put("attrs", attrsChanges);
  
        WXSDKManager.getInstance().fireEvent(getInstanceId(), getRef(), event, params, domChanges);
      }
    }
  
    public void performOnChange(String value) {
      if (getEvents() != null) {
        String event = getEvents().contains(Constants.Event.CHANGE) ? Constants.Event.CHANGE : null;
        fireEvent(event, value);
      }
    }
  
    @Override
    protected boolean setProperty(String key, Object param) {
      switch (key) {
        case Constants.Name.DISABLED:
          Boolean disabled = WXUtils.getBoolean(param, null);
          if (disabled != null && mHost != null) {
            if (disabled) {
              mHost.setFocusable(false);
              mHost.setFocusableInTouchMode(false);
            } else {
              mHost.setFocusableInTouchMode(true);
              mHost.setFocusable(true);
            }
          }
          return true;
        case Constants.Name.PLACEHOLDER:
          String placeholder = WXUtils.getString(param, null);
          if (placeholder != null)
            setPlaceholder(placeholder);
          return true;
        case Constants.Name.PLACEHOLDER_COLOR:
          String placeholder_color = WXUtils.getString(param, null);
          if (placeholder_color != null)
            setPlaceholderColor(placeholder_color);
          return true;
        case Constants.Name.TYPE:
          String input_type = WXUtils.getString(param, null);
          if (input_type != null)
            setType(input_type);
          return true;
        case Constants.Name.AUTOFOCUS:
          Boolean result = WXUtils.getBoolean(param, null);
          if (result != null)
            setAutofocus(result);
          return true;
        case Constants.Name.COLOR:
          String color = WXUtils.getString(param, null);
          if (color != null)
            setColor(color);
          return true;
        case Constants.Name.FONT_SIZE:
          String fontsize = WXUtils.getString(param, null);
          if (fontsize != null)
            setFontSize(fontsize);
          return true;
        case Constants.Name.TEXT_ALIGN:
          String text_align = WXUtils.getString(param, null);
          if (text_align != null)
            setTextAlign(text_align);
          return true;
        case Constants.Name.SINGLELINE:
          Boolean singLineResult = WXUtils.getBoolean(param, null);
          if (singLineResult != null)
            setSingleLine(singLineResult);
          return true;
        case Constants.Name.LINES:
          Integer lines = WXUtils.getInteger(param, null);
          if (lines != null)
            setLines(lines);
          return true;
        case Constants.Name.MAX_LENGTH:
          Integer maxlength = WXUtils.getInteger(param, null);
          if (maxlength != null)
            setMaxLength(maxlength);
          return true;
        case Constants.Name.MAXLENGTH:
          Integer maxLength = WXUtils.getInteger(param, null);
          if (maxLength != null)
            setMaxLength(maxLength);
          return true;
        case Constants.Name.MAX:
          setMax(String.valueOf(param));
          return true;
        case Constants.Name.MIN:
          setMin(String.valueOf(param));
          return true;
        case Constants.Name.RETURN_KEY_TYPE:
          setReturnKeyType(String.valueOf(param));
          return true;
        case Constants.Name.KEEP_SELECTION_INDEX:
          boolean keepIndex = WXUtils.getBoolean(param, false);
          mKeepSelectionIndex = keepIndex;
          return true;
        case Constants.Name.ALLOW_COPY_PASTE:
          boolean allowCopyPaste = WXUtils.getBoolean(param, true);
          if (getHostView() != null) {
            getHostView().setAllowCopyPaste(allowCopyPaste);
          }
          return true;
      }
      return super.setProperty(key, param);
    }
  
    @WXComponentProp(name = Constants.Name.RETURN_KEY_TYPE)
    public void setReturnKeyType(String type) {
      if (getHostView() == null) {
        return;
      }
      mReturnKeyType = type;
      switch (type) {
        case ReturnTypes.DEFAULT:
          mEditorAction = EditorInfo.IME_ACTION_UNSPECIFIED;
          break;
        case ReturnTypes.GO:
          mEditorAction = EditorInfo.IME_ACTION_GO;
          break;
        case ReturnTypes.NEXT:
          mEditorAction = EditorInfo.IME_ACTION_NEXT;
          break;
        case ReturnTypes.SEARCH:
          mEditorAction = EditorInfo.IME_ACTION_SEARCH;
          break;
        case ReturnTypes.SEND:
          mEditorAction = EditorInfo.IME_ACTION_SEND;
          break;
        case ReturnTypes.DONE:
          mEditorAction = EditorInfo.IME_ACTION_DONE;
          break;
        default:
          break;
      }
  
      //remove focus and hide keyboard first, the ImeOptions will take effect when show keyboard next time
      blur();
      getHostView().setImeOptions(mEditorAction);
    }
  
    @WXComponentProp(name = Constants.Name.PLACEHOLDER)
    public void setPlaceholder(String placeholder) {
      if (placeholder == null || getHostView() == null) {
        return;
      }
      ((WXEditText) getHostView()).setHint(placeholder);
    }
  
    @WXComponentProp(name = Constants.Name.PLACEHOLDER_COLOR)
    public void setPlaceholderColor(String color) {
      if (getHostView() != null && !TextUtils.isEmpty(color)) {
        int colorInt = WXResourceUtils.getColor(color);
        if (colorInt != Integer.MIN_VALUE) {
          ((WXEditText) getHostView()).setHintTextColor(colorInt);
        }
      }
    }
  
    @WXComponentProp(name = Constants.Name.TYPE)
    public void setType(String type) {
      Log.e("weex", "setType=" + type);
      if (type == null || getHostView() == null) {
        return;
      }
      mType = type;
      ((EditText) getHostView()).setRawInputType(getInputType(mType));
      switch (mType) {
        case Constants.Value.DATE:
        case Constants.Value.TIME:
          applyOnClickListener();
          break;
      }
    }
  
    @WXComponentProp(name = Constants.Name.AUTOFOCUS)
    public void setAutofocus(boolean autofocus) {
      if (getHostView() == null) {
        return;
      }
      mAutoFocus = autofocus;
      EditText inputView = getHostView();
      if (mAutoFocus) {
        inputView.setFocusable(true);
        inputView.requestFocus();
        inputView.setFocusableInTouchMode(true);
        showSoftKeyboard();
      } else {
        hideSoftKeyboard();
      }
    }
  
    @WXComponentProp(name = Constants.Name.VALUE)
    public void setValue(String value) {
      WXEditText view;
      if ((view = getHostView()) == null) {
        return;
      }
      if (TextUtils.equals(view.getText(), value)) {
        return;
      }
  
      mIgnoreNextOnInputEvent = true;
      int oldIndex = view.getSelectionStart();
      view.setText(value);
      int index = mKeepSelectionIndex ? oldIndex : value.length();
      view.setSelection(value == null ? 0 : index);
    }
  
    @WXComponentProp(name = Constants.Name.COLOR)
    public void setColor(String color) {
      if (getHostView() != null && !TextUtils.isEmpty(color)) {
        int colorInt = WXResourceUtils.getColor(color);
        if (colorInt != Integer.MIN_VALUE) {
          getHostView().setTextColor(colorInt);
        }
      }
    }
  
    @WXComponentProp(name = Constants.Name.FONT_SIZE)
    public void setFontSize(String fontSize) {
      if (getHostView() != null && fontSize != null ) {
        Map<String, Object> map = new HashMap<>(1);
        map.put(Constants.Name.FONT_SIZE, fontSize);
        getHostView().setTextSize(TypedValue.COMPLEX_UNIT_PX, WXStyle.getFontSize(map, getInstance().getInstanceViewPortWidth()));
      }
    }
  
    @WXComponentProp(name = Constants.Name.TEXT_ALIGN)
    public void setTextAlign(String textAlign) {
      int align = getTextAlign(textAlign);
      if (align > 0) {
        getHostView().setGravity(align | getVerticalGravity());
      }
    }
  
    @WXComponentProp(name = Constants.Name.SINGLELINE)
    public void setSingleLine(boolean singleLine) {
      if (getHostView() == null) {
        return;
      }
      getHostView().setSingleLine(singleLine);
    }
  
    @WXComponentProp(name = Constants.Name.LINES)
    public void setLines(int lines) {
      if (getHostView() == null) {
        return;
      }
      getHostView().setLines(lines);
    }
  
    /**
     * Compatible with both 'max-length' and 'maxlength'
     * @param maxLength
     */
    @WXComponentProp(name = Constants.Name.MAX_LENGTH)
    public void setMaxLength(int maxLength) {
      if (getHostView() == null) {
        return;
      }
      getHostView().setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)});
    }
  
    /**
     * Compatible with both 'max-length' and 'maxlength'
     * @param maxLength
     */
    @WXComponentProp(name = Constants.Name.MAXLENGTH)
    @Deprecated
    public void setMaxlength(int maxLength) {
      setMaxLength(maxLength);
    }
  
    private int getInputType(String type) {
      int inputType;
      switch (type) {
        case Constants.Value.TEXT:
          inputType = InputType.TYPE_CLASS_TEXT;
          break;
        case Constants.Value.DATE:
          inputType = InputType.TYPE_NULL;
          getHostView().setFocusable(false);
          break;
        case Constants.Value.DATETIME:
          inputType = InputType.TYPE_CLASS_DATETIME;
          break;
        case Constants.Value.EMAIL:
          inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
          break;
        case Constants.Value.PASSWORD:
          inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD;
          if(getHostView() != null){
              getHostView().setTransformationMethod(PasswordTransformationMethod.getInstance());
          }
          break;
        case Constants.Value.TEL:
          inputType = InputType.TYPE_CLASS_PHONE;
          break;
        case Constants.Value.TIME:
          inputType = InputType.TYPE_NULL;
          if(getHostView() != null){
              getHostView().setFocusable(false);
          }
          break;
        case Constants.Value.URL:
          inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI;
          break;
        case Constants.Value.NUMBER:
          inputType = InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL;
          break;
        default:
          inputType = InputType.TYPE_CLASS_TEXT;
      }
      return inputType;
    }
  
    @WXComponentProp(name = Constants.Name.MAX)
    public void setMax(String max) {
      mMax = max;
    }
  
    @WXComponentProp(name = Constants.Name.MIN)
    public void setMin(String min) {
      mMin = min;
    }
  
    private boolean showSoftKeyboard() {
      if (getHostView() == null) {
        return false;
      } else {
        getHostView().postDelayed(WXThread.secure(new Runnable() {
          @Override
          public void run() {
            if (getInstance() != null && getInstance().getApmForInstance() != null){
              getInstance().getApmForInstance().forceStopRecordInteraction = true;
            }
            mInputMethodManager.showSoftInput(getHostView(), InputMethodManager.SHOW_IMPLICIT);
          }
        }), 100);
      }
      return true;
    }
  
    private void hideSoftKeyboard() {
      if (getHostView() != null) {
        getHostView().postDelayed(WXThread.secure(new Runnable() {
          @Override
          public void run() {
            mInputMethodManager.hideSoftInputFromWindow(getHostView().getWindowToken(), 0);
          }
        }), 16);
      }
    }
  
    private int getTextAlign(String textAlign) {
      boolean isRTL = isLayoutRTL();
      int align = isRTL ? Gravity.END : Gravity.START;
      if (TextUtils.isEmpty(textAlign)) {
        return align;
      }
  
      if (textAlign.equals(Constants.Value.LEFT)) {
        align = Gravity.START;
      } else if (textAlign.equals(Constants.Value.CENTER)) {
        align = Gravity.CENTER;
      } else if (textAlign.equals(Constants.Value.RIGHT)) {
        align = Gravity.END;
      }
      return align;
    }
  
    @JSMethod
    public void blur() {
      WXEditText host = getHostView();
      if (host != null && host.hasFocus()) {
        if (getParent() != null) {
          getParent().interceptFocus();
        }
        host.clearFocus();
        hideSoftKeyboard();
      }
    }
  
    @JSMethod
    public void focus() {
      WXEditText host = getHostView();
      if (host != null && !host.hasFocus()) {
        if (getParent() != null) {
          getParent().ignoreFocus();
        }
        host.requestFocus();
        host.setFocusable(true);
        host.setFocusableInTouchMode(true);
        showSoftKeyboard();
      }
    }
  
    @Override
    protected Object convertEmptyProperty(String propName, Object originalValue) {
      switch (propName) {
        case Constants.Name.FONT_SIZE:
          return WXText.sDEFAULT_SIZE;
        case Constants.Name.COLOR:
          return "black";
      }
      return super.convertEmptyProperty(propName, originalValue);
    }
  
    private void decideSoftKeyboard() {
      View hostView;
      if ((hostView = getHostView()) != null) {
        final Context context = getContext();
        if (context != null && context instanceof Activity) {
          hostView.postDelayed(WXThread.secure(new Runnable() {
            @Override
            public void run() {
              View currentFocus = ((Activity) context).getCurrentFocus();
              if (!(currentFocus instanceof EditText)) {
                mInputMethodManager.hideSoftInputFromWindow(getHostView().getWindowToken(), 0);
              }
            }
          }), 16);
        }
      }
    }
  
    @JSMethod
    public void setSelectionRange(int selectionStart, int selectionEnd) {
      EditText hostView;
      if ((hostView = getHostView()) != null) {
        int length = getHostView().length();
        if (selectionStart > length || selectionEnd > length) {
          return;
        }
        focus();
        hostView.setSelection(selectionStart, selectionEnd);
      }
    }
  
    @JSMethod
    public void getSelectionRange(String callbackId) {
      EditText hostView;
      Map<String, Object> result = new HashMap<>(2);
      if ((hostView = getHostView()) != null) {
        int start = hostView.getSelectionStart();
        int end = hostView.getSelectionEnd();
  
        if (!hostView.hasFocus()) {
          //The default behavior, same as iOS and web
          start = 0;
          end = 0;
        }
  
        result.put(Constants.Name.SELECTION_START, start);
        result.put(Constants.Name.SELECTION_END, end);
      }
      WXBridgeManager.getInstance().callback(getInstanceId(), callbackId, result, false);
    }
  
    @JSMethod
    public void setTextFormatter(JSONObject params) {
      try {
        String formatRule = params.getString("formatRule");
        String formatReplace = params.getString("formatReplace");
        String recoverRule = params.getString("recoverRule");
        String recoverReplace = params.getString("recoverReplace");
  
        PatternWrapper format = parseToPattern(formatRule, formatReplace);
        PatternWrapper recover = parseToPattern(recoverRule, recoverReplace);
  
        if (format != null && recover != null) {
          mFormatter = new TextFormatter(format, recover);
        }
      } catch (Throwable t) {
        t.printStackTrace();
      }
    }
  
    protected final void addEditorActionListener(TextView.OnEditorActionListener listener) {
      TextView view;
      if (listener != null && (view = getHostView()) != null) {
        if (mEditorActionListeners == null) {
          mEditorActionListeners = new ArrayList<>();
          view.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            private boolean handled = true;
  
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
              for (TextView.OnEditorActionListener l : mEditorActionListeners) {
                if (l != null) {
                  handled = handled & l.onEditorAction(v, actionId, event);
                }
              }
              return handled;
            }
          });
        }
        mEditorActionListeners.add(listener);
      }
    }
  
    public final void addTextChangedListener(TextWatcher watcher) {
      if (mTextChangedListeners == null) {
        mTextChangedListeners = new ArrayList<>();
      }
      mTextChangedListeners.add(watcher);
    }
  
    private void addKeyboardListener(final WXEditText host) {
      if (host == null) {
        return;
      }
      final Context context = host.getContext();
      if (context != null && context instanceof Activity) {
        mUnregister = SoftKeyboardDetector.registerKeyboardEventListener((Activity) context, new SoftKeyboardDetector.OnKeyboardEventListener() {
          @Override
          public void onKeyboardEvent(boolean isShown) {
            if (mListeningKeyboard) {
              Map<String, Object> event = new HashMap<>(1);
              event.put("isShow", isShown);
              if (isShown) {
                Rect r = new Rect();
                ((Activity) context).getWindow().getDecorView().getWindowVisibleDisplayFrame(r);
                float keyboardSize = WXViewUtils.getWebPxByWidth(WXViewUtils.getScreenHeight(context) - (r.bottom - r.top),
                        getInstance().getInstanceViewPortWidth());
                event.put("keyboardSize", keyboardSize);
              }
              fireEvent(Constants.Event.KEYBOARD, event);
            }
            if (!isShown) {
              blur();
            }
          }
        });
      }
    }
  
    @Override
    public void destroy() {
      super.destroy();
      if (mUnregister != null) {
        try {
          mUnregister.execute();
          mUnregister = null;
        } catch (Throwable throwable) {
          WXLogUtils.w("Unregister throw ", throwable);
        }
      }
    }
  
    private PatternWrapper parseToPattern(String jsPattern, String replace) {
      if (jsPattern == null || replace == null) {
        return null;
      }
  
      String checker = "/[\\S]+/[i]?[m]?[g]?";
      if (!Pattern.compile(checker).matcher(jsPattern).matches()) {
        WXLogUtils.w("WXInput", "Illegal js pattern syntax: " + jsPattern);
        return null;
      }
  
      int flags = 0;
      boolean global = false;
      String flagsStr = jsPattern.substring(jsPattern.lastIndexOf("/") + 1);
      String regExp = jsPattern.substring(jsPattern.indexOf("/") + 1, jsPattern.lastIndexOf("/"));
  
      if (flagsStr.contains("i")) {
        flags |= Pattern.CASE_INSENSITIVE;
      }
  
      if (flagsStr.contains("m")) {
        flags |= Pattern.DOTALL;
      }
  
      if (flagsStr.contains("g")) {
        global = true;
      }
  
      Pattern pattern = null;
      try {
        pattern = Pattern.compile(regExp, flags);
      } catch (PatternSyntaxException e) {
        WXLogUtils.w("WXInput", "Pattern syntax error: " + regExp);
      }
      if (pattern == null) {
        return null;
      }
  
      PatternWrapper wrapper = new PatternWrapper();
      wrapper.global = global;
      wrapper.matcher = pattern;
      wrapper.replace = replace;
      return wrapper;
    }
  
    private interface ReturnTypes {
      String DEFAULT = "default";
      String GO = "go";
      String NEXT = "next";
      String SEARCH = "search";
      String SEND = "send";
      String DONE = "done";
    }
  
    private static class PatternWrapper {
      private boolean global = false;
      private Pattern matcher;
      private String replace;
    }
  
    private static class TextFormatter {
      private PatternWrapper format;
      private PatternWrapper recover;
  
      private TextFormatter(PatternWrapper format, PatternWrapper recover) {
        this.format = format;
        this.recover = recover;
      }
  
      private String format(String src) {
        try {
          if (format != null) {
            if (format.global) {
              return format.matcher.matcher(src).replaceAll(format.replace);
            } else {
              return format.matcher.matcher(src).replaceFirst(format.replace);
            }
          }
        } catch (Throwable t) {
          //maybe IndexOutOfBoundsException caused by illegal replace
          WXLogUtils.w("WXInput", "[format] " + t.getMessage());
        }
        return src;
      }
  
      private String recover(String formatted) {
        try {
          if (recover != null) {
            if (recover.global) {
              return recover.matcher.matcher(formatted).replaceAll(recover.replace);
            } else {
              return recover.matcher.matcher(formatted).replaceFirst(recover.replace);
            }
          }
        } catch (Throwable t) {
          //same cause as format
          WXLogUtils.w("WXInput", "[formatted] " + t.getMessage());
        }
        return formatted;
      }
    }
  }
  