1/*
2 * Copyright (C) 2015 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.launcher3.folder;
18
19import android.annotation.SuppressLint;
20import android.content.Context;
21import android.graphics.Canvas;
22import android.graphics.drawable.Drawable;
23import android.util.ArrayMap;
24import android.util.AttributeSet;
25import android.util.Log;
26import android.view.Gravity;
27import android.view.LayoutInflater;
28import android.view.View;
29import android.view.ViewDebug;
30
31import com.android.launcher3.BubbleTextView;
32import com.android.launcher3.CellLayout;
33import com.android.launcher3.DeviceProfile;
34import com.android.launcher3.InvariantDeviceProfile;
35import com.android.launcher3.ItemInfo;
36import com.android.launcher3.Launcher;
37import com.android.launcher3.LauncherAppState;
38import com.android.launcher3.PagedView;
39import com.android.launcher3.R;
40import com.android.launcher3.ShortcutAndWidgetContainer;
41import com.android.launcher3.ShortcutInfo;
42import com.android.launcher3.Utilities;
43import com.android.launcher3.Workspace.ItemOperator;
44import com.android.launcher3.anim.Interpolators;
45import com.android.launcher3.keyboard.ViewGroupFocusHelper;
46import com.android.launcher3.pageindicators.PageIndicatorDots;
47import com.android.launcher3.touch.ItemClickHandler;
48import com.android.launcher3.util.Thunk;
49
50import java.util.ArrayList;
51import java.util.Iterator;
52import java.util.Map;
53
54public class FolderPagedView extends PagedView<PageIndicatorDots> {
55
56    private static final String TAG = "FolderPagedView";
57
58    private static final int REORDER_ANIMATION_DURATION = 230;
59    private static final int START_VIEW_REORDER_DELAY = 30;
60    private static final float VIEW_REORDER_DELAY_FACTOR = 0.9f;
61
62    /**
63     * Fraction of the width to scroll when showing the next page hint.
64     */
65    private static final float SCROLL_HINT_FRACTION = 0.07f;
66
67    private static final int[] sTmpArray = new int[2];
68
69    public final boolean mIsRtl;
70
71    private final LayoutInflater mInflater;
72    private final ViewGroupFocusHelper mFocusIndicatorHelper;
73
74    @Thunk final ArrayMap<View, Runnable> mPendingAnimations = new ArrayMap<>();
75
76    @ViewDebug.ExportedProperty(category = "launcher")
77    private final int mMaxCountX;
78    @ViewDebug.ExportedProperty(category = "launcher")
79    private final int mMaxCountY;
80    @ViewDebug.ExportedProperty(category = "launcher")
81    private final int mMaxItemsPerPage;
82
83    private int mAllocatedContentSize;
84    @ViewDebug.ExportedProperty(category = "launcher")
85    private int mGridCountX;
86    @ViewDebug.ExportedProperty(category = "launcher")
87    private int mGridCountY;
88
89    private Folder mFolder;
90
91    public FolderPagedView(Context context, AttributeSet attrs) {
92        super(context, attrs);
93        InvariantDeviceProfile profile = LauncherAppState.getIDP(context);
94        mMaxCountX = profile.numFolderColumns;
95        mMaxCountY = profile.numFolderRows;
96
97        mMaxItemsPerPage = mMaxCountX * mMaxCountY;
98
99        mInflater = LayoutInflater.from(context);
100
101        mIsRtl = Utilities.isRtl(getResources());
102        setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
103
104        mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
105    }
106
107    public void setFolder(Folder folder) {
108        mFolder = folder;
109        mPageIndicator = folder.findViewById(R.id.folder_page_indicator);
110        initParentViews(folder);
111    }
112
113    /**
114     * Calculates the grid size such that {@param count} items can fit in the grid.
115     * The grid size is calculated such that countY <= countX and countX = ceil(sqrt(count)) while
116     * maintaining the restrictions of {@link #mMaxCountX} &amp; {@link #mMaxCountY}.
117     */
118    public static void calculateGridSize(int count, int countX, int countY, int maxCountX,
119            int maxCountY, int maxItemsPerPage, int[] out) {
120        boolean done;
121        int gridCountX = countX;
122        int gridCountY = countY;
123
124        if (count >= maxItemsPerPage) {
125            gridCountX = maxCountX;
126            gridCountY = maxCountY;
127            done = true;
128        } else {
129            done = false;
130        }
131
132        while (!done) {
133            int oldCountX = gridCountX;
134            int oldCountY = gridCountY;
135            if (gridCountX * gridCountY < count) {
136                // Current grid is too small, expand it
137                if ((gridCountX <= gridCountY || gridCountY == maxCountY)
138                        && gridCountX < maxCountX) {
139                    gridCountX++;
140                } else if (gridCountY < maxCountY) {
141                    gridCountY++;
142                }
143                if (gridCountY == 0) gridCountY++;
144            } else if ((gridCountY - 1) * gridCountX >= count && gridCountY >= gridCountX) {
145                gridCountY = Math.max(0, gridCountY - 1);
146            } else if ((gridCountX - 1) * gridCountY >= count) {
147                gridCountX = Math.max(0, gridCountX - 1);
148            }
149            done = gridCountX == oldCountX && gridCountY == oldCountY;
150        }
151
152        out[0] = gridCountX;
153        out[1] = gridCountY;
154    }
155
156    /**
157     * Sets up the grid size such that {@param count} items can fit in the grid.
158     */
159    public void setupContentDimensions(int count) {
160        mAllocatedContentSize = count;
161        calculateGridSize(count, mGridCountX, mGridCountY, mMaxCountX, mMaxCountY, mMaxItemsPerPage,
162                sTmpArray);
163        mGridCountX = sTmpArray[0];
164        mGridCountY = sTmpArray[1];
165
166        // Update grid size
167        for (int i = getPageCount() - 1; i >= 0; i--) {
168            getPageAt(i).setGridSize(mGridCountX, mGridCountY);
169        }
170    }
171
172    @Override
173    protected void dispatchDraw(Canvas canvas) {
174        mFocusIndicatorHelper.draw(canvas);
175        super.dispatchDraw(canvas);
176    }
177
178    /**
179     * Binds items to the layout.
180     */
181    public void bindItems(ArrayList<ShortcutInfo> items) {
182        ArrayList<View> icons = new ArrayList<>();
183        for (ShortcutInfo item : items) {
184            icons.add(createNewView(item));
185        }
186        arrangeChildren(icons, icons.size(), false);
187    }
188
189    public void allocateSpaceForRank(int rank) {
190        ArrayList<View> views = new ArrayList<>(mFolder.getItemsInReadingOrder());
191        views.add(rank, null);
192        arrangeChildren(views, views.size(), false);
193    }
194
195    /**
196     * Create space for a new item at the end, and returns the rank for that item.
197     * Also sets the current page to the last page.
198     */
199    public int allocateRankForNewItem() {
200        int rank = getItemCount();
201        allocateSpaceForRank(rank);
202        setCurrentPage(rank / mMaxItemsPerPage);
203        return rank;
204    }
205
206    public View createAndAddViewForRank(ShortcutInfo item, int rank) {
207        View icon = createNewView(item);
208        allocateSpaceForRank(rank);
209        addViewForRank(icon, item, rank);
210        return icon;
211    }
212
213    /**
214     * Adds the {@param view} to the layout based on {@param rank} and updated the position
215     * related attributes. It assumes that {@param item} is already attached to the view.
216     */
217    public void addViewForRank(View view, ShortcutInfo item, int rank) {
218        int pagePos = rank % mMaxItemsPerPage;
219        int pageNo = rank / mMaxItemsPerPage;
220
221        item.rank = rank;
222        item.cellX = pagePos % mGridCountX;
223        item.cellY = pagePos / mGridCountX;
224
225        CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
226        lp.cellX = item.cellX;
227        lp.cellY = item.cellY;
228        getPageAt(pageNo).addViewToCellLayout(
229                view, -1, mFolder.mLauncher.getViewIdForItem(item), lp, true);
230    }
231
232    @SuppressLint("InflateParams")
233    public View createNewView(ShortcutInfo item) {
234        final BubbleTextView textView = (BubbleTextView) mInflater.inflate(
235                R.layout.folder_application, null, false);
236        textView.applyFromShortcutInfo(item);
237        textView.setHapticFeedbackEnabled(false);
238        textView.setOnClickListener(ItemClickHandler.INSTANCE);
239        textView.setOnLongClickListener(mFolder);
240        textView.setOnFocusChangeListener(mFocusIndicatorHelper);
241
242        textView.setLayoutParams(new CellLayout.LayoutParams(
243                item.cellX, item.cellY, item.spanX, item.spanY));
244        return textView;
245    }
246
247    @Override
248    public CellLayout getPageAt(int index) {
249        return (CellLayout) getChildAt(index);
250    }
251
252    public CellLayout getCurrentCellLayout() {
253        return getPageAt(getNextPage());
254    }
255
256    private CellLayout createAndAddNewPage() {
257        DeviceProfile grid = Launcher.getLauncher(getContext()).getDeviceProfile();
258        CellLayout page = (CellLayout) mInflater.inflate(R.layout.folder_page, this, false);
259        page.setCellDimensions(grid.folderCellWidthPx, grid.folderCellHeightPx);
260        page.getShortcutsAndWidgets().setMotionEventSplittingEnabled(false);
261        page.setInvertIfRtl(true);
262        page.setGridSize(mGridCountX, mGridCountY);
263
264        addView(page, -1, generateDefaultLayoutParams());
265        return page;
266    }
267
268    @Override
269    protected int getChildGap() {
270        return getPaddingLeft() + getPaddingRight();
271    }
272
273    public void setFixedSize(int width, int height) {
274        width -= (getPaddingLeft() + getPaddingRight());
275        height -= (getPaddingTop() + getPaddingBottom());
276        for (int i = getChildCount() - 1; i >= 0; i --) {
277            ((CellLayout) getChildAt(i)).setFixedSize(width, height);
278        }
279    }
280
281    public void removeItem(View v) {
282        for (int i = getChildCount() - 1; i >= 0; i --) {
283            getPageAt(i).removeView(v);
284        }
285    }
286
287    @Override
288    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
289        super.onScrollChanged(l, t, oldl, oldt);
290        mPageIndicator.setScroll(l, mMaxScrollX);
291    }
292
293    /**
294     * Updates position and rank of all the children in the view.
295     * It essentially removes all views from all the pages and then adds them again in appropriate
296     * page.
297     *
298     * @param list the ordered list of children.
299     * @param itemCount if greater than the total children count, empty spaces are left
300     * at the end, otherwise it is ignored.
301     *
302     */
303    public void arrangeChildren(ArrayList<View> list, int itemCount) {
304        arrangeChildren(list, itemCount, true);
305    }
306
307    @SuppressLint("RtlHardcoded")
308    private void arrangeChildren(ArrayList<View> list, int itemCount, boolean saveChanges) {
309        ArrayList<CellLayout> pages = new ArrayList<>();
310        for (int i = 0; i < getChildCount(); i++) {
311            CellLayout page = (CellLayout) getChildAt(i);
312            page.removeAllViews();
313            pages.add(page);
314        }
315        setupContentDimensions(itemCount);
316
317        Iterator<CellLayout> pageItr = pages.iterator();
318        CellLayout currentPage = null;
319
320        int position = 0;
321        int newX, newY, rank;
322
323        FolderIconPreviewVerifier verifier = new FolderIconPreviewVerifier(
324                Launcher.getLauncher(getContext()).getDeviceProfile().inv);
325        rank = 0;
326        for (int i = 0; i < itemCount; i++) {
327            View v = list.size() > i ? list.get(i) : null;
328            if (currentPage == null || position >= mMaxItemsPerPage) {
329                // Next page
330                if (pageItr.hasNext()) {
331                    currentPage = pageItr.next();
332                } else {
333                    currentPage = createAndAddNewPage();
334                }
335                position = 0;
336            }
337
338            if (v != null) {
339                CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
340                newX = position % mGridCountX;
341                newY = position / mGridCountX;
342                ItemInfo info = (ItemInfo) v.getTag();
343                if (info.cellX != newX || info.cellY != newY || info.rank != rank) {
344                    info.cellX = newX;
345                    info.cellY = newY;
346                    info.rank = rank;
347                    if (saveChanges) {
348                        mFolder.mLauncher.getModelWriter().addOrMoveItemInDatabase(info,
349                                mFolder.mInfo.id, 0, info.cellX, info.cellY);
350                    }
351                }
352                lp.cellX = info.cellX;
353                lp.cellY = info.cellY;
354                currentPage.addViewToCellLayout(
355                        v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true);
356
357                if (verifier.isItemInPreview(rank) && v instanceof BubbleTextView) {
358                    ((BubbleTextView) v).verifyHighRes();
359                }
360            }
361
362            rank ++;
363            position++;
364        }
365
366        // Remove extra views.
367        boolean removed = false;
368        while (pageItr.hasNext()) {
369            removeView(pageItr.next());
370            removed = true;
371        }
372        if (removed) {
373            setCurrentPage(0);
374        }
375
376        setEnableOverscroll(getPageCount() > 1);
377
378        // Update footer
379        mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
380        // Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text.
381        mFolder.mFolderName.setGravity(getPageCount() > 1 ?
382                (mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
383    }
384
385    public int getDesiredWidth() {
386        return getPageCount() > 0 ?
387                (getPageAt(0).getDesiredWidth() + getPaddingLeft() + getPaddingRight()) : 0;
388    }
389
390    public int getDesiredHeight()  {
391        return  getPageCount() > 0 ?
392                (getPageAt(0).getDesiredHeight() + getPaddingTop() + getPaddingBottom()) : 0;
393    }
394
395    public int getItemCount() {
396        int lastPageIndex = getChildCount() - 1;
397        if (lastPageIndex < 0) {
398            // If there are no pages, nothing has yet been added to the folder.
399            return 0;
400        }
401        return getPageAt(lastPageIndex).getShortcutsAndWidgets().getChildCount()
402                + lastPageIndex * mMaxItemsPerPage;
403    }
404
405    /**
406     * @return the rank of the cell nearest to the provided pixel position.
407     */
408    public int findNearestArea(int pixelX, int pixelY) {
409        int pageIndex = getNextPage();
410        CellLayout page = getPageAt(pageIndex);
411        page.findNearestArea(pixelX, pixelY, 1, 1, sTmpArray);
412        if (mFolder.isLayoutRtl()) {
413            sTmpArray[0] = page.getCountX() - sTmpArray[0] - 1;
414        }
415        return Math.min(mAllocatedContentSize - 1,
416                pageIndex * mMaxItemsPerPage + sTmpArray[1] * mGridCountX + sTmpArray[0]);
417    }
418
419    public View getFirstItem() {
420        if (getChildCount() < 1) {
421            return null;
422        }
423        ShortcutAndWidgetContainer currContainer = getCurrentCellLayout().getShortcutsAndWidgets();
424        if (mGridCountX > 0) {
425            return currContainer.getChildAt(0, 0);
426        } else {
427            return currContainer.getChildAt(0);
428        }
429    }
430
431    public View getLastItem() {
432        if (getChildCount() < 1) {
433            return null;
434        }
435        ShortcutAndWidgetContainer currContainer = getCurrentCellLayout().getShortcutsAndWidgets();
436        int lastRank = currContainer.getChildCount() - 1;
437        if (mGridCountX > 0) {
438            return currContainer.getChildAt(lastRank % mGridCountX, lastRank / mGridCountX);
439        } else {
440            return currContainer.getChildAt(lastRank);
441        }
442    }
443
444    /**
445     * Iterates over all its items in a reading order.
446     * @return the view for which the operator returned true.
447     */
448    public View iterateOverItems(ItemOperator op) {
449        for (int k = 0 ; k < getChildCount(); k++) {
450            CellLayout page = getPageAt(k);
451            for (int j = 0; j < page.getCountY(); j++) {
452                for (int i = 0; i < page.getCountX(); i++) {
453                    View v = page.getChildAt(i, j);
454                    if ((v != null) && op.evaluate((ItemInfo) v.getTag(), v)) {
455                        return v;
456                    }
457                }
458            }
459        }
460        return null;
461    }
462
463    public String getAccessibilityDescription() {
464        return getContext().getString(R.string.folder_opened, mGridCountX, mGridCountY);
465    }
466
467    /**
468     * Sets the focus on the first visible child.
469     */
470    public void setFocusOnFirstChild() {
471        View firstChild = getCurrentCellLayout().getChildAt(0, 0);
472        if (firstChild != null) {
473            firstChild.requestFocus();
474        }
475    }
476
477    @Override
478    protected void notifyPageSwitchListener(int prevPage) {
479        super.notifyPageSwitchListener(prevPage);
480        if (mFolder != null) {
481            mFolder.updateTextViewFocus();
482        }
483    }
484
485    /**
486     * Scrolls the current view by a fraction
487     */
488    public void showScrollHint(int direction) {
489        float fraction = (direction == Folder.SCROLL_LEFT) ^ mIsRtl
490                ? -SCROLL_HINT_FRACTION : SCROLL_HINT_FRACTION;
491        int hint = (int) (fraction * getWidth());
492        int scroll = getScrollForPage(getNextPage()) + hint;
493        int delta = scroll - getScrollX();
494        if (delta != 0) {
495            mScroller.setInterpolator(Interpolators.DEACCEL);
496            mScroller.startScroll(getScrollX(), 0, delta, 0, Folder.SCROLL_HINT_DURATION);
497            invalidate();
498        }
499    }
500
501    public void clearScrollHint() {
502        if (getScrollX() != getScrollForPage(getNextPage())) {
503            snapToPage(getNextPage());
504        }
505    }
506
507    /**
508     * Finish animation all the views which are animating across pages
509     */
510    public void completePendingPageChanges() {
511        if (!mPendingAnimations.isEmpty()) {
512            ArrayMap<View, Runnable> pendingViews = new ArrayMap<>(mPendingAnimations);
513            for (Map.Entry<View, Runnable> e : pendingViews.entrySet()) {
514                e.getKey().animate().cancel();
515                e.getValue().run();
516            }
517        }
518    }
519
520    public boolean rankOnCurrentPage(int rank) {
521        int p = rank / mMaxItemsPerPage;
522        return p == getNextPage();
523    }
524
525    @Override
526    protected void onPageBeginTransition() {
527        super.onPageBeginTransition();
528        // Ensure that adjacent pages have high resolution icons
529        verifyVisibleHighResIcons(getCurrentPage() - 1);
530        verifyVisibleHighResIcons(getCurrentPage() + 1);
531    }
532
533    /**
534     * Ensures that all the icons on the given page are of high-res
535     */
536    public void verifyVisibleHighResIcons(int pageNo) {
537        CellLayout page = getPageAt(pageNo);
538        if (page != null) {
539            ShortcutAndWidgetContainer parent = page.getShortcutsAndWidgets();
540            for (int i = parent.getChildCount() - 1; i >= 0; i--) {
541                BubbleTextView icon = ((BubbleTextView) parent.getChildAt(i));
542                icon.verifyHighRes();
543                // Set the callback back to the actual icon, in case
544                // it was captured by the FolderIcon
545                Drawable d = icon.getCompoundDrawables()[1];
546                if (d != null) {
547                    d.setCallback(icon);
548                }
549            }
550        }
551    }
552
553    public int getAllocatedContentSize() {
554        return mAllocatedContentSize;
555    }
556
557    /**
558     * Reorders the items such that the {@param empty} spot moves to {@param target}
559     */
560    public void realTimeReorder(int empty, int target) {
561        completePendingPageChanges();
562        int delay = 0;
563        float delayAmount = START_VIEW_REORDER_DELAY;
564
565        // Animation only happens on the current page.
566        int pageToAnimate = getNextPage();
567
568        int pageT = target / mMaxItemsPerPage;
569        int pagePosT = target % mMaxItemsPerPage;
570
571        if (pageT != pageToAnimate) {
572            Log.e(TAG, "Cannot animate when the target cell is invisible");
573        }
574        int pagePosE = empty % mMaxItemsPerPage;
575        int pageE = empty / mMaxItemsPerPage;
576
577        int startPos, endPos;
578        int moveStart, moveEnd;
579        int direction;
580
581        if (target == empty) {
582            // No animation
583            return;
584        } else if (target > empty) {
585            // Items will move backwards to make room for the empty cell.
586            direction = 1;
587
588            // If empty cell is in a different page, move them instantly.
589            if (pageE < pageToAnimate) {
590                moveStart = empty;
591                // Instantly move the first item in the current page.
592                moveEnd = pageToAnimate * mMaxItemsPerPage;
593                // Animate the 2nd item in the current page, as the first item was already moved to
594                // the last page.
595                startPos = 0;
596            } else {
597                moveStart = moveEnd = -1;
598                startPos = pagePosE;
599            }
600
601            endPos = pagePosT;
602        } else {
603            // The items will move forward.
604            direction = -1;
605
606            if (pageE > pageToAnimate) {
607                // Move the items immediately.
608                moveStart = empty;
609                // Instantly move the last item in the current page.
610                moveEnd = (pageToAnimate + 1) * mMaxItemsPerPage - 1;
611
612                // Animations start with the second last item in the page
613                startPos = mMaxItemsPerPage - 1;
614            } else {
615                moveStart = moveEnd = -1;
616                startPos = pagePosE;
617            }
618
619            endPos = pagePosT;
620        }
621
622        // Instant moving views.
623        while (moveStart != moveEnd) {
624            int rankToMove = moveStart + direction;
625            int p = rankToMove / mMaxItemsPerPage;
626            int pagePos = rankToMove % mMaxItemsPerPage;
627            int x = pagePos % mGridCountX;
628            int y = pagePos / mGridCountX;
629
630            final CellLayout page = getPageAt(p);
631            final View v = page.getChildAt(x, y);
632            if (v != null) {
633                if (pageToAnimate != p) {
634                    page.removeView(v);
635                    addViewForRank(v, (ShortcutInfo) v.getTag(), moveStart);
636                } else {
637                    // Do a fake animation before removing it.
638                    final int newRank = moveStart;
639                    final float oldTranslateX = v.getTranslationX();
640
641                    Runnable endAction = new Runnable() {
642
643                        @Override
644                        public void run() {
645                            mPendingAnimations.remove(v);
646                            v.setTranslationX(oldTranslateX);
647                            ((CellLayout) v.getParent().getParent()).removeView(v);
648                            addViewForRank(v, (ShortcutInfo) v.getTag(), newRank);
649                        }
650                    };
651                    v.animate()
652                        .translationXBy((direction > 0 ^ mIsRtl) ? -v.getWidth() : v.getWidth())
653                        .setDuration(REORDER_ANIMATION_DURATION)
654                        .setStartDelay(0)
655                        .withEndAction(endAction);
656                    mPendingAnimations.put(v, endAction);
657                }
658            }
659            moveStart = rankToMove;
660        }
661
662        if ((endPos - startPos) * direction <= 0) {
663            // No animation
664            return;
665        }
666
667        CellLayout page = getPageAt(pageToAnimate);
668        for (int i = startPos; i != endPos; i += direction) {
669            int nextPos = i + direction;
670            View v = page.getChildAt(nextPos % mGridCountX, nextPos / mGridCountX);
671            if (v != null) {
672                ((ItemInfo) v.getTag()).rank -= direction;
673            }
674            if (page.animateChildToPosition(v, i % mGridCountX, i / mGridCountX,
675                    REORDER_ANIMATION_DURATION, delay, true, true)) {
676                delay += delayAmount;
677                delayAmount *= VIEW_REORDER_DELAY_FACTOR;
678            }
679        }
680    }
681
682    public int itemsPerPage() {
683        return mMaxItemsPerPage;
684    }
685}
686