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 */
16package com.android.launcher3.allapps;
17
18import android.annotation.SuppressLint;
19import android.content.Context;
20import android.content.Intent;
21import android.content.res.Resources;
22import android.graphics.Point;
23import android.graphics.Rect;
24import android.graphics.drawable.InsetDrawable;
25import android.support.v7.widget.RecyclerView;
26import android.text.Selection;
27import android.text.SpannableStringBuilder;
28import android.text.method.TextKeyListener;
29import android.util.AttributeSet;
30import android.view.KeyEvent;
31import android.view.MotionEvent;
32import android.view.View;
33import android.view.ViewConfiguration;
34import android.view.ViewGroup;
35import android.widget.LinearLayout;
36import com.android.launcher3.AppInfo;
37import com.android.launcher3.BaseContainerView;
38import com.android.launcher3.CellLayout;
39import com.android.launcher3.DeleteDropTarget;
40import com.android.launcher3.DeviceProfile;
41import com.android.launcher3.DragSource;
42import com.android.launcher3.DropTarget;
43import com.android.launcher3.Folder;
44import com.android.launcher3.ItemInfo;
45import com.android.launcher3.Launcher;
46import com.android.launcher3.LauncherTransitionable;
47import com.android.launcher3.R;
48import com.android.launcher3.Utilities;
49import com.android.launcher3.Workspace;
50import com.android.launcher3.util.ComponentKey;
51import com.android.launcher3.util.Thunk;
52
53import java.nio.charset.Charset;
54import java.nio.charset.CharsetEncoder;
55import java.util.ArrayList;
56import java.util.List;
57
58
59
60/**
61 * A merge algorithm that merges every section indiscriminately.
62 */
63final class FullMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm {
64
65    @Override
66    public boolean continueMerging(AlphabeticalAppsList.SectionInfo section,
67           AlphabeticalAppsList.SectionInfo withSection,
68           int sectionAppCount, int numAppsPerRow, int mergeCount) {
69        // Don't merge the predicted apps
70        if (section.firstAppItem.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) {
71            return false;
72        }
73        // Otherwise, merge every other section
74        return true;
75    }
76}
77
78/**
79 * The logic we use to merge multiple sections.  We only merge sections when their final row
80 * contains less than a certain number of icons, and stop at a specified max number of merges.
81 * In addition, we will try and not merge sections that identify apps from different scripts.
82 */
83final class SimpleSectionMergeAlgorithm implements AlphabeticalAppsList.MergeAlgorithm {
84
85    private int mMinAppsPerRow;
86    private int mMinRowsInMergedSection;
87    private int mMaxAllowableMerges;
88    private CharsetEncoder mAsciiEncoder;
89
90    public SimpleSectionMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) {
91        mMinAppsPerRow = minAppsPerRow;
92        mMinRowsInMergedSection = minRowsInMergedSection;
93        mMaxAllowableMerges = maxNumMerges;
94        mAsciiEncoder = Charset.forName("US-ASCII").newEncoder();
95    }
96
97    @Override
98    public boolean continueMerging(AlphabeticalAppsList.SectionInfo section,
99           AlphabeticalAppsList.SectionInfo withSection,
100           int sectionAppCount, int numAppsPerRow, int mergeCount) {
101        // Don't merge the predicted apps
102        if (section.firstAppItem.viewType != AllAppsGridAdapter.ICON_VIEW_TYPE) {
103            return false;
104        }
105
106        // Continue merging if the number of hanging apps on the final row is less than some
107        // fixed number (ragged), the merged rows has yet to exceed some minimum row count,
108        // and while the number of merged sections is less than some fixed number of merges
109        int rows = sectionAppCount / numAppsPerRow;
110        int cols = sectionAppCount % numAppsPerRow;
111
112        // Ensure that we do not merge across scripts, currently we only allow for english and
113        // native scripts so we can test if both can just be ascii encoded
114        boolean isCrossScript = false;
115        if (section.firstAppItem != null && withSection.firstAppItem != null) {
116            isCrossScript = mAsciiEncoder.canEncode(section.firstAppItem.sectionName) !=
117                    mAsciiEncoder.canEncode(withSection.firstAppItem.sectionName);
118        }
119        return (0 < cols && cols < mMinAppsPerRow) &&
120                rows < mMinRowsInMergedSection &&
121                mergeCount < mMaxAllowableMerges &&
122                !isCrossScript;
123    }
124}
125
126/**
127 * The all apps view container.
128 */
129public class AllAppsContainerView extends BaseContainerView implements DragSource,
130        LauncherTransitionable, View.OnTouchListener, View.OnLongClickListener,
131        AllAppsSearchBarController.Callbacks {
132
133    private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3;
134    private static final int MAX_NUM_MERGES_PHONE = 2;
135
136    @Thunk Launcher mLauncher;
137    @Thunk AlphabeticalAppsList mApps;
138    private AllAppsGridAdapter mAdapter;
139    private RecyclerView.LayoutManager mLayoutManager;
140    private RecyclerView.ItemDecoration mItemDecoration;
141
142    @Thunk View mContent;
143    @Thunk View mContainerView;
144    @Thunk View mRevealView;
145    @Thunk AllAppsRecyclerView mAppsRecyclerView;
146    @Thunk AllAppsSearchBarController mSearchBarController;
147    private ViewGroup mSearchBarContainerView;
148    private View mSearchBarView;
149    private SpannableStringBuilder mSearchQueryBuilder = null;
150
151    private int mSectionNamesMargin;
152    private int mNumAppsPerRow;
153    private int mNumPredictedAppsPerRow;
154    private int mRecyclerViewTopBottomPadding;
155    // This coordinate is relative to this container view
156    private final Point mBoundsCheckLastTouchDownPos = new Point(-1, -1);
157    // This coordinate is relative to its parent
158    private final Point mIconLastTouchPos = new Point();
159
160    private View.OnClickListener mSearchClickListener = new View.OnClickListener() {
161        @Override
162        public void onClick(View v) {
163            Intent searchIntent = (Intent) v.getTag();
164            mLauncher.startActivitySafely(v, searchIntent, null);
165        }
166    };
167
168    public AllAppsContainerView(Context context) {
169        this(context, null);
170    }
171
172    public AllAppsContainerView(Context context, AttributeSet attrs) {
173        this(context, attrs, 0);
174    }
175
176    public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
177        super(context, attrs, defStyleAttr);
178        Resources res = context.getResources();
179
180        mLauncher = (Launcher) context;
181        mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
182        mApps = new AlphabeticalAppsList(context);
183        mAdapter = new AllAppsGridAdapter(mLauncher, mApps, this, mLauncher, this);
184        mApps.setAdapter(mAdapter);
185        mLayoutManager = mAdapter.getLayoutManager();
186        mItemDecoration = mAdapter.getItemDecoration();
187        mRecyclerViewTopBottomPadding =
188                res.getDimensionPixelSize(R.dimen.all_apps_list_top_bottom_padding);
189
190        mSearchQueryBuilder = new SpannableStringBuilder();
191        Selection.setSelection(mSearchQueryBuilder, 0);
192    }
193
194    /**
195     * Sets the current set of predicted apps.
196     */
197    public void setPredictedApps(List<ComponentKey> apps) {
198        mApps.setPredictedApps(apps);
199    }
200
201    /**
202     * Sets the current set of apps.
203     */
204    public void setApps(List<AppInfo> apps) {
205        mApps.setApps(apps);
206    }
207
208    /**
209     * Adds new apps to the list.
210     */
211    public void addApps(List<AppInfo> apps) {
212        mApps.addApps(apps);
213    }
214
215    /**
216     * Updates existing apps in the list
217     */
218    public void updateApps(List<AppInfo> apps) {
219        mApps.updateApps(apps);
220    }
221
222    /**
223     * Removes some apps from the list.
224     */
225    public void removeApps(List<AppInfo> apps) {
226        mApps.removeApps(apps);
227    }
228
229    /**
230     * Sets the search bar that shows above the a-z list.
231     */
232    public void setSearchBarController(AllAppsSearchBarController searchController) {
233        if (mSearchBarController != null) {
234            throw new RuntimeException("Expected search bar controller to only be set once");
235        }
236        mSearchBarController = searchController;
237        mSearchBarController.initialize(mApps, this);
238
239        // Add the new search view to the layout
240        View searchBarView = searchController.getView(mSearchBarContainerView);
241        mSearchBarContainerView.addView(searchBarView);
242        mSearchBarContainerView.setVisibility(View.VISIBLE);
243        mSearchBarView = searchBarView;
244        setHasSearchBar();
245
246        updateBackgroundAndPaddings();
247    }
248
249    /**
250     * Scrolls this list view to the top.
251     */
252    public void scrollToTop() {
253        mAppsRecyclerView.scrollToTop();
254    }
255
256    /**
257     * Returns the content view used for the launcher transitions.
258     */
259    public View getContentView() {
260        return mContainerView;
261    }
262
263    /**
264     * Returns the all apps search view.
265     */
266    public View getSearchBarView() {
267        return mSearchBarView;
268    }
269
270    /**
271     * Returns the reveal view used for the launcher transitions.
272     */
273    public View getRevealView() {
274        return mRevealView;
275    }
276
277    /**
278     * Returns an new instance of the default app search controller.
279     */
280    public AllAppsSearchBarController newDefaultAppSearchController() {
281        return new DefaultAppSearchController(getContext(), this, mAppsRecyclerView);
282    }
283
284    /**
285     * Focuses the search field and begins an app search.
286     */
287    public void startAppsSearch() {
288        if (mSearchBarController != null) {
289            mSearchBarController.focusSearchField();
290        }
291    }
292
293    @Override
294    protected void onFinishInflate() {
295        super.onFinishInflate();
296        boolean isRtl = Utilities.isRtl(getResources());
297        mAdapter.setRtl(isRtl);
298        mContent = findViewById(R.id.content);
299
300        // This is a focus listener that proxies focus from a view into the list view.  This is to
301        // work around the search box from getting first focus and showing the cursor.
302        View.OnFocusChangeListener focusProxyListener = new View.OnFocusChangeListener() {
303            @Override
304            public void onFocusChange(View v, boolean hasFocus) {
305                if (hasFocus) {
306                    mAppsRecyclerView.requestFocus();
307                }
308            }
309        };
310        mSearchBarContainerView = (ViewGroup) findViewById(R.id.search_box_container);
311        mSearchBarContainerView.setOnFocusChangeListener(focusProxyListener);
312        mContainerView = findViewById(R.id.all_apps_container);
313        mContainerView.setOnFocusChangeListener(focusProxyListener);
314        mRevealView = findViewById(R.id.all_apps_reveal);
315
316        // Load the all apps recycler view
317        mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view);
318        mAppsRecyclerView.setApps(mApps);
319        mAppsRecyclerView.setLayoutManager(mLayoutManager);
320        mAppsRecyclerView.setAdapter(mAdapter);
321        mAppsRecyclerView.setHasFixedSize(true);
322        if (mItemDecoration != null) {
323            mAppsRecyclerView.addItemDecoration(mItemDecoration);
324        }
325
326        updateBackgroundAndPaddings();
327    }
328
329    @Override
330    public void onBoundsChanged(Rect newBounds) {
331        mLauncher.updateOverlayBounds(newBounds);
332    }
333
334    @Override
335    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
336        // Update the number of items in the grid before we measure the view
337        int availableWidth = !mContentBounds.isEmpty() ? mContentBounds.width() :
338                MeasureSpec.getSize(widthMeasureSpec);
339        DeviceProfile grid = mLauncher.getDeviceProfile();
340        grid.updateAppsViewNumCols(getResources(), availableWidth);
341        if (mNumAppsPerRow != grid.allAppsNumCols ||
342                mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) {
343            mNumAppsPerRow = grid.allAppsNumCols;
344            mNumPredictedAppsPerRow = grid.allAppsNumPredictiveCols;
345
346            // If there is a start margin to draw section names, determine how we are going to merge
347            // app sections
348            boolean mergeSectionsFully = mSectionNamesMargin == 0 || !grid.isPhone;
349            AlphabeticalAppsList.MergeAlgorithm mergeAlgorithm = mergeSectionsFully ?
350                    new FullMergeAlgorithm() :
351                    new SimpleSectionMergeAlgorithm((int) Math.ceil(mNumAppsPerRow / 2f),
352                            MIN_ROWS_IN_MERGED_SECTION_PHONE, MAX_NUM_MERGES_PHONE);
353
354            mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow);
355            mAdapter.setNumAppsPerRow(mNumAppsPerRow);
356            mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow, mergeAlgorithm);
357        }
358
359        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
360    }
361
362    /**
363     * Update the background and padding of the Apps view and children.  Instead of insetting the
364     * container view, we inset the background and padding of the recycler view to allow for the
365     * recycler view to handle touch events (for fast scrolling) all the way to the edge.
366     */
367    @Override
368    protected void onUpdateBackgroundAndPaddings(Rect searchBarBounds, Rect padding) {
369        boolean isRtl = Utilities.isRtl(getResources());
370
371        // TODO: Use quantum_panel instead of quantum_panel_shape
372        InsetDrawable background = new InsetDrawable(
373                getResources().getDrawable(R.drawable.quantum_panel_shape), padding.left, 0,
374                padding.right, 0);
375        Rect bgPadding = new Rect();
376        background.getPadding(bgPadding);
377        mContainerView.setBackground(background);
378        mRevealView.setBackground(background.getConstantState().newDrawable());
379        mAppsRecyclerView.updateBackgroundPadding(bgPadding);
380        mAdapter.updateBackgroundPadding(bgPadding);
381
382        // Hack: We are going to let the recycler view take the full width, so reset the padding on
383        // the container to zero after setting the background and apply the top-bottom padding to
384        // the content view instead so that the launcher transition clips correctly.
385        mContent.setPadding(0, padding.top, 0, padding.bottom);
386        mContainerView.setPadding(0, 0, 0, 0);
387
388        // Pad the recycler view by the background padding plus the start margin (for the section
389        // names)
390        int startInset = Math.max(mSectionNamesMargin, mAppsRecyclerView.getMaxScrollbarWidth());
391        int topBottomPadding = mRecyclerViewTopBottomPadding;
392        if (isRtl) {
393            mAppsRecyclerView.setPadding(padding.left + mAppsRecyclerView.getMaxScrollbarWidth(),
394                    topBottomPadding, padding.right + startInset, topBottomPadding);
395        } else {
396            mAppsRecyclerView.setPadding(padding.left + startInset, topBottomPadding,
397                    padding.right + mAppsRecyclerView.getMaxScrollbarWidth(), topBottomPadding);
398        }
399
400        // Inset the search bar to fit its bounds above the container
401        if (mSearchBarView != null) {
402            Rect backgroundPadding = new Rect();
403            if (mSearchBarView.getBackground() != null) {
404                mSearchBarView.getBackground().getPadding(backgroundPadding);
405            }
406            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
407                    mSearchBarContainerView.getLayoutParams();
408            lp.leftMargin = searchBarBounds.left - backgroundPadding.left;
409            lp.topMargin = searchBarBounds.top - backgroundPadding.top;
410            lp.rightMargin = (getMeasuredWidth() - searchBarBounds.right) - backgroundPadding.right;
411            mSearchBarContainerView.requestLayout();
412        }
413    }
414
415    @Override
416    public boolean dispatchKeyEvent(KeyEvent event) {
417        // Determine if the key event was actual text, if so, focus the search bar and then dispatch
418        // the key normally so that it can process this key event
419        if (!mSearchBarController.isSearchFieldFocused() &&
420                event.getAction() == KeyEvent.ACTION_DOWN) {
421            final int unicodeChar = event.getUnicodeChar();
422            final boolean isKeyNotWhitespace = unicodeChar > 0 &&
423                    !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar);
424            if (isKeyNotWhitespace) {
425                boolean gotKey = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder,
426                        event.getKeyCode(), event);
427                if (gotKey && mSearchQueryBuilder.length() > 0) {
428                    mSearchBarController.focusSearchField();
429                }
430            }
431        }
432
433        return super.dispatchKeyEvent(event);
434    }
435
436    @Override
437    public boolean onInterceptTouchEvent(MotionEvent ev) {
438        return handleTouchEvent(ev);
439    }
440
441    @SuppressLint("ClickableViewAccessibility")
442    @Override
443    public boolean onTouchEvent(MotionEvent ev) {
444        return handleTouchEvent(ev);
445    }
446
447    @SuppressLint("ClickableViewAccessibility")
448    @Override
449    public boolean onTouch(View v, MotionEvent ev) {
450        switch (ev.getAction()) {
451            case MotionEvent.ACTION_DOWN:
452            case MotionEvent.ACTION_MOVE:
453                mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
454                break;
455        }
456        return false;
457    }
458
459    @Override
460    public boolean onLongClick(View v) {
461        // Return early if this is not initiated from a touch
462        if (!v.isInTouchMode()) return false;
463        // When we have exited all apps or are in transition, disregard long clicks
464        if (!mLauncher.isAppsViewVisible() ||
465                mLauncher.getWorkspace().isSwitchingState()) return false;
466        // Return if global dragging is not enabled
467        if (!mLauncher.isDraggingEnabled()) return false;
468
469        // Start the drag
470        mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false);
471        // Enter spring loaded mode
472        mLauncher.enterSpringLoadedDragMode();
473
474        return false;
475    }
476
477    @Override
478    public boolean supportsFlingToDelete() {
479        return true;
480    }
481
482    @Override
483    public boolean supportsAppInfoDropTarget() {
484        return true;
485    }
486
487    @Override
488    public boolean supportsDeleteDropTarget() {
489        return false;
490    }
491
492    @Override
493    public float getIntrinsicIconScaleFactor() {
494        DeviceProfile grid = mLauncher.getDeviceProfile();
495        return (float) grid.allAppsIconSizePx / grid.iconSizePx;
496    }
497
498    @Override
499    public void onFlingToDeleteCompleted() {
500        // We just dismiss the drag when we fling, so cleanup here
501        mLauncher.exitSpringLoadedDragModeDelayed(true,
502                Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
503        mLauncher.unlockScreenOrientation(false);
504    }
505
506    @Override
507    public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete,
508            boolean success) {
509        if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() &&
510                !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) {
511            // Exit spring loaded mode if we have not successfully dropped or have not handled the
512            // drop in Workspace
513            mLauncher.exitSpringLoadedDragModeDelayed(true,
514                    Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
515        }
516        mLauncher.unlockScreenOrientation(false);
517
518        // Display an error message if the drag failed due to there not being enough space on the
519        // target layout we were dropping on.
520        if (!success) {
521            boolean showOutOfSpaceMessage = false;
522            if (target instanceof Workspace) {
523                int currentScreen = mLauncher.getCurrentWorkspaceScreen();
524                Workspace workspace = (Workspace) target;
525                CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen);
526                ItemInfo itemInfo = (ItemInfo) d.dragInfo;
527                if (layout != null) {
528                    showOutOfSpaceMessage =
529                            !layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY);
530                }
531            }
532            if (showOutOfSpaceMessage) {
533                mLauncher.showOutOfSpaceMessage(false);
534            }
535
536            d.deferDragViewCleanupPostAnimation = false;
537        }
538    }
539
540    @Override
541    public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) {
542        // Do nothing
543    }
544
545    @Override
546    public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) {
547        // Do nothing
548    }
549
550    @Override
551    public void onLauncherTransitionStep(Launcher l, float t) {
552        // Do nothing
553    }
554
555    @Override
556    public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
557        if (toWorkspace) {
558            // Reset the search bar and base recycler view after transitioning home
559            mSearchBarController.reset();
560            mAppsRecyclerView.reset();
561        }
562    }
563
564    /**
565     * Handles the touch events to dismiss all apps when clicking outside the bounds of the
566     * recycler view.
567     */
568    private boolean handleTouchEvent(MotionEvent ev) {
569        DeviceProfile grid = mLauncher.getDeviceProfile();
570        int x = (int) ev.getX();
571        int y = (int) ev.getY();
572
573        switch (ev.getAction()) {
574            case MotionEvent.ACTION_DOWN:
575                if (!mContentBounds.isEmpty()) {
576                    // Outset the fixed bounds and check if the touch is outside all apps
577                    Rect tmpRect = new Rect(mContentBounds);
578                    tmpRect.inset(-grid.allAppsIconSizePx / 2, 0);
579                    if (ev.getX() < tmpRect.left || ev.getX() > tmpRect.right) {
580                        mBoundsCheckLastTouchDownPos.set(x, y);
581                        return true;
582                    }
583                } else {
584                    // Check if the touch is outside all apps
585                    if (ev.getX() < getPaddingLeft() ||
586                            ev.getX() > (getWidth() - getPaddingRight())) {
587                        mBoundsCheckLastTouchDownPos.set(x, y);
588                        return true;
589                    }
590                }
591                break;
592            case MotionEvent.ACTION_UP:
593                if (mBoundsCheckLastTouchDownPos.x > -1) {
594                    ViewConfiguration viewConfig = ViewConfiguration.get(getContext());
595                    float dx = ev.getX() - mBoundsCheckLastTouchDownPos.x;
596                    float dy = ev.getY() - mBoundsCheckLastTouchDownPos.y;
597                    float distance = (float) Math.hypot(dx, dy);
598                    if (distance < viewConfig.getScaledTouchSlop()) {
599                        // The background was clicked, so just go home
600                        Launcher launcher = (Launcher) getContext();
601                        launcher.showWorkspace(true);
602                        return true;
603                    }
604                }
605                // Fall through
606            case MotionEvent.ACTION_CANCEL:
607                mBoundsCheckLastTouchDownPos.set(-1, -1);
608                break;
609        }
610        return false;
611    }
612
613    @Override
614    public void onSearchResult(String query, ArrayList<ComponentKey> apps) {
615        if (apps != null) {
616            mApps.setOrderedFilter(apps);
617            mAdapter.setLastSearchQuery(query);
618            mAppsRecyclerView.onSearchResultsChanged();
619        }
620    }
621
622    @Override
623    public void clearSearchResult() {
624        mApps.setOrderedFilter(null);
625        mAppsRecyclerView.onSearchResultsChanged();
626
627        // Clear the search query
628        mSearchQueryBuilder.clear();
629        mSearchQueryBuilder.clearSpans();
630        Selection.setSelection(mSearchQueryBuilder, 0);
631    }
632}
633