RichInputMethodManager.java revision b86ca76cea9aedf47a81f9272fb59897de3bbbe7
1/*
2 * Copyright (C) 2012 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
17package com.android.inputmethod.latin;
18
19import static com.android.inputmethod.latin.common.Constants.Subtype.KEYBOARD_MODE;
20
21import android.content.Context;
22import android.content.SharedPreferences;
23import android.inputmethodservice.InputMethodService;
24import android.os.AsyncTask;
25import android.os.Build;
26import android.os.IBinder;
27import android.preference.PreferenceManager;
28import android.util.Log;
29import android.view.inputmethod.InputMethodInfo;
30import android.view.inputmethod.InputMethodManager;
31import android.view.inputmethod.InputMethodSubtype;
32
33import com.android.inputmethod.annotations.UsedForTesting;
34import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
35import com.android.inputmethod.latin.settings.Settings;
36import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
37import com.android.inputmethod.latin.utils.LanguageOnSpacebarUtils;
38import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
39
40import java.util.Collections;
41import java.util.HashMap;
42import java.util.HashSet;
43import java.util.List;
44import java.util.Locale;
45import java.util.Map;
46import java.util.Set;
47
48import javax.annotation.Nonnull;
49import javax.annotation.Nullable;
50
51/**
52 * Enrichment class for InputMethodManager to simplify interaction and add functionality.
53 */
54// non final for easy mocking.
55public class RichInputMethodManager {
56    private static final String TAG = RichInputMethodManager.class.getSimpleName();
57    private static final boolean DEBUG = false;
58
59    private RichInputMethodManager() {
60        // This utility class is not publicly instantiable.
61    }
62
63    private static final RichInputMethodManager sInstance = new RichInputMethodManager();
64
65    private Context mContext;
66    private InputMethodManagerCompatWrapper mImmWrapper;
67    private InputMethodInfoCache mInputMethodInfoCache;
68    private RichInputMethodSubtype mCurrentRichInputMethodSubtype;
69    private InputMethodInfo mShortcutInputMethodInfo;
70    private InputMethodSubtype mShortcutSubtype;
71
72    private static final int INDEX_NOT_FOUND = -1;
73
74    public static RichInputMethodManager getInstance() {
75        sInstance.checkInitialized();
76        return sInstance;
77    }
78
79    public static void init(final Context context) {
80        sInstance.initInternal(context);
81    }
82
83    private boolean isInitialized() {
84        return mImmWrapper != null;
85    }
86
87    private void checkInitialized() {
88        if (!isInitialized()) {
89            throw new RuntimeException(TAG + " is used before initialization");
90        }
91    }
92
93    private void initInternal(final Context context) {
94        if (isInitialized()) {
95            return;
96        }
97        mImmWrapper = new InputMethodManagerCompatWrapper(context);
98        mContext = context;
99        mInputMethodInfoCache = new InputMethodInfoCache(
100                mImmWrapper.mImm, context.getPackageName());
101
102        // Initialize additional subtypes.
103        SubtypeLocaleUtils.init(context);
104        final InputMethodSubtype[] additionalSubtypes = getAdditionalSubtypes();
105        mImmWrapper.mImm.setAdditionalInputMethodSubtypes(
106                getInputMethodIdOfThisIme(), additionalSubtypes);
107
108        // Initialize the current input method subtype and the shortcut IME.
109        refreshSubtypeCaches();
110    }
111
112    public InputMethodSubtype[] getAdditionalSubtypes() {
113        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
114        final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes(
115                prefs, mContext.getResources());
116        return AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefAdditionalSubtypes);
117    }
118
119    public InputMethodManager getInputMethodManager() {
120        checkInitialized();
121        return mImmWrapper.mImm;
122    }
123
124    public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList(
125            boolean allowsImplicitlySelectedSubtypes) {
126        return getEnabledInputMethodSubtypeList(
127                getInputMethodInfoOfThisIme(), allowsImplicitlySelectedSubtypes);
128    }
129
130    public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) {
131        if (mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme)) {
132            return true;
133        }
134        // Was not able to call {@link InputMethodManager#switchToNextInputMethodIBinder,boolean)}
135        // because the current device is running ICS or previous and lacks the API.
136        if (switchToNextInputSubtypeInThisIme(token, onlyCurrentIme)) {
137            return true;
138        }
139        return switchToNextInputMethodAndSubtype(token);
140    }
141
142    private boolean switchToNextInputSubtypeInThisIme(final IBinder token,
143            final boolean onlyCurrentIme) {
144        final InputMethodManager imm = mImmWrapper.mImm;
145        final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype();
146        final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(
147                true /* allowsImplicitlySelectedSubtypes */);
148        final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes);
149        if (currentIndex == INDEX_NOT_FOUND) {
150            Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
151                    + SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype));
152            return false;
153        }
154        final int nextIndex = (currentIndex + 1) % enabledSubtypes.size();
155        if (nextIndex <= currentIndex && !onlyCurrentIme) {
156            // The current subtype is the last or only enabled one and it needs to switch to
157            // next IME.
158            return false;
159        }
160        final InputMethodSubtype nextSubtype = enabledSubtypes.get(nextIndex);
161        setInputMethodAndSubtype(token, nextSubtype);
162        return true;
163    }
164
165    private boolean switchToNextInputMethodAndSubtype(final IBinder token) {
166        final InputMethodManager imm = mImmWrapper.mImm;
167        final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList();
168        final int currentIndex = getImiIndexInList(getInputMethodInfoOfThisIme(), enabledImis);
169        if (currentIndex == INDEX_NOT_FOUND) {
170            Log.w(TAG, "Can't find current IME in enabled IMEs: IME package="
171                    + getInputMethodInfoOfThisIme().getPackageName());
172            return false;
173        }
174        final InputMethodInfo nextImi = getNextNonAuxiliaryIme(currentIndex, enabledImis);
175        final List<InputMethodSubtype> enabledSubtypes = getEnabledInputMethodSubtypeList(nextImi,
176                true /* allowsImplicitlySelectedSubtypes */);
177        if (enabledSubtypes.isEmpty()) {
178            // The next IME has no subtype.
179            imm.setInputMethod(token, nextImi.getId());
180            return true;
181        }
182        final InputMethodSubtype firstSubtype = enabledSubtypes.get(0);
183        imm.setInputMethodAndSubtype(token, nextImi.getId(), firstSubtype);
184        return true;
185    }
186
187    private static int getImiIndexInList(final InputMethodInfo inputMethodInfo,
188            final List<InputMethodInfo> imiList) {
189        final int count = imiList.size();
190        for (int index = 0; index < count; index++) {
191            final InputMethodInfo imi = imiList.get(index);
192            if (imi.equals(inputMethodInfo)) {
193                return index;
194            }
195        }
196        return INDEX_NOT_FOUND;
197    }
198
199    // This method mimics {@link InputMethodManager#switchToNextInputMethod(IBinder,boolean)}.
200    private static InputMethodInfo getNextNonAuxiliaryIme(final int currentIndex,
201            final List<InputMethodInfo> imiList) {
202        final int count = imiList.size();
203        for (int i = 1; i < count; i++) {
204            final int nextIndex = (currentIndex + i) % count;
205            final InputMethodInfo nextImi = imiList.get(nextIndex);
206            if (!isAuxiliaryIme(nextImi)) {
207                return nextImi;
208            }
209        }
210        return imiList.get(currentIndex);
211    }
212
213    // Copied from {@link InputMethodInfo}. See how auxiliary of IME is determined.
214    private static boolean isAuxiliaryIme(final InputMethodInfo imi) {
215        final int count = imi.getSubtypeCount();
216        if (count == 0) {
217            return false;
218        }
219        for (int index = 0; index < count; index++) {
220            final InputMethodSubtype subtype = imi.getSubtypeAt(index);
221            if (!subtype.isAuxiliary()) {
222                return false;
223            }
224        }
225        return true;
226    }
227
228    private static class InputMethodInfoCache {
229        private final InputMethodManager mImm;
230        private final String mImePackageName;
231
232        private InputMethodInfo mCachedThisImeInfo;
233        private final HashMap<InputMethodInfo, List<InputMethodSubtype>>
234                mCachedSubtypeListWithImplicitlySelected;
235        private final HashMap<InputMethodInfo, List<InputMethodSubtype>>
236                mCachedSubtypeListOnlyExplicitlySelected;
237
238        public InputMethodInfoCache(final InputMethodManager imm, final String imePackageName) {
239            mImm = imm;
240            mImePackageName = imePackageName;
241            mCachedSubtypeListWithImplicitlySelected = new HashMap<>();
242            mCachedSubtypeListOnlyExplicitlySelected = new HashMap<>();
243        }
244
245        public synchronized InputMethodInfo getInputMethodOfThisIme() {
246            if (mCachedThisImeInfo != null) {
247                return mCachedThisImeInfo;
248            }
249            for (final InputMethodInfo imi : mImm.getInputMethodList()) {
250                if (imi.getPackageName().equals(mImePackageName)) {
251                    mCachedThisImeInfo = imi;
252                    return imi;
253                }
254            }
255            throw new RuntimeException("Input method id for " + mImePackageName + " not found.");
256        }
257
258        public synchronized List<InputMethodSubtype> getEnabledInputMethodSubtypeList(
259                final InputMethodInfo imi, final boolean allowsImplicitlySelectedSubtypes) {
260            final HashMap<InputMethodInfo, List<InputMethodSubtype>> cache =
261                    allowsImplicitlySelectedSubtypes
262                    ? mCachedSubtypeListWithImplicitlySelected
263                    : mCachedSubtypeListOnlyExplicitlySelected;
264            final List<InputMethodSubtype> cachedList = cache.get(imi);
265            if (cachedList != null) {
266                return cachedList;
267            }
268            final List<InputMethodSubtype> result = mImm.getEnabledInputMethodSubtypeList(
269                    imi, allowsImplicitlySelectedSubtypes);
270            cache.put(imi, result);
271            return result;
272        }
273
274        public synchronized void clear() {
275            mCachedThisImeInfo = null;
276            mCachedSubtypeListWithImplicitlySelected.clear();
277            mCachedSubtypeListOnlyExplicitlySelected.clear();
278        }
279    }
280
281    public InputMethodInfo getInputMethodInfoOfThisIme() {
282        return mInputMethodInfoCache.getInputMethodOfThisIme();
283    }
284
285    public String getInputMethodIdOfThisIme() {
286        return getInputMethodInfoOfThisIme().getId();
287    }
288
289    public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) {
290        return checkIfSubtypeBelongsToList(subtype,
291                getEnabledInputMethodSubtypeList(
292                        getInputMethodInfoOfThisIme(),
293                        true /* allowsImplicitlySelectedSubtypes */));
294    }
295
296    public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(
297            final InputMethodSubtype subtype) {
298        final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype);
299        final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList(subtype,
300                getMyEnabledInputMethodSubtypeList(false /* allowsImplicitlySelectedSubtypes */));
301        return subtypeEnabled && !subtypeExplicitlyEnabled;
302    }
303
304    private static boolean checkIfSubtypeBelongsToList(final InputMethodSubtype subtype,
305            final List<InputMethodSubtype> subtypes) {
306        return getSubtypeIndexInList(subtype, subtypes) != INDEX_NOT_FOUND;
307    }
308
309    private static int getSubtypeIndexInList(final InputMethodSubtype subtype,
310            final List<InputMethodSubtype> subtypes) {
311        final int count = subtypes.size();
312        for (int index = 0; index < count; index++) {
313            final InputMethodSubtype ims = subtypes.get(index);
314            if (ims.equals(subtype)) {
315                return index;
316            }
317        }
318        return INDEX_NOT_FOUND;
319    }
320
321    public void onSubtypeChanged(@Nonnull final InputMethodSubtype newSubtype) {
322        updateCurrentSubtype(newSubtype);
323        updateShortcutIme();
324        if (DEBUG) {
325            Log.w(TAG, "onSubtypeChanged: " + mCurrentRichInputMethodSubtype.getNameForLogging());
326        }
327    }
328
329    private static RichInputMethodSubtype sForcedSubtypeForTesting = null;
330
331    @UsedForTesting
332    static void forceSubtype(@Nonnull final InputMethodSubtype subtype) {
333        sForcedSubtypeForTesting = RichInputMethodSubtype.getRichInputMethodSubtype(subtype);
334    }
335
336    @Nonnull
337    public Locale[] getCurrentSubtypeLocales() {
338        if (null != sForcedSubtypeForTesting) {
339            return sForcedSubtypeForTesting.getLocales();
340        }
341        return getCurrentSubtype().getLocales();
342    }
343
344    @Nonnull
345    public RichInputMethodSubtype getCurrentSubtype() {
346        if (null != sForcedSubtypeForTesting) {
347            return sForcedSubtypeForTesting;
348        }
349        return mCurrentRichInputMethodSubtype;
350    }
351
352
353    public String getCombiningRulesExtraValueOfCurrentSubtype() {
354        return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype().getRawSubtype());
355    }
356
357    public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) {
358        final List<InputMethodInfo> enabledImis = mImmWrapper.mImm.getEnabledInputMethodList();
359        return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, enabledImis);
360    }
361
362    public boolean hasMultipleEnabledSubtypesInThisIme(
363            final boolean shouldIncludeAuxiliarySubtypes) {
364        final List<InputMethodInfo> imiList = Collections.singletonList(
365                getInputMethodInfoOfThisIme());
366        return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList);
367    }
368
369    private boolean hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes,
370            final List<InputMethodInfo> imiList) {
371        // Number of the filtered IMEs
372        int filteredImisCount = 0;
373
374        for (InputMethodInfo imi : imiList) {
375            // We can return true immediately after we find two or more filtered IMEs.
376            if (filteredImisCount > 1) return true;
377            final List<InputMethodSubtype> subtypes = getEnabledInputMethodSubtypeList(imi, true);
378            // IMEs that have no subtypes should be counted.
379            if (subtypes.isEmpty()) {
380                ++filteredImisCount;
381                continue;
382            }
383
384            int auxCount = 0;
385            for (InputMethodSubtype subtype : subtypes) {
386                if (subtype.isAuxiliary()) {
387                    ++auxCount;
388                }
389            }
390            final int nonAuxCount = subtypes.size() - auxCount;
391
392            // IMEs that have one or more non-auxiliary subtypes should be counted.
393            // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary
394            // subtypes should be counted as well.
395            if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) {
396                ++filteredImisCount;
397            }
398        }
399
400        if (filteredImisCount > 1) {
401            return true;
402        }
403        final List<InputMethodSubtype> subtypes = getMyEnabledInputMethodSubtypeList(true);
404        int keyboardCount = 0;
405        // imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's
406        // both explicitly and implicitly enabled input method subtype.
407        // (The current IME should be LatinIME.)
408        for (InputMethodSubtype subtype : subtypes) {
409            if (KEYBOARD_MODE.equals(subtype.getMode())) {
410                ++keyboardCount;
411            }
412        }
413        return keyboardCount > 1;
414    }
415
416    public InputMethodSubtype findSubtypeByLocaleAndKeyboardLayoutSet(final String localeString,
417            final String keyboardLayoutSetName) {
418        final InputMethodInfo myImi = getInputMethodInfoOfThisIme();
419        final int count = myImi.getSubtypeCount();
420        for (int i = 0; i < count; i++) {
421            final InputMethodSubtype subtype = myImi.getSubtypeAt(i);
422            final String layoutName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
423            if (localeString.equals(subtype.getLocale())
424                    && keyboardLayoutSetName.equals(layoutName)) {
425                return subtype;
426            }
427        }
428        return null;
429    }
430
431    public void setInputMethodAndSubtype(final IBinder token, final InputMethodSubtype subtype) {
432        mImmWrapper.mImm.setInputMethodAndSubtype(
433                token, getInputMethodIdOfThisIme(), subtype);
434    }
435
436    public void setAdditionalInputMethodSubtypes(final InputMethodSubtype[] subtypes) {
437        mImmWrapper.mImm.setAdditionalInputMethodSubtypes(
438                getInputMethodIdOfThisIme(), subtypes);
439        // Clear the cache so that we go read the {@link InputMethodInfo} of this IME and list of
440        // subtypes again next time.
441        refreshSubtypeCaches();
442    }
443
444    private List<InputMethodSubtype> getEnabledInputMethodSubtypeList(final InputMethodInfo imi,
445            final boolean allowsImplicitlySelectedSubtypes) {
446        return mInputMethodInfoCache.getEnabledInputMethodSubtypeList(
447                imi, allowsImplicitlySelectedSubtypes);
448    }
449
450    public void refreshSubtypeCaches() {
451        mInputMethodInfoCache.clear();
452        updateCurrentSubtype(mImmWrapper.mImm.getCurrentInputMethodSubtype());
453        updateShortcutIme();
454    }
455
456    public boolean shouldOfferSwitchingToNextInputMethod(final IBinder binder,
457            boolean defaultValue) {
458        // Use the default value instead on Jelly Bean MR2 and previous where
459        // {@link InputMethodManager#shouldOfferSwitchingToNextInputMethod} isn't yet available
460        // and on KitKat where the API is still just a stub to return true always.
461        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
462            return defaultValue;
463        }
464        return mImmWrapper.shouldOfferSwitchingToNextInputMethod(binder);
465    }
466
467    public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes() {
468        final Locale systemLocale = mContext.getResources().getConfiguration().locale;
469        final Set<InputMethodSubtype> enabledSubtypesOfEnabledImes = new HashSet<>();
470        final InputMethodManager inputMethodManager = getInputMethodManager();
471        final List<InputMethodInfo> enabledInputMethodInfoList =
472                inputMethodManager.getEnabledInputMethodList();
473        for (final InputMethodInfo info : enabledInputMethodInfoList) {
474            final List<InputMethodSubtype> enabledSubtypes =
475                    inputMethodManager.getEnabledInputMethodSubtypeList(
476                            info, true /* allowsImplicitlySelectedSubtypes */);
477            if (enabledSubtypes.isEmpty()) {
478                // An IME with no subtypes is found.
479                return false;
480            }
481            enabledSubtypesOfEnabledImes.addAll(enabledSubtypes);
482        }
483        for (final InputMethodSubtype subtype : enabledSubtypesOfEnabledImes) {
484            if (!subtype.isAuxiliary() && !subtype.getLocale().isEmpty()
485                    && !systemLocale.equals(SubtypeLocaleUtils.getSubtypeLocale(subtype))) {
486                return false;
487            }
488        }
489        return true;
490    }
491
492    private void updateCurrentSubtype(@Nullable final InputMethodSubtype subtype) {
493        mCurrentRichInputMethodSubtype = RichInputMethodSubtype.getRichInputMethodSubtype(subtype);
494    }
495
496    private void updateShortcutIme() {
497        if (DEBUG) {
498            Log.d(TAG, "Update shortcut IME from : "
499                    + (mShortcutInputMethodInfo == null
500                            ? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
501                    + (mShortcutSubtype == null ? "<null>" : (
502                            mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
503        }
504        final RichInputMethodSubtype richSubtype = mCurrentRichInputMethodSubtype;
505        final boolean implicitlyEnabledSubtype = checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(
506                richSubtype.getRawSubtype());
507        final Locale systemLocale = mContext.getResources().getConfiguration().locale;
508        LanguageOnSpacebarUtils.onSubtypeChanged(
509                richSubtype, implicitlyEnabledSubtype, systemLocale);
510        LanguageOnSpacebarUtils.setEnabledSubtypes(getMyEnabledInputMethodSubtypeList(
511                true /* allowsImplicitlySelectedSubtypes */));
512
513        // TODO: Update an icon for shortcut IME
514        final Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts =
515                getInputMethodManager().getShortcutInputMethodsAndSubtypes();
516        mShortcutInputMethodInfo = null;
517        mShortcutSubtype = null;
518        for (final InputMethodInfo imi : shortcuts.keySet()) {
519            final List<InputMethodSubtype> subtypes = shortcuts.get(imi);
520            // TODO: Returns the first found IMI for now. Should handle all shortcuts as
521            // appropriate.
522            mShortcutInputMethodInfo = imi;
523            // TODO: Pick up the first found subtype for now. Should handle all subtypes
524            // as appropriate.
525            mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null;
526            break;
527        }
528        if (DEBUG) {
529            Log.d(TAG, "Update shortcut IME to : "
530                    + (mShortcutInputMethodInfo == null
531                            ? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
532                    + (mShortcutSubtype == null ? "<null>" : (
533                            mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
534        }
535    }
536
537    public void switchToShortcutIme(final InputMethodService context) {
538        if (mShortcutInputMethodInfo == null) {
539            return;
540        }
541
542        final String imiId = mShortcutInputMethodInfo.getId();
543        switchToTargetIME(imiId, mShortcutSubtype, context);
544    }
545
546    private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype,
547            final InputMethodService context) {
548        final IBinder token = context.getWindow().getWindow().getAttributes().token;
549        if (token == null) {
550            return;
551        }
552        final InputMethodManager imm = getInputMethodManager();
553        new AsyncTask<Void, Void, Void>() {
554            @Override
555            protected Void doInBackground(Void... params) {
556                imm.setInputMethodAndSubtype(token, imiId, subtype);
557                return null;
558            }
559        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
560    }
561
562    public boolean isShortcutImeReady() {
563        if (mShortcutInputMethodInfo == null) {
564            return false;
565        }
566        if (mShortcutSubtype == null) {
567            return true;
568        }
569        return true;
570    }
571}
572