content: /*
   * Copyright (C) 2011 The Android Open Source Project
   *
   * Licensed 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 com.amnesica.kryptey.inputmethod.keyboard;
  
  import android.content.Context;
  import android.content.res.Resources;
  import android.content.res.TypedArray;
  import android.content.res.XmlResourceParser;
  import android.text.InputType;
  import android.util.Log;
  import android.util.SparseArray;
  import android.util.Xml;
  import android.view.inputmethod.EditorInfo;
  
  import com.amnesica.kryptey.inputmethod.R;
  import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardBuilder;
  import com.amnesica.kryptey.inputmethod.keyboard.internal.KeyboardParams;
  import com.amnesica.kryptey.inputmethod.keyboard.internal.UniqueKeysCache;
  import com.amnesica.kryptey.inputmethod.latin.Subtype;
  import com.amnesica.kryptey.inputmethod.latin.utils.InputTypeUtils;
  import com.amnesica.kryptey.inputmethod.latin.utils.XmlParseUtils;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  
  import java.io.IOException;
  import java.lang.ref.SoftReference;
  import java.util.HashMap;
  
  /**
   * This class represents a set of keyboard layouts. Each of them represents a different keyboard
   * specific to a keyboard state, such as alphabet, symbols, and so on.  Layouts in the same
   * {@link KeyboardLayoutSet} are related to each other.
   * A {@link KeyboardLayoutSet} needs to be created for each
   * {@link android.view.inputmethod.EditorInfo}.
   */
  public final class KeyboardLayoutSet {
    private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
    private static final boolean DEBUG_CACHE = false;
  
    private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
    private static final String TAG_ELEMENT = "Element";
  
    private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
  
    private final Context mContext;
    private final Params mParams;
  
    // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
    // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
    // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
    private static final int FORCIBLE_CACHE_SIZE = 4;
    // By construction of soft references, anything that is also referenced somewhere else
    // will stay in the cache. So we forcibly keep some references in an array to prevent
    // them from disappearing from sKeyboardCache.
    private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
    private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
        new HashMap<>();
    private static final UniqueKeysCache sUniqueKeysCache = UniqueKeysCache.newInstance();
  
    @SuppressWarnings("serial")
    public static final class KeyboardLayoutSetException extends RuntimeException {
      public final KeyboardId mKeyboardId;
  
      public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
        super(cause);
        mKeyboardId = keyboardId;
      }
    }
  
    private static final class ElementParams {
      int mKeyboardXmlId;
      boolean mAllowRedundantMoreKeys;
  
      public ElementParams() {
      }
    }
  
    public static final class Params {
      String mKeyboardLayoutSetName;
      int mMode;
      // TODO: Use {@link InputAttributes} instead of these variables.
      EditorInfo mEditorInfo;
      boolean mLanguageSwitchKeyEnabled;
      Subtype mSubtype;
      int mKeyboardThemeId;
      int mKeyboardWidth;
      int mKeyboardHeight;
      boolean mShowMoreKeys;
      boolean mShowNumberRow;
      // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
      final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
          new SparseArray<>();
    }
  
    public static void onSystemLocaleChanged() {
      clearKeyboardCache();
    }
  
    public static void onKeyboardThemeChanged() {
      clearKeyboardCache();
    }
  
    private static void clearKeyboardCache() {
      sKeyboardCache.clear();
      sUniqueKeysCache.clear();
    }
  
    KeyboardLayoutSet(final Context context, final Params params) {
      mContext = context;
      mParams = params;
    }
  
    public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
      final int keyboardLayoutSetElementId;
      switch (mParams.mMode) {
        case KeyboardId.MODE_PHONE:
          if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
            keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
          } else {
            keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
          }
          break;
        case KeyboardId.MODE_NUMBER:
        case KeyboardId.MODE_DATE:
        case KeyboardId.MODE_TIME:
        case KeyboardId.MODE_DATETIME:
          keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
          break;
        default:
          keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
          break;
      }
  
      ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
          keyboardLayoutSetElementId);
      if (elementParams == null) {
        elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
            KeyboardId.ELEMENT_ALPHABET);
      }
      // Note: The keyboard for each shift state, and mode are represented as an elementName
      // attribute in a keyboard_layout_set XML file.  Also each keyboard layout XML resource is
      // specified as an elementKeyboard attribute in the file.
      // The KeyboardId is an internal key for a Keyboard object.
  
      final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
      return getKeyboard(elementParams, id);
    }
  
    private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
      final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
      final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
      if (cachedKeyboard != null) {
        if (DEBUG_CACHE) {
          Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
        }
        return cachedKeyboard;
      }
  
      final KeyboardBuilder<KeyboardParams> builder =
          new KeyboardBuilder<>(mContext, new KeyboardParams(sUniqueKeysCache));
      sUniqueKeysCache.setEnabled(id.isAlphabetKeyboard());
      builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys);
      final int keyboardXmlId = elementParams.mKeyboardXmlId;
      builder.load(keyboardXmlId, id);
      final Keyboard keyboard = builder.build();
      sKeyboardCache.put(id, new SoftReference<>(keyboard));
      if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
          || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)) {
        // We only forcibly cache the primary, "ALPHABET", layouts.
        for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
          sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
        }
        sForcibleKeyboardCache[0] = keyboard;
        if (DEBUG_CACHE) {
          Log.d(TAG, "forcing caching of keyboard with id=" + id);
        }
      }
      if (DEBUG_CACHE) {
        Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
            + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
      }
      return keyboard;
    }
  
    public static final class Builder {
      private final Context mContext;
      private final Resources mResources;
  
      private final Params mParams = new Params();
  
      private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
  
      public Builder(final Context context, final EditorInfo ei) {
        mContext = context;
        mResources = context.getResources();
        final Params params = mParams;
  
        final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO;
        params.mMode = getKeyboardMode(editorInfo);
        // TODO: Consolidate those with {@link InputAttributes}.
        params.mEditorInfo = editorInfo;
      }
  
      public Builder setKeyboardTheme(final int themeId) {
        mParams.mKeyboardThemeId = themeId;
        return this;
      }
  
      public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
        mParams.mKeyboardWidth = keyboardWidth;
        mParams.mKeyboardHeight = keyboardHeight;
        return this;
      }
  
      public Builder setSubtype(final Subtype subtype) {
        // TODO: Consolidate with {@link InputAttributes}.
        mParams.mSubtype = subtype;
        mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
            + subtype.getKeyboardLayoutSet();
        return this;
      }
  
      public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
        mParams.mLanguageSwitchKeyEnabled = enabled;
        return this;
      }
  
      public Builder setShowSpecialChars(final boolean enabled) {
        mParams.mShowMoreKeys = enabled;
        return this;
      }
  
      public Builder setShowNumberRow(final boolean enabled) {
        mParams.mShowNumberRow = enabled;
        return this;
      }
  
      public KeyboardLayoutSet build() {
        if (mParams.mSubtype == null)
          throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
        final int xmlId = getXmlId(mResources, mParams.mKeyboardLayoutSetName);
        try {
          parseKeyboardLayoutSet(mResources, xmlId);
        } catch (final IOException | XmlPullParserException e) {
          throw new RuntimeException(e.getMessage() + " in " + mParams.mKeyboardLayoutSetName,
              e);
        }
        return new KeyboardLayoutSet(mContext, mParams);
      }
  
      private static int getXmlId(final Resources resources, final String keyboardLayoutSetName) {
        final String packageName = resources.getResourcePackageName(
            R.xml.keyboard_layout_set_qwerty);
        return resources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
      }
  
      private void parseKeyboardLayoutSet(final Resources res, final int resId)
          throws XmlPullParserException, IOException {
        final XmlResourceParser parser = res.getXml(resId);
        try {
          while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
            final int event = parser.next();
            if (event == XmlPullParser.START_TAG) {
              final String tag = parser.getName();
              if (TAG_KEYBOARD_SET.equals(tag)) {
                parseKeyboardLayoutSetContent(parser);
              } else {
                throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
              }
            }
          }
        } finally {
          parser.close();
        }
      }
  
      private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
          throws XmlPullParserException, IOException {
        while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
          final int event = parser.next();
          if (event == XmlPullParser.START_TAG) {
            final String tag = parser.getName();
            if (TAG_ELEMENT.equals(tag)) {
              parseKeyboardLayoutSetElement(parser);
            } else {
              throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
            }
          } else if (event == XmlPullParser.END_TAG) {
            final String tag = parser.getName();
            if (TAG_KEYBOARD_SET.equals(tag)) {
              break;
            }
            throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
          }
        }
      }
  
      private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
          throws XmlPullParserException, IOException {
        final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
            R.styleable.KeyboardLayoutSet_Element);
        try {
          XmlParseUtils.checkAttributeExists(a,
              R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
              TAG_ELEMENT, parser);
          XmlParseUtils.checkAttributeExists(a,
              R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
              TAG_ELEMENT, parser);
          XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
  
          final ElementParams elementParams = new ElementParams();
          final int elementName = a.getInt(
              R.styleable.KeyboardLayoutSet_Element_elementName, 0);
          elementParams.mKeyboardXmlId = a.getResourceId(
              R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
          elementParams.mAllowRedundantMoreKeys = a.getBoolean(
              R.styleable.KeyboardLayoutSet_Element_allowRedundantMoreKeys, true);
          mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
        } finally {
          a.recycle();
        }
      }
  
      private static int getKeyboardMode(final EditorInfo editorInfo) {
        final int inputType = editorInfo.inputType;
        final int variation = inputType & InputType.TYPE_MASK_VARIATION;
  
        switch (inputType & InputType.TYPE_MASK_CLASS) {
          case InputType.TYPE_CLASS_NUMBER:
            return KeyboardId.MODE_NUMBER;
          case InputType.TYPE_CLASS_DATETIME:
            switch (variation) {
              case InputType.TYPE_DATETIME_VARIATION_DATE:
                return KeyboardId.MODE_DATE;
              case InputType.TYPE_DATETIME_VARIATION_TIME:
                return KeyboardId.MODE_TIME;
              default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
                return KeyboardId.MODE_DATETIME;
            }
          case InputType.TYPE_CLASS_PHONE:
            return KeyboardId.MODE_PHONE;
          case InputType.TYPE_CLASS_TEXT:
            if (InputTypeUtils.isEmailVariation(variation)) {
              return KeyboardId.MODE_EMAIL;
            } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
              return KeyboardId.MODE_URL;
            } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
              return KeyboardId.MODE_IM;
            } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
              return KeyboardId.MODE_TEXT;
            } else {
              return KeyboardId.MODE_TEXT;
            }
          default:
            return KeyboardId.MODE_TEXT;
        }
      }
    }
  }
  