Home | History | Annotate | Download | only in keyboard
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.keyboard;
     18 
     19 import static com.android.inputmethod.latin.common.Constants.ImeOption.FORCE_ASCII;
     20 import static com.android.inputmethod.latin.common.Constants.ImeOption.NO_SETTINGS_KEY;
     21 
     22 import android.content.Context;
     23 import android.content.res.Resources;
     24 import android.content.res.TypedArray;
     25 import android.content.res.XmlResourceParser;
     26 import android.text.InputType;
     27 import android.util.Log;
     28 import android.util.SparseArray;
     29 import android.util.Xml;
     30 import android.view.inputmethod.EditorInfo;
     31 import android.view.inputmethod.InputMethodSubtype;
     32 
     33 import com.android.inputmethod.compat.EditorInfoCompatUtils;
     34 import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
     35 import com.android.inputmethod.compat.UserManagerCompatUtils;
     36 import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
     37 import com.android.inputmethod.keyboard.internal.KeyboardParams;
     38 import com.android.inputmethod.keyboard.internal.UniqueKeysCache;
     39 import com.android.inputmethod.latin.InputAttributes;
     40 import com.android.inputmethod.latin.R;
     41 import com.android.inputmethod.latin.RichInputMethodSubtype;
     42 import com.android.inputmethod.latin.define.DebugFlags;
     43 import com.android.inputmethod.latin.utils.InputTypeUtils;
     44 import com.android.inputmethod.latin.utils.ScriptUtils;
     45 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
     46 import com.android.inputmethod.latin.utils.XmlParseUtils;
     47 
     48 import org.xmlpull.v1.XmlPullParser;
     49 import org.xmlpull.v1.XmlPullParserException;
     50 
     51 import java.io.IOException;
     52 import java.lang.ref.SoftReference;
     53 import java.util.HashMap;
     54 
     55 import javax.annotation.Nonnull;
     56 import javax.annotation.Nullable;
     57 
     58 /**
     59  * This class represents a set of keyboard layouts. Each of them represents a different keyboard
     60  * specific to a keyboard state, such as alphabet, symbols, and so on.  Layouts in the same
     61  * {@link KeyboardLayoutSet} are related to each other.
     62  * A {@link KeyboardLayoutSet} needs to be created for each
     63  * {@link android.view.inputmethod.EditorInfo}.
     64  */
     65 public final class KeyboardLayoutSet {
     66     private static final String TAG = KeyboardLayoutSet.class.getSimpleName();
     67     private static final boolean DEBUG_CACHE = false;
     68 
     69     private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet";
     70     private static final String TAG_ELEMENT = "Element";
     71     private static final String TAG_FEATURE = "Feature";
     72 
     73     private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
     74 
     75     private final Context mContext;
     76     @Nonnull
     77     private final Params mParams;
     78 
     79     // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and
     80     // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of
     81     // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts.
     82     private static final int FORCIBLE_CACHE_SIZE = 4;
     83     // By construction of soft references, anything that is also referenced somewhere else
     84     // will stay in the cache. So we forcibly keep some references in an array to prevent
     85     // them from disappearing from sKeyboardCache.
     86     private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE];
     87     private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
     88             new HashMap<>();
     89     @Nonnull
     90     private static final UniqueKeysCache sUniqueKeysCache = UniqueKeysCache.newInstance();
     91     private final static HashMap<InputMethodSubtype, Integer> sScriptIdsForSubtypes =
     92             new HashMap<>();
     93 
     94     @SuppressWarnings("serial")
     95     public static final class KeyboardLayoutSetException extends RuntimeException {
     96         public final KeyboardId mKeyboardId;
     97 
     98         public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) {
     99             super(cause);
    100             mKeyboardId = keyboardId;
    101         }
    102     }
    103 
    104     private static final class ElementParams {
    105         int mKeyboardXmlId;
    106         boolean mProximityCharsCorrectionEnabled;
    107         boolean mSupportsSplitLayout;
    108         boolean mAllowRedundantMoreKeys;
    109         public ElementParams() {}
    110     }
    111 
    112     public static final class Params {
    113         String mKeyboardLayoutSetName;
    114         int mMode;
    115         boolean mDisableTouchPositionCorrectionDataForTest;
    116         // TODO: Use {@link InputAttributes} instead of these variables.
    117         EditorInfo mEditorInfo;
    118         boolean mIsPasswordField;
    119         boolean mVoiceInputKeyEnabled;
    120         boolean mNoSettingsKey;
    121         boolean mLanguageSwitchKeyEnabled;
    122         RichInputMethodSubtype mSubtype;
    123         boolean mIsSpellChecker;
    124         int mKeyboardWidth;
    125         int mKeyboardHeight;
    126         int mScriptId = ScriptUtils.SCRIPT_LATIN;
    127         // Indicates if the user has enabled the split-layout preference
    128         // and the required ProductionFlags are enabled.
    129         boolean mIsSplitLayoutEnabledByUser;
    130         // Indicates if split layout is actually enabled, taking into account
    131         // whether the user has enabled it, and the keyboard layout supports it.
    132         boolean mIsSplitLayoutEnabled;
    133         // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
    134         final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
    135                 new SparseArray<>();
    136     }
    137 
    138     public static void onSystemLocaleChanged() {
    139         clearKeyboardCache();
    140     }
    141 
    142     public static void onKeyboardThemeChanged() {
    143         clearKeyboardCache();
    144     }
    145 
    146     private static void clearKeyboardCache() {
    147         sKeyboardCache.clear();
    148         sUniqueKeysCache.clear();
    149     }
    150 
    151     public static int getScriptId(final Resources resources,
    152             @Nonnull final InputMethodSubtype subtype) {
    153         final Integer value = sScriptIdsForSubtypes.get(subtype);
    154         if (null == value) {
    155             final int scriptId = Builder.readScriptId(resources, subtype);
    156             sScriptIdsForSubtypes.put(subtype, scriptId);
    157             return scriptId;
    158         }
    159         return value;
    160     }
    161 
    162     KeyboardLayoutSet(final Context context, @Nonnull final Params params) {
    163         mContext = context;
    164         mParams = params;
    165     }
    166 
    167     @Nonnull
    168     public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) {
    169         final int keyboardLayoutSetElementId;
    170         switch (mParams.mMode) {
    171         case KeyboardId.MODE_PHONE:
    172             if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) {
    173                 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS;
    174             } else {
    175                 keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE;
    176             }
    177             break;
    178         case KeyboardId.MODE_NUMBER:
    179         case KeyboardId.MODE_DATE:
    180         case KeyboardId.MODE_TIME:
    181         case KeyboardId.MODE_DATETIME:
    182             keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER;
    183             break;
    184         default:
    185             keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId;
    186             break;
    187         }
    188 
    189         ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
    190                 keyboardLayoutSetElementId);
    191         if (elementParams == null) {
    192             elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get(
    193                     KeyboardId.ELEMENT_ALPHABET);
    194         }
    195         // Note: The keyboard for each shift state, and mode are represented as an elementName
    196         // attribute in a keyboard_layout_set XML file.  Also each keyboard layout XML resource is
    197         // specified as an elementKeyboard attribute in the file.
    198         // The KeyboardId is an internal key for a Keyboard object.
    199 
    200         mParams.mIsSplitLayoutEnabled = mParams.mIsSplitLayoutEnabledByUser
    201                 && elementParams.mSupportsSplitLayout;
    202         final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
    203         try {
    204             return getKeyboard(elementParams, id);
    205         } catch (final RuntimeException e) {
    206             Log.e(TAG, "Can't create keyboard: " + id, e);
    207             throw new KeyboardLayoutSetException(e, id);
    208         }
    209     }
    210 
    211     @Nonnull
    212     private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) {
    213         final SoftReference<Keyboard> ref = sKeyboardCache.get(id);
    214         final Keyboard cachedKeyboard = (ref == null) ? null : ref.get();
    215         if (cachedKeyboard != null) {
    216             if (DEBUG_CACHE) {
    217                 Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT  id=" + id);
    218             }
    219             return cachedKeyboard;
    220         }
    221 
    222         final KeyboardBuilder<KeyboardParams> builder =
    223                 new KeyboardBuilder<>(mContext, new KeyboardParams(sUniqueKeysCache));
    224         sUniqueKeysCache.setEnabled(id.isAlphabetKeyboard());
    225         builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys);
    226         final int keyboardXmlId = elementParams.mKeyboardXmlId;
    227         builder.load(keyboardXmlId, id);
    228         if (mParams.mDisableTouchPositionCorrectionDataForTest) {
    229             builder.disableTouchPositionCorrectionDataForTest();
    230         }
    231         builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled);
    232         final Keyboard keyboard = builder.build();
    233         sKeyboardCache.put(id, new SoftReference<>(keyboard));
    234         if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET
    235                 || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)
    236                 && !mParams.mIsSpellChecker) {
    237             // We only forcibly cache the primary, "ALPHABET", layouts.
    238             for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) {
    239                 sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1];
    240             }
    241             sForcibleKeyboardCache[0] = keyboard;
    242             if (DEBUG_CACHE) {
    243                 Log.d(TAG, "forcing caching of keyboard with id=" + id);
    244             }
    245         }
    246         if (DEBUG_CACHE) {
    247             Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": "
    248                     + ((ref == null) ? "LOAD" : "GCed") + " id=" + id);
    249         }
    250         return keyboard;
    251     }
    252 
    253     public int getScriptId() {
    254         return mParams.mScriptId;
    255     }
    256 
    257     public static final class Builder {
    258         private final Context mContext;
    259         private final String mPackageName;
    260         private final Resources mResources;
    261 
    262         private final Params mParams = new Params();
    263 
    264         private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
    265 
    266         public Builder(final Context context, @Nullable final EditorInfo ei) {
    267             mContext = context;
    268             mPackageName = context.getPackageName();
    269             mResources = context.getResources();
    270             final Params params = mParams;
    271 
    272             final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO;
    273             params.mMode = getKeyboardMode(editorInfo);
    274             // TODO: Consolidate those with {@link InputAttributes}.
    275             params.mEditorInfo = editorInfo;
    276             params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType);
    277             params.mNoSettingsKey = InputAttributes.inPrivateImeOptions(
    278                     mPackageName, NO_SETTINGS_KEY, editorInfo);
    279 
    280             // When the device is still unlocked, features like showing the IME setting app need to
    281             // be locked down.
    282             // TODO: Switch to {@code UserManagerCompat.isUserUnlocked()} in the support-v4 library
    283             // when it becomes publicly available.
    284             @UserManagerCompatUtils.LockState
    285             final int lockState = UserManagerCompatUtils.getUserLockState(context);
    286             if (lockState == UserManagerCompatUtils.LOCK_STATE_LOCKED) {
    287                 params.mNoSettingsKey = true;
    288             }
    289         }
    290 
    291         public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) {
    292             mParams.mKeyboardWidth = keyboardWidth;
    293             mParams.mKeyboardHeight = keyboardHeight;
    294             return this;
    295         }
    296 
    297         public Builder setSubtype(@Nonnull final RichInputMethodSubtype subtype) {
    298             final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype);
    299             // TODO: Consolidate with {@link InputAttributes}.
    300             @SuppressWarnings("deprecation")
    301             final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions(
    302                     mPackageName, FORCE_ASCII, mParams.mEditorInfo);
    303             final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
    304                     mParams.mEditorInfo.imeOptions)
    305                     || deprecatedForceAscii;
    306             final RichInputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
    307                     ? RichInputMethodSubtype.getNoLanguageSubtype()
    308                     : subtype;
    309             mParams.mSubtype = keyboardSubtype;
    310             mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
    311                     + keyboardSubtype.getKeyboardLayoutSetName();
    312             return this;
    313         }
    314 
    315         public Builder setIsSpellChecker(final boolean isSpellChecker) {
    316             mParams.mIsSpellChecker = isSpellChecker;
    317             return this;
    318         }
    319 
    320         public Builder setVoiceInputKeyEnabled(final boolean enabled) {
    321             mParams.mVoiceInputKeyEnabled = enabled;
    322             return this;
    323         }
    324 
    325         public Builder setLanguageSwitchKeyEnabled(final boolean enabled) {
    326             mParams.mLanguageSwitchKeyEnabled = enabled;
    327             return this;
    328         }
    329 
    330         public Builder disableTouchPositionCorrectionData() {
    331             mParams.mDisableTouchPositionCorrectionDataForTest = true;
    332             return this;
    333         }
    334 
    335         public Builder setSplitLayoutEnabledByUser(final boolean enabled) {
    336             mParams.mIsSplitLayoutEnabledByUser = enabled;
    337             return this;
    338         }
    339 
    340         // Super redux version of reading the script ID for some subtype from Xml.
    341         static int readScriptId(final Resources resources, final InputMethodSubtype subtype) {
    342             final String layoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
    343                     + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
    344             final int xmlId = getXmlId(resources, layoutSetName);
    345             final XmlResourceParser parser = resources.getXml(xmlId);
    346             try {
    347                 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    348                     // Bovinate through the XML stupidly searching for TAG_FEATURE, and read
    349                     // the script Id from it.
    350                     parser.next();
    351                     final String tag = parser.getName();
    352                     if (TAG_FEATURE.equals(tag)) {
    353                         return readScriptIdFromTagFeature(resources, parser);
    354                     }
    355                 }
    356             } catch (final IOException | XmlPullParserException e) {
    357                 throw new RuntimeException(e.getMessage() + " in " + layoutSetName, e);
    358             } finally {
    359                 parser.close();
    360             }
    361             // If the tag is not found, then the default script is Latin.
    362             return ScriptUtils.SCRIPT_LATIN;
    363         }
    364 
    365         private static int readScriptIdFromTagFeature(final Resources resources,
    366                 final XmlPullParser parser) throws IOException, XmlPullParserException {
    367             final TypedArray featureAttr = resources.obtainAttributes(Xml.asAttributeSet(parser),
    368                     R.styleable.KeyboardLayoutSet_Feature);
    369             try {
    370                 final int scriptId =
    371                         featureAttr.getInt(R.styleable.KeyboardLayoutSet_Feature_supportedScript,
    372                                 ScriptUtils.SCRIPT_UNKNOWN);
    373                 XmlParseUtils.checkEndTag(TAG_FEATURE, parser);
    374                 return scriptId;
    375             } finally {
    376                 featureAttr.recycle();
    377             }
    378         }
    379 
    380         public KeyboardLayoutSet build() {
    381             if (mParams.mSubtype == null)
    382                 throw new RuntimeException("KeyboardLayoutSet subtype is not specified");
    383             final int xmlId = getXmlId(mResources, mParams.mKeyboardLayoutSetName);
    384             try {
    385                 parseKeyboardLayoutSet(mResources, xmlId);
    386             } catch (final IOException | XmlPullParserException e) {
    387                 throw new RuntimeException(e.getMessage() + " in " + mParams.mKeyboardLayoutSetName,
    388                         e);
    389             }
    390             return new KeyboardLayoutSet(mContext, mParams);
    391         }
    392 
    393         private static int getXmlId(final Resources resources, final String keyboardLayoutSetName) {
    394             final String packageName = resources.getResourcePackageName(
    395                     R.xml.keyboard_layout_set_qwerty);
    396             return resources.getIdentifier(keyboardLayoutSetName, "xml", packageName);
    397         }
    398 
    399         private void parseKeyboardLayoutSet(final Resources res, final int resId)
    400                 throws XmlPullParserException, IOException {
    401             final XmlResourceParser parser = res.getXml(resId);
    402             try {
    403                 while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    404                     final int event = parser.next();
    405                     if (event == XmlPullParser.START_TAG) {
    406                         final String tag = parser.getName();
    407                         if (TAG_KEYBOARD_SET.equals(tag)) {
    408                             parseKeyboardLayoutSetContent(parser);
    409                         } else {
    410                             throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
    411                         }
    412                     }
    413                 }
    414             } finally {
    415                 parser.close();
    416             }
    417         }
    418 
    419         private void parseKeyboardLayoutSetContent(final XmlPullParser parser)
    420                 throws XmlPullParserException, IOException {
    421             while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
    422                 final int event = parser.next();
    423                 if (event == XmlPullParser.START_TAG) {
    424                     final String tag = parser.getName();
    425                     if (TAG_ELEMENT.equals(tag)) {
    426                         parseKeyboardLayoutSetElement(parser);
    427                     } else if (TAG_FEATURE.equals(tag)) {
    428                         mParams.mScriptId = readScriptIdFromTagFeature(mResources, parser);
    429                     } else {
    430                         throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
    431                     }
    432                 } else if (event == XmlPullParser.END_TAG) {
    433                     final String tag = parser.getName();
    434                     if (TAG_KEYBOARD_SET.equals(tag)) {
    435                         break;
    436                     }
    437                     throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET);
    438                 }
    439             }
    440         }
    441 
    442         private void parseKeyboardLayoutSetElement(final XmlPullParser parser)
    443                 throws XmlPullParserException, IOException {
    444             final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser),
    445                     R.styleable.KeyboardLayoutSet_Element);
    446             try {
    447                 XmlParseUtils.checkAttributeExists(a,
    448                         R.styleable.KeyboardLayoutSet_Element_elementName, "elementName",
    449                         TAG_ELEMENT, parser);
    450                 XmlParseUtils.checkAttributeExists(a,
    451                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard",
    452                         TAG_ELEMENT, parser);
    453                 XmlParseUtils.checkEndTag(TAG_ELEMENT, parser);
    454 
    455                 final ElementParams elementParams = new ElementParams();
    456                 final int elementName = a.getInt(
    457                         R.styleable.KeyboardLayoutSet_Element_elementName, 0);
    458                 elementParams.mKeyboardXmlId = a.getResourceId(
    459                         R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0);
    460                 elementParams.mProximityCharsCorrectionEnabled = a.getBoolean(
    461                         R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection,
    462                         false);
    463                 elementParams.mSupportsSplitLayout = a.getBoolean(
    464                         R.styleable.KeyboardLayoutSet_Element_supportsSplitLayout, false);
    465                 elementParams.mAllowRedundantMoreKeys = a.getBoolean(
    466                         R.styleable.KeyboardLayoutSet_Element_allowRedundantMoreKeys, true);
    467                 mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams);
    468             } finally {
    469                 a.recycle();
    470             }
    471         }
    472 
    473         private static int getKeyboardMode(final EditorInfo editorInfo) {
    474             final int inputType = editorInfo.inputType;
    475             final int variation = inputType & InputType.TYPE_MASK_VARIATION;
    476 
    477             switch (inputType & InputType.TYPE_MASK_CLASS) {
    478             case InputType.TYPE_CLASS_NUMBER:
    479                 return KeyboardId.MODE_NUMBER;
    480             case InputType.TYPE_CLASS_DATETIME:
    481                 switch (variation) {
    482                 case InputType.TYPE_DATETIME_VARIATION_DATE:
    483                     return KeyboardId.MODE_DATE;
    484                 case InputType.TYPE_DATETIME_VARIATION_TIME:
    485                     return KeyboardId.MODE_TIME;
    486                 default: // InputType.TYPE_DATETIME_VARIATION_NORMAL
    487                     return KeyboardId.MODE_DATETIME;
    488                 }
    489             case InputType.TYPE_CLASS_PHONE:
    490                 return KeyboardId.MODE_PHONE;
    491             case InputType.TYPE_CLASS_TEXT:
    492                 if (InputTypeUtils.isEmailVariation(variation)) {
    493                     return KeyboardId.MODE_EMAIL;
    494                 } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
    495                     return KeyboardId.MODE_URL;
    496                 } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
    497                     return KeyboardId.MODE_IM;
    498                 } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
    499                     return KeyboardId.MODE_TEXT;
    500                 } else {
    501                     return KeyboardId.MODE_TEXT;
    502                 }
    503             default:
    504                 return KeyboardId.MODE_TEXT;
    505             }
    506         }
    507     }
    508 }
    509