1/*
2 * Copyright (C) 2008 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 android.view;
18
19import android.content.Context;
20import android.os.Build;
21import android.os.Handler;
22import android.os.Message;
23
24/**
25 * Detects various gestures and events using the supplied {@link MotionEvent}s.
26 * The {@link OnGestureListener} callback will notify users when a particular
27 * motion event has occurred. This class should only be used with {@link MotionEvent}s
28 * reported via touch (don't use for trackball events).
29 *
30 * To use this class:
31 * <ul>
32 *  <li>Create an instance of the {@code GestureDetector} for your {@link View}
33 *  <li>In the {@link View#onTouchEvent(MotionEvent)} method ensure you call
34 *          {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback
35 *          will be executed when the events occur.
36 * </ul>
37 */
38public class GestureDetector {
39    /**
40     * The listener that is used to notify when gestures occur.
41     * If you want to listen for all the different gestures then implement
42     * this interface. If you only want to listen for a subset it might
43     * be easier to extend {@link SimpleOnGestureListener}.
44     */
45    public interface OnGestureListener {
46
47        /**
48         * Notified when a tap occurs with the down {@link MotionEvent}
49         * that triggered it. This will be triggered immediately for
50         * every down event. All other events should be preceded by this.
51         *
52         * @param e The down motion event.
53         */
54        boolean onDown(MotionEvent e);
55
56        /**
57         * The user has performed a down {@link MotionEvent} and not performed
58         * a move or up yet. This event is commonly used to provide visual
59         * feedback to the user to let them know that their action has been
60         * recognized i.e. highlight an element.
61         *
62         * @param e The down motion event
63         */
64        void onShowPress(MotionEvent e);
65
66        /**
67         * Notified when a tap occurs with the up {@link MotionEvent}
68         * that triggered it.
69         *
70         * @param e The up motion event that completed the first tap
71         * @return true if the event is consumed, else false
72         */
73        boolean onSingleTapUp(MotionEvent e);
74
75        /**
76         * Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
77         * current move {@link MotionEvent}. The distance in x and y is also supplied for
78         * convenience.
79         *
80         * @param e1 The first down motion event that started the scrolling.
81         * @param e2 The move motion event that triggered the current onScroll.
82         * @param distanceX The distance along the X axis that has been scrolled since the last
83         *              call to onScroll. This is NOT the distance between {@code e1}
84         *              and {@code e2}.
85         * @param distanceY The distance along the Y axis that has been scrolled since the last
86         *              call to onScroll. This is NOT the distance between {@code e1}
87         *              and {@code e2}.
88         * @return true if the event is consumed, else false
89         */
90        boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
91
92        /**
93         * Notified when a long press occurs with the initial on down {@link MotionEvent}
94         * that trigged it.
95         *
96         * @param e The initial on down motion event that started the longpress.
97         */
98        void onLongPress(MotionEvent e);
99
100        /**
101         * Notified of a fling event when it occurs with the initial on down {@link MotionEvent}
102         * and the matching up {@link MotionEvent}. The calculated velocity is supplied along
103         * the x and y axis in pixels per second.
104         *
105         * @param e1 The first down motion event that started the fling.
106         * @param e2 The move motion event that triggered the current onFling.
107         * @param velocityX The velocity of this fling measured in pixels per second
108         *              along the x axis.
109         * @param velocityY The velocity of this fling measured in pixels per second
110         *              along the y axis.
111         * @return true if the event is consumed, else false
112         */
113        boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);
114    }
115
116    /**
117     * The listener that is used to notify when a double-tap or a confirmed
118     * single-tap occur.
119     */
120    public interface OnDoubleTapListener {
121        /**
122         * Notified when a single-tap occurs.
123         * <p>
124         * Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
125         * will only be called after the detector is confident that the user's
126         * first tap is not followed by a second tap leading to a double-tap
127         * gesture.
128         *
129         * @param e The down motion event of the single-tap.
130         * @return true if the event is consumed, else false
131         */
132        boolean onSingleTapConfirmed(MotionEvent e);
133
134        /**
135         * Notified when a double-tap occurs.
136         *
137         * @param e The down motion event of the first tap of the double-tap.
138         * @return true if the event is consumed, else false
139         */
140        boolean onDoubleTap(MotionEvent e);
141
142        /**
143         * Notified when an event within a double-tap gesture occurs, including
144         * the down, move, and up events.
145         *
146         * @param e The motion event that occurred during the double-tap gesture.
147         * @return true if the event is consumed, else false
148         */
149        boolean onDoubleTapEvent(MotionEvent e);
150    }
151
152    /**
153     * A convenience class to extend when you only want to listen for a subset
154     * of all the gestures. This implements all methods in the
155     * {@link OnGestureListener} and {@link OnDoubleTapListener} but does
156     * nothing and return {@code false} for all applicable methods.
157     */
158    public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener {
159        public boolean onSingleTapUp(MotionEvent e) {
160            return false;
161        }
162
163        public void onLongPress(MotionEvent e) {
164        }
165
166        public boolean onScroll(MotionEvent e1, MotionEvent e2,
167                float distanceX, float distanceY) {
168            return false;
169        }
170
171        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
172                float velocityY) {
173            return false;
174        }
175
176        public void onShowPress(MotionEvent e) {
177        }
178
179        public boolean onDown(MotionEvent e) {
180            return false;
181        }
182
183        public boolean onDoubleTap(MotionEvent e) {
184            return false;
185        }
186
187        public boolean onDoubleTapEvent(MotionEvent e) {
188            return false;
189        }
190
191        public boolean onSingleTapConfirmed(MotionEvent e) {
192            return false;
193        }
194    }
195
196    private int mTouchSlopSquare;
197    private int mDoubleTapTouchSlopSquare;
198    private int mDoubleTapSlopSquare;
199    private int mMinimumFlingVelocity;
200    private int mMaximumFlingVelocity;
201
202    private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
203    private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
204    private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
205
206    // constants for Message.what used by GestureHandler below
207    private static final int SHOW_PRESS = 1;
208    private static final int LONG_PRESS = 2;
209    private static final int TAP = 3;
210
211    private final Handler mHandler;
212    private final OnGestureListener mListener;
213    private OnDoubleTapListener mDoubleTapListener;
214
215    private boolean mStillDown;
216    private boolean mInLongPress;
217    private boolean mAlwaysInTapRegion;
218    private boolean mAlwaysInBiggerTapRegion;
219
220    private MotionEvent mCurrentDownEvent;
221    private MotionEvent mPreviousUpEvent;
222
223    /**
224     * True when the user is still touching for the second tap (down, move, and
225     * up events). Can only be true if there is a double tap listener attached.
226     */
227    private boolean mIsDoubleTapping;
228
229    private float mLastFocusX;
230    private float mLastFocusY;
231    private float mDownFocusX;
232    private float mDownFocusY;
233
234    private boolean mIsLongpressEnabled;
235
236    /**
237     * Determines speed during touch scrolling
238     */
239    private VelocityTracker mVelocityTracker;
240
241    /**
242     * Consistency verifier for debugging purposes.
243     */
244    private final InputEventConsistencyVerifier mInputEventConsistencyVerifier =
245            InputEventConsistencyVerifier.isInstrumentationEnabled() ?
246                    new InputEventConsistencyVerifier(this, 0) : null;
247
248    private class GestureHandler extends Handler {
249        GestureHandler() {
250            super();
251        }
252
253        GestureHandler(Handler handler) {
254            super(handler.getLooper());
255        }
256
257        @Override
258        public void handleMessage(Message msg) {
259            switch (msg.what) {
260            case SHOW_PRESS:
261                mListener.onShowPress(mCurrentDownEvent);
262                break;
263
264            case LONG_PRESS:
265                dispatchLongPress();
266                break;
267
268            case TAP:
269                // If the user's finger is still down, do not count it as a tap
270                if (mDoubleTapListener != null && !mStillDown) {
271                    mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
272                }
273                break;
274
275            default:
276                throw new RuntimeException("Unknown message " + msg); //never
277            }
278        }
279    }
280
281    /**
282     * Creates a GestureDetector with the supplied listener.
283     * This variant of the constructor should be used from a non-UI thread
284     * (as it allows specifying the Handler).
285     *
286     * @param listener the listener invoked for all the callbacks, this must
287     * not be null.
288     * @param handler the handler to use
289     *
290     * @throws NullPointerException if either {@code listener} or
291     * {@code handler} is null.
292     *
293     * @deprecated Use {@link #GestureDetector(android.content.Context,
294     *      android.view.GestureDetector.OnGestureListener, android.os.Handler)} instead.
295     */
296    @Deprecated
297    public GestureDetector(OnGestureListener listener, Handler handler) {
298        this(null, listener, handler);
299    }
300
301    /**
302     * Creates a GestureDetector with the supplied listener.
303     * You may only use this constructor from a UI thread (this is the usual situation).
304     * @see android.os.Handler#Handler()
305     *
306     * @param listener the listener invoked for all the callbacks, this must
307     * not be null.
308     *
309     * @throws NullPointerException if {@code listener} is null.
310     *
311     * @deprecated Use {@link #GestureDetector(android.content.Context,
312     *      android.view.GestureDetector.OnGestureListener)} instead.
313     */
314    @Deprecated
315    public GestureDetector(OnGestureListener listener) {
316        this(null, listener, null);
317    }
318
319    /**
320     * Creates a GestureDetector with the supplied listener.
321     * You may only use this constructor from a UI thread (this is the usual situation).
322     * @see android.os.Handler#Handler()
323     *
324     * @param context the application's context
325     * @param listener the listener invoked for all the callbacks, this must
326     * not be null.
327     *
328     * @throws NullPointerException if {@code listener} is null.
329     */
330    public GestureDetector(Context context, OnGestureListener listener) {
331        this(context, listener, null);
332    }
333
334    /**
335     * Creates a GestureDetector with the supplied listener.
336     * You may only use this constructor from a UI thread (this is the usual situation).
337     * @see android.os.Handler#Handler()
338     *
339     * @param context the application's context
340     * @param listener the listener invoked for all the callbacks, this must
341     * not be null.
342     * @param handler the handler to use
343     *
344     * @throws NullPointerException if {@code listener} is null.
345     */
346    public GestureDetector(Context context, OnGestureListener listener, Handler handler) {
347        if (handler != null) {
348            mHandler = new GestureHandler(handler);
349        } else {
350            mHandler = new GestureHandler();
351        }
352        mListener = listener;
353        if (listener instanceof OnDoubleTapListener) {
354            setOnDoubleTapListener((OnDoubleTapListener) listener);
355        }
356        init(context);
357    }
358
359    /**
360     * Creates a GestureDetector with the supplied listener.
361     * You may only use this constructor from a UI thread (this is the usual situation).
362     * @see android.os.Handler#Handler()
363     *
364     * @param context the application's context
365     * @param listener the listener invoked for all the callbacks, this must
366     * not be null.
367     * @param handler the handler to use
368     *
369     * @throws NullPointerException if {@code listener} is null.
370     */
371    public GestureDetector(Context context, OnGestureListener listener, Handler handler,
372            boolean unused) {
373        this(context, listener, handler);
374    }
375
376    private void init(Context context) {
377        if (mListener == null) {
378            throw new NullPointerException("OnGestureListener must not be null");
379        }
380        mIsLongpressEnabled = true;
381
382        // Fallback to support pre-donuts releases
383        int touchSlop, doubleTapSlop, doubleTapTouchSlop;
384        if (context == null) {
385            //noinspection deprecation
386            touchSlop = ViewConfiguration.getTouchSlop();
387            doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this
388            doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
389            //noinspection deprecation
390            mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
391            mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
392        } else {
393            final ViewConfiguration configuration = ViewConfiguration.get(context);
394            touchSlop = configuration.getScaledTouchSlop();
395            doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
396            doubleTapSlop = configuration.getScaledDoubleTapSlop();
397            mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
398            mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity();
399        }
400        mTouchSlopSquare = touchSlop * touchSlop;
401        mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
402        mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
403    }
404
405    /**
406     * Sets the listener which will be called for double-tap and related
407     * gestures.
408     *
409     * @param onDoubleTapListener the listener invoked for all the callbacks, or
410     *        null to stop listening for double-tap gestures.
411     */
412    public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
413        mDoubleTapListener = onDoubleTapListener;
414    }
415
416    /**
417     * Set whether longpress is enabled, if this is enabled when a user
418     * presses and holds down you get a longpress event and nothing further.
419     * If it's disabled the user can press and hold down and then later
420     * moved their finger and you will get scroll events. By default
421     * longpress is enabled.
422     *
423     * @param isLongpressEnabled whether longpress should be enabled.
424     */
425    public void setIsLongpressEnabled(boolean isLongpressEnabled) {
426        mIsLongpressEnabled = isLongpressEnabled;
427    }
428
429    /**
430     * @return true if longpress is enabled, else false.
431     */
432    public boolean isLongpressEnabled() {
433        return mIsLongpressEnabled;
434    }
435
436    /**
437     * Analyzes the given motion event and if applicable triggers the
438     * appropriate callbacks on the {@link OnGestureListener} supplied.
439     *
440     * @param ev The current motion event.
441     * @return true if the {@link OnGestureListener} consumed the event,
442     *              else false.
443     */
444    public boolean onTouchEvent(MotionEvent ev) {
445        if (mInputEventConsistencyVerifier != null) {
446            mInputEventConsistencyVerifier.onTouchEvent(ev, 0);
447        }
448
449        final int action = ev.getAction();
450
451        if (mVelocityTracker == null) {
452            mVelocityTracker = VelocityTracker.obtain();
453        }
454        mVelocityTracker.addMovement(ev);
455
456        final boolean pointerUp =
457                (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_UP;
458        final int skipIndex = pointerUp ? ev.getActionIndex() : -1;
459
460        // Determine focal point
461        float sumX = 0, sumY = 0;
462        final int count = ev.getPointerCount();
463        for (int i = 0; i < count; i++) {
464            if (skipIndex == i) continue;
465            sumX += ev.getX(i);
466            sumY += ev.getY(i);
467        }
468        final int div = pointerUp ? count - 1 : count;
469        final float focusX = sumX / div;
470        final float focusY = sumY / div;
471
472        boolean handled = false;
473
474        switch (action & MotionEvent.ACTION_MASK) {
475        case MotionEvent.ACTION_POINTER_DOWN:
476            mDownFocusX = mLastFocusX = focusX;
477            mDownFocusY = mLastFocusY = focusY;
478            // Cancel long press and taps
479            cancelTaps();
480            break;
481
482        case MotionEvent.ACTION_POINTER_UP:
483            mDownFocusX = mLastFocusX = focusX;
484            mDownFocusY = mLastFocusY = focusY;
485
486            // Check the dot product of current velocities.
487            // If the pointer that left was opposing another velocity vector, clear.
488            mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
489            final int upIndex = ev.getActionIndex();
490            final int id1 = ev.getPointerId(upIndex);
491            final float x1 = mVelocityTracker.getXVelocity(id1);
492            final float y1 = mVelocityTracker.getYVelocity(id1);
493            for (int i = 0; i < count; i++) {
494                if (i == upIndex) continue;
495
496                final int id2 = ev.getPointerId(i);
497                final float x = x1 * mVelocityTracker.getXVelocity(id2);
498                final float y = y1 * mVelocityTracker.getYVelocity(id2);
499
500                final float dot = x + y;
501                if (dot < 0) {
502                    mVelocityTracker.clear();
503                    break;
504                }
505            }
506            break;
507
508        case MotionEvent.ACTION_DOWN:
509            if (mDoubleTapListener != null) {
510                boolean hadTapMessage = mHandler.hasMessages(TAP);
511                if (hadTapMessage) mHandler.removeMessages(TAP);
512                if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage &&
513                        isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) {
514                    // This is a second tap
515                    mIsDoubleTapping = true;
516                    // Give a callback with the first tap of the double-tap
517                    handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
518                    // Give a callback with down event of the double-tap
519                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
520                } else {
521                    // This is a first tap
522                    mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
523                }
524            }
525
526            mDownFocusX = mLastFocusX = focusX;
527            mDownFocusY = mLastFocusY = focusY;
528            if (mCurrentDownEvent != null) {
529                mCurrentDownEvent.recycle();
530            }
531            mCurrentDownEvent = MotionEvent.obtain(ev);
532            mAlwaysInTapRegion = true;
533            mAlwaysInBiggerTapRegion = true;
534            mStillDown = true;
535            mInLongPress = false;
536
537            if (mIsLongpressEnabled) {
538                mHandler.removeMessages(LONG_PRESS);
539                mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime()
540                        + TAP_TIMEOUT + LONGPRESS_TIMEOUT);
541            }
542            mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
543            handled |= mListener.onDown(ev);
544            break;
545
546        case MotionEvent.ACTION_MOVE:
547            if (mInLongPress) {
548                break;
549            }
550            final float scrollX = mLastFocusX - focusX;
551            final float scrollY = mLastFocusY - focusY;
552            if (mIsDoubleTapping) {
553                // Give the move events of the double-tap
554                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
555            } else if (mAlwaysInTapRegion) {
556                final int deltaX = (int) (focusX - mDownFocusX);
557                final int deltaY = (int) (focusY - mDownFocusY);
558                int distance = (deltaX * deltaX) + (deltaY * deltaY);
559                if (distance > mTouchSlopSquare) {
560                    handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
561                    mLastFocusX = focusX;
562                    mLastFocusY = focusY;
563                    mAlwaysInTapRegion = false;
564                    mHandler.removeMessages(TAP);
565                    mHandler.removeMessages(SHOW_PRESS);
566                    mHandler.removeMessages(LONG_PRESS);
567                }
568                if (distance > mDoubleTapTouchSlopSquare) {
569                    mAlwaysInBiggerTapRegion = false;
570                }
571            } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
572                handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
573                mLastFocusX = focusX;
574                mLastFocusY = focusY;
575            }
576            break;
577
578        case MotionEvent.ACTION_UP:
579            mStillDown = false;
580            MotionEvent currentUpEvent = MotionEvent.obtain(ev);
581            if (mIsDoubleTapping) {
582                // Finally, give the up event of the double-tap
583                handled |= mDoubleTapListener.onDoubleTapEvent(ev);
584            } else if (mInLongPress) {
585                mHandler.removeMessages(TAP);
586                mInLongPress = false;
587            } else if (mAlwaysInTapRegion) {
588                handled = mListener.onSingleTapUp(ev);
589            } else {
590
591                // A fling must travel the minimum tap distance
592                final VelocityTracker velocityTracker = mVelocityTracker;
593                final int pointerId = ev.getPointerId(0);
594                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
595                final float velocityY = velocityTracker.getYVelocity(pointerId);
596                final float velocityX = velocityTracker.getXVelocity(pointerId);
597
598                if ((Math.abs(velocityY) > mMinimumFlingVelocity)
599                        || (Math.abs(velocityX) > mMinimumFlingVelocity)){
600                    handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY);
601                }
602            }
603            if (mPreviousUpEvent != null) {
604                mPreviousUpEvent.recycle();
605            }
606            // Hold the event we obtained above - listeners may have changed the original.
607            mPreviousUpEvent = currentUpEvent;
608            if (mVelocityTracker != null) {
609                // This may have been cleared when we called out to the
610                // application above.
611                mVelocityTracker.recycle();
612                mVelocityTracker = null;
613            }
614            mIsDoubleTapping = false;
615            mHandler.removeMessages(SHOW_PRESS);
616            mHandler.removeMessages(LONG_PRESS);
617            break;
618
619        case MotionEvent.ACTION_CANCEL:
620            cancel();
621            break;
622        }
623
624        if (!handled && mInputEventConsistencyVerifier != null) {
625            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 0);
626        }
627        return handled;
628    }
629
630    private void cancel() {
631        mHandler.removeMessages(SHOW_PRESS);
632        mHandler.removeMessages(LONG_PRESS);
633        mHandler.removeMessages(TAP);
634        mVelocityTracker.recycle();
635        mVelocityTracker = null;
636        mIsDoubleTapping = false;
637        mStillDown = false;
638        mAlwaysInTapRegion = false;
639        mAlwaysInBiggerTapRegion = false;
640        if (mInLongPress) {
641            mInLongPress = false;
642        }
643    }
644
645    private void cancelTaps() {
646        mHandler.removeMessages(SHOW_PRESS);
647        mHandler.removeMessages(LONG_PRESS);
648        mHandler.removeMessages(TAP);
649        mIsDoubleTapping = false;
650        mAlwaysInTapRegion = false;
651        mAlwaysInBiggerTapRegion = false;
652        if (mInLongPress) {
653            mInLongPress = false;
654        }
655    }
656
657    private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
658            MotionEvent secondDown) {
659        if (!mAlwaysInBiggerTapRegion) {
660            return false;
661        }
662
663        if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) {
664            return false;
665        }
666
667        int deltaX = (int) firstDown.getX() - (int) secondDown.getX();
668        int deltaY = (int) firstDown.getY() - (int) secondDown.getY();
669        return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
670    }
671
672    private void dispatchLongPress() {
673        mHandler.removeMessages(TAP);
674        mInLongPress = true;
675        mListener.onLongPress(mCurrentDownEvent);
676    }
677}
678