• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view;
18 
19 import android.app.AppGlobals;
20 import android.content.Context;
21 import android.content.res.Configuration;
22 import android.content.res.Resources;
23 import android.os.RemoteException;
24 import android.provider.Settings;
25 import android.util.DisplayMetrics;
26 import android.util.SparseArray;
27 
28 /**
29  * Contains methods to standard constants used in the UI for timeouts, sizes, and distances.
30  */
31 public class ViewConfiguration {
32     /**
33      * Expected bit depth of the display panel.
34      *
35      * @hide
36      */
37     public static final float PANEL_BIT_DEPTH = 24;
38 
39     /**
40      * Minimum alpha required for a view to draw.
41      *
42      * @hide
43      */
44     public static final float ALPHA_THRESHOLD = 0.5f / PANEL_BIT_DEPTH;
45     /**
46      * @hide
47      */
48     public static final float ALPHA_THRESHOLD_INT = 0x7f / PANEL_BIT_DEPTH;
49 
50     /**
51      * Defines the width of the horizontal scrollbar and the height of the vertical scrollbar in
52      * pixels
53      */
54     private static final int SCROLL_BAR_SIZE = 10;
55 
56     /**
57      * Duration of the fade when scrollbars fade away in milliseconds
58      */
59     private static final int SCROLL_BAR_FADE_DURATION = 250;
60 
61     /**
62      * Default delay before the scrollbars fade in milliseconds
63      */
64     private static final int SCROLL_BAR_DEFAULT_DELAY = 300;
65 
66     /**
67      * Defines the length of the fading edges in pixels
68      */
69     private static final int FADING_EDGE_LENGTH = 12;
70 
71     /**
72      * Defines the duration in milliseconds of the pressed state in child
73      * components.
74      */
75     private static final int PRESSED_STATE_DURATION = 125;
76 
77     /**
78      * Defines the default duration in milliseconds before a press turns into
79      * a long press
80      */
81     private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500;
82 
83     /**
84      * Defines the time between successive key repeats in milliseconds.
85      */
86     private static final int KEY_REPEAT_DELAY = 50;
87 
88     /**
89      * Defines the duration in milliseconds a user needs to hold down the
90      * appropriate button to bring up the global actions dialog (power off,
91      * lock screen, etc).
92      */
93     private static final int GLOBAL_ACTIONS_KEY_TIMEOUT = 500;
94 
95     /**
96      * Defines the duration in milliseconds we will wait to see if a touch event
97      * is a tap or a scroll. If the user does not move within this interval, it is
98      * considered to be a tap.
99      */
100     private static final int TAP_TIMEOUT = 180;
101 
102     /**
103      * Defines the duration in milliseconds we will wait to see if a touch event
104      * is a jump tap. If the user does not complete the jump tap within this interval, it is
105      * considered to be a tap.
106      */
107     private static final int JUMP_TAP_TIMEOUT = 500;
108 
109     /**
110      * Defines the duration in milliseconds between the first tap's up event and
111      * the second tap's down event for an interaction to be considered a
112      * double-tap.
113      */
114     private static final int DOUBLE_TAP_TIMEOUT = 300;
115 
116     /**
117      * Defines the maximum duration in milliseconds between a touch pad
118      * touch and release for a given touch to be considered a tap (click) as
119      * opposed to a hover movement gesture.
120      */
121     private static final int HOVER_TAP_TIMEOUT = 150;
122 
123     /**
124      * Defines the maximum distance in pixels that a touch pad touch can move
125      * before being released for it to be considered a tap (click) as opposed
126      * to a hover movement gesture.
127      */
128     private static final int HOVER_TAP_SLOP = 20;
129 
130     /**
131      * Defines the duration in milliseconds we want to display zoom controls in response
132      * to a user panning within an application.
133      */
134     private static final int ZOOM_CONTROLS_TIMEOUT = 3000;
135 
136     /**
137      * Inset in pixels to look for touchable content when the user touches the edge of the screen
138      */
139     private static final int EDGE_SLOP = 12;
140 
141     /**
142      * Distance a touch can wander before we think the user is scrolling in pixels
143      */
144     private static final int TOUCH_SLOP = 16;
145 
146     /**
147      * Distance a touch can wander before we think the user is attempting a paged scroll
148      * (in dips)
149      */
150     private static final int PAGING_TOUCH_SLOP = TOUCH_SLOP * 2;
151 
152     /**
153      * Distance between the first touch and second touch to still be considered a double tap
154      */
155     private static final int DOUBLE_TAP_SLOP = 100;
156 
157     /**
158      * Distance a touch needs to be outside of a window's bounds for it to
159      * count as outside for purposes of dismissing the window.
160      */
161     private static final int WINDOW_TOUCH_SLOP = 16;
162 
163     /**
164      * Minimum velocity to initiate a fling, as measured in pixels per second
165      */
166     private static final int MINIMUM_FLING_VELOCITY = 50;
167 
168     /**
169      * Maximum velocity to initiate a fling, as measured in pixels per second
170      */
171     private static final int MAXIMUM_FLING_VELOCITY = 8000;
172 
173     /**
174      * Distance between a touch up event denoting the end of a touch exploration
175      * gesture and the touch up event of a subsequent tap for the latter tap to be
176      * considered as a tap i.e. to perform a click.
177      */
178     private static final int TOUCH_EXPLORATION_TAP_SLOP = 80;
179 
180     /**
181      * Delay before dispatching a recurring accessibility event in milliseconds.
182      * This delay guarantees that a recurring event will be send at most once
183      * during the {@link #SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS} time
184      * frame.
185      */
186     private static final long SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS = 400;
187 
188     /**
189      * The maximum size of View's drawing cache, expressed in bytes. This size
190      * should be at least equal to the size of the screen in ARGB888 format.
191      */
192     @Deprecated
193     private static final int MAXIMUM_DRAWING_CACHE_SIZE = 480 * 800 * 4; // ARGB8888
194 
195     /**
196      * The coefficient of friction applied to flings/scrolls.
197      */
198     private static final float SCROLL_FRICTION = 0.015f;
199 
200     /**
201      * Max distance to overscroll for edge effects
202      */
203     private static final int OVERSCROLL_DISTANCE = 0;
204 
205     /**
206      * Max distance to overfling for edge effects
207      */
208     private static final int OVERFLING_DISTANCE = 6;
209 
210     private final int mEdgeSlop;
211     private final int mFadingEdgeLength;
212     private final int mMinimumFlingVelocity;
213     private final int mMaximumFlingVelocity;
214     private final int mScrollbarSize;
215     private final int mTouchSlop;
216     private final int mPagingTouchSlop;
217     private final int mDoubleTapSlop;
218     private final int mScaledTouchExplorationTapSlop;
219     private final int mWindowTouchSlop;
220     private final int mMaximumDrawingCacheSize;
221     private final int mOverscrollDistance;
222     private final int mOverflingDistance;
223     private final boolean mFadingMarqueeEnabled;
224 
225     private boolean sHasPermanentMenuKey;
226     private boolean sHasPermanentMenuKeySet;
227 
228     static final SparseArray<ViewConfiguration> sConfigurations =
229             new SparseArray<ViewConfiguration>(2);
230 
231     /**
232      * @deprecated Use {@link android.view.ViewConfiguration#get(android.content.Context)} instead.
233      */
234     @Deprecated
ViewConfiguration()235     public ViewConfiguration() {
236         mEdgeSlop = EDGE_SLOP;
237         mFadingEdgeLength = FADING_EDGE_LENGTH;
238         mMinimumFlingVelocity = MINIMUM_FLING_VELOCITY;
239         mMaximumFlingVelocity = MAXIMUM_FLING_VELOCITY;
240         mScrollbarSize = SCROLL_BAR_SIZE;
241         mTouchSlop = TOUCH_SLOP;
242         mPagingTouchSlop = PAGING_TOUCH_SLOP;
243         mDoubleTapSlop = DOUBLE_TAP_SLOP;
244         mScaledTouchExplorationTapSlop = TOUCH_EXPLORATION_TAP_SLOP;
245         mWindowTouchSlop = WINDOW_TOUCH_SLOP;
246         //noinspection deprecation
247         mMaximumDrawingCacheSize = MAXIMUM_DRAWING_CACHE_SIZE;
248         mOverscrollDistance = OVERSCROLL_DISTANCE;
249         mOverflingDistance = OVERFLING_DISTANCE;
250         mFadingMarqueeEnabled = true;
251     }
252 
253     /**
254      * Creates a new configuration for the specified context. The configuration depends on
255      * various parameters of the context, like the dimension of the display or the density
256      * of the display.
257      *
258      * @param context The application context used to initialize this view configuration.
259      *
260      * @see #get(android.content.Context)
261      * @see android.util.DisplayMetrics
262      */
ViewConfiguration(Context context)263     private ViewConfiguration(Context context) {
264         final Resources res = context.getResources();
265         final DisplayMetrics metrics = res.getDisplayMetrics();
266         final Configuration config = res.getConfiguration();
267         final float density = metrics.density;
268         final float sizeAndDensity;
269         if (config.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_XLARGE)) {
270             sizeAndDensity = density * 1.5f;
271         } else {
272             sizeAndDensity = density;
273         }
274 
275         mEdgeSlop = (int) (sizeAndDensity * EDGE_SLOP + 0.5f);
276         mFadingEdgeLength = (int) (sizeAndDensity * FADING_EDGE_LENGTH + 0.5f);
277         mMinimumFlingVelocity = (int) (density * MINIMUM_FLING_VELOCITY + 0.5f);
278         mMaximumFlingVelocity = (int) (density * MAXIMUM_FLING_VELOCITY + 0.5f);
279         mScrollbarSize = (int) (density * SCROLL_BAR_SIZE + 0.5f);
280         mTouchSlop = (int) (sizeAndDensity * TOUCH_SLOP + 0.5f);
281         mPagingTouchSlop = (int) (sizeAndDensity * PAGING_TOUCH_SLOP + 0.5f);
282         mDoubleTapSlop = (int) (sizeAndDensity * DOUBLE_TAP_SLOP + 0.5f);
283         mScaledTouchExplorationTapSlop = (int) (density * TOUCH_EXPLORATION_TAP_SLOP + 0.5f);
284         mWindowTouchSlop = (int) (sizeAndDensity * WINDOW_TOUCH_SLOP + 0.5f);
285 
286         // Size of the screen in bytes, in ARGB_8888 format
287         mMaximumDrawingCacheSize = 4 * metrics.widthPixels * metrics.heightPixels;
288 
289         mOverscrollDistance = (int) (sizeAndDensity * OVERSCROLL_DISTANCE + 0.5f);
290         mOverflingDistance = (int) (sizeAndDensity * OVERFLING_DISTANCE + 0.5f);
291 
292         if (!sHasPermanentMenuKeySet) {
293             IWindowManager wm = Display.getWindowManager();
294             try {
295                 sHasPermanentMenuKey = wm.canStatusBarHide() && !wm.hasNavigationBar();
296                 sHasPermanentMenuKeySet = true;
297             } catch (RemoteException ex) {
298                 sHasPermanentMenuKey = false;
299             }
300         }
301 
302         mFadingMarqueeEnabled = res.getBoolean(
303                 com.android.internal.R.bool.config_ui_enableFadingMarquee);
304     }
305 
306     /**
307      * Returns a configuration for the specified context. The configuration depends on
308      * various parameters of the context, like the dimension of the display or the
309      * density of the display.
310      *
311      * @param context The application context used to initialize the view configuration.
312      */
get(Context context)313     public static ViewConfiguration get(Context context) {
314         final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
315         final int density = (int) (100.0f * metrics.density);
316 
317         ViewConfiguration configuration = sConfigurations.get(density);
318         if (configuration == null) {
319             configuration = new ViewConfiguration(context);
320             sConfigurations.put(density, configuration);
321         }
322 
323         return configuration;
324     }
325 
326     /**
327      * @return The width of the horizontal scrollbar and the height of the vertical
328      *         scrollbar in pixels
329      *
330      * @deprecated Use {@link #getScaledScrollBarSize()} instead.
331      */
332     @Deprecated
getScrollBarSize()333     public static int getScrollBarSize() {
334         return SCROLL_BAR_SIZE;
335     }
336 
337     /**
338      * @return The width of the horizontal scrollbar and the height of the vertical
339      *         scrollbar in pixels
340      */
getScaledScrollBarSize()341     public int getScaledScrollBarSize() {
342         return mScrollbarSize;
343     }
344 
345     /**
346      * @return Duration of the fade when scrollbars fade away in milliseconds
347      */
getScrollBarFadeDuration()348     public static int getScrollBarFadeDuration() {
349         return SCROLL_BAR_FADE_DURATION;
350     }
351 
352     /**
353      * @return Default delay before the scrollbars fade in milliseconds
354      */
getScrollDefaultDelay()355     public static int getScrollDefaultDelay() {
356         return SCROLL_BAR_DEFAULT_DELAY;
357     }
358 
359     /**
360      * @return the length of the fading edges in pixels
361      *
362      * @deprecated Use {@link #getScaledFadingEdgeLength()} instead.
363      */
364     @Deprecated
getFadingEdgeLength()365     public static int getFadingEdgeLength() {
366         return FADING_EDGE_LENGTH;
367     }
368 
369     /**
370      * @return the length of the fading edges in pixels
371      */
getScaledFadingEdgeLength()372     public int getScaledFadingEdgeLength() {
373         return mFadingEdgeLength;
374     }
375 
376     /**
377      * @return the duration in milliseconds of the pressed state in child
378      * components.
379      */
getPressedStateDuration()380     public static int getPressedStateDuration() {
381         return PRESSED_STATE_DURATION;
382     }
383 
384     /**
385      * @return the duration in milliseconds before a press turns into
386      * a long press
387      */
getLongPressTimeout()388     public static int getLongPressTimeout() {
389         return AppGlobals.getIntCoreSetting(Settings.Secure.LONG_PRESS_TIMEOUT,
390                 DEFAULT_LONG_PRESS_TIMEOUT);
391     }
392 
393     /**
394      * @return the time before the first key repeat in milliseconds.
395      */
getKeyRepeatTimeout()396     public static int getKeyRepeatTimeout() {
397         return getLongPressTimeout();
398     }
399 
400     /**
401      * @return the time between successive key repeats in milliseconds.
402      */
getKeyRepeatDelay()403     public static int getKeyRepeatDelay() {
404         return KEY_REPEAT_DELAY;
405     }
406 
407     /**
408      * @return the duration in milliseconds we will wait to see if a touch event
409      * is a tap or a scroll. If the user does not move within this interval, it is
410      * considered to be a tap.
411      */
getTapTimeout()412     public static int getTapTimeout() {
413         return TAP_TIMEOUT;
414     }
415 
416     /**
417      * @return the duration in milliseconds we will wait to see if a touch event
418      * is a jump tap. If the user does not move within this interval, it is
419      * considered to be a tap.
420      */
getJumpTapTimeout()421     public static int getJumpTapTimeout() {
422         return JUMP_TAP_TIMEOUT;
423     }
424 
425     /**
426      * @return the duration in milliseconds between the first tap's up event and
427      * the second tap's down event for an interaction to be considered a
428      * double-tap.
429      */
getDoubleTapTimeout()430     public static int getDoubleTapTimeout() {
431         return DOUBLE_TAP_TIMEOUT;
432     }
433 
434     /**
435      * @return the maximum duration in milliseconds between a touch pad
436      * touch and release for a given touch to be considered a tap (click) as
437      * opposed to a hover movement gesture.
438      * @hide
439      */
getHoverTapTimeout()440     public static int getHoverTapTimeout() {
441         return HOVER_TAP_TIMEOUT;
442     }
443 
444     /**
445      * @return the maximum distance in pixels that a touch pad touch can move
446      * before being released for it to be considered a tap (click) as opposed
447      * to a hover movement gesture.
448      * @hide
449      */
getHoverTapSlop()450     public static int getHoverTapSlop() {
451         return HOVER_TAP_SLOP;
452     }
453 
454     /**
455      * @return Inset in pixels to look for touchable content when the user touches the edge of the
456      *         screen
457      *
458      * @deprecated Use {@link #getScaledEdgeSlop()} instead.
459      */
460     @Deprecated
getEdgeSlop()461     public static int getEdgeSlop() {
462         return EDGE_SLOP;
463     }
464 
465     /**
466      * @return Inset in pixels to look for touchable content when the user touches the edge of the
467      *         screen
468      */
getScaledEdgeSlop()469     public int getScaledEdgeSlop() {
470         return mEdgeSlop;
471     }
472 
473     /**
474      * @return Distance a touch can wander before we think the user is scrolling in pixels
475      *
476      * @deprecated Use {@link #getScaledTouchSlop()} instead.
477      */
478     @Deprecated
getTouchSlop()479     public static int getTouchSlop() {
480         return TOUCH_SLOP;
481     }
482 
483     /**
484      * @return Distance a touch can wander before we think the user is scrolling in pixels
485      */
getScaledTouchSlop()486     public int getScaledTouchSlop() {
487         return mTouchSlop;
488     }
489 
490     /**
491      * @return Distance a touch can wander before we think the user is scrolling a full page
492      *         in dips
493      */
getScaledPagingTouchSlop()494     public int getScaledPagingTouchSlop() {
495         return mPagingTouchSlop;
496     }
497 
498     /**
499      * @return Distance between the first touch and second touch to still be
500      *         considered a double tap
501      * @deprecated Use {@link #getScaledDoubleTapSlop()} instead.
502      * @hide The only client of this should be GestureDetector, which needs this
503      *       for clients that still use its deprecated constructor.
504      */
505     @Deprecated
getDoubleTapSlop()506     public static int getDoubleTapSlop() {
507         return DOUBLE_TAP_SLOP;
508     }
509 
510     /**
511      * @return Distance between the first touch and second touch to still be
512      *         considered a double tap
513      */
getScaledDoubleTapSlop()514     public int getScaledDoubleTapSlop() {
515         return mDoubleTapSlop;
516     }
517 
518     /**
519      * @return Distance between a touch up event denoting the end of a touch exploration
520      * gesture and the touch up event of a subsequent tap for the latter tap to be
521      * considered as a tap i.e. to perform a click.
522      *
523      * @hide
524      */
getScaledTouchExplorationTapSlop()525     public int getScaledTouchExplorationTapSlop() {
526         return mScaledTouchExplorationTapSlop;
527     }
528 
529     /**
530      * Interval for dispatching a recurring accessibility event in milliseconds.
531      * This interval guarantees that a recurring event will be send at most once
532      * during the {@link #getSendRecurringAccessibilityEventsInterval()} time frame.
533      *
534      * @return The delay in milliseconds.
535      *
536      * @hide
537      */
getSendRecurringAccessibilityEventsInterval()538     public static long getSendRecurringAccessibilityEventsInterval() {
539         return SEND_RECURRING_ACCESSIBILITY_EVENTS_INTERVAL_MILLIS;
540     }
541 
542     /**
543      * @return Distance a touch must be outside the bounds of a window for it
544      * to be counted as outside the window for purposes of dismissing that
545      * window.
546      *
547      * @deprecated Use {@link #getScaledWindowTouchSlop()} instead.
548      */
549     @Deprecated
getWindowTouchSlop()550     public static int getWindowTouchSlop() {
551         return WINDOW_TOUCH_SLOP;
552     }
553 
554     /**
555      * @return Distance a touch must be outside the bounds of a window for it
556      * to be counted as outside the window for purposes of dismissing that
557      * window.
558      */
getScaledWindowTouchSlop()559     public int getScaledWindowTouchSlop() {
560         return mWindowTouchSlop;
561     }
562 
563     /**
564      * @return Minimum velocity to initiate a fling, as measured in pixels per second.
565      *
566      * @deprecated Use {@link #getScaledMinimumFlingVelocity()} instead.
567      */
568     @Deprecated
getMinimumFlingVelocity()569     public static int getMinimumFlingVelocity() {
570         return MINIMUM_FLING_VELOCITY;
571     }
572 
573     /**
574      * @return Minimum velocity to initiate a fling, as measured in pixels per second.
575      */
getScaledMinimumFlingVelocity()576     public int getScaledMinimumFlingVelocity() {
577         return mMinimumFlingVelocity;
578     }
579 
580     /**
581      * @return Maximum velocity to initiate a fling, as measured in pixels per second.
582      *
583      * @deprecated Use {@link #getScaledMaximumFlingVelocity()} instead.
584      */
585     @Deprecated
getMaximumFlingVelocity()586     public static int getMaximumFlingVelocity() {
587         return MAXIMUM_FLING_VELOCITY;
588     }
589 
590     /**
591      * @return Maximum velocity to initiate a fling, as measured in pixels per second.
592      */
getScaledMaximumFlingVelocity()593     public int getScaledMaximumFlingVelocity() {
594         return mMaximumFlingVelocity;
595     }
596 
597     /**
598      * The maximum drawing cache size expressed in bytes.
599      *
600      * @return the maximum size of View's drawing cache expressed in bytes
601      *
602      * @deprecated Use {@link #getScaledMaximumDrawingCacheSize()} instead.
603      */
604     @Deprecated
getMaximumDrawingCacheSize()605     public static int getMaximumDrawingCacheSize() {
606         //noinspection deprecation
607         return MAXIMUM_DRAWING_CACHE_SIZE;
608     }
609 
610     /**
611      * The maximum drawing cache size expressed in bytes.
612      *
613      * @return the maximum size of View's drawing cache expressed in bytes
614      */
getScaledMaximumDrawingCacheSize()615     public int getScaledMaximumDrawingCacheSize() {
616         return mMaximumDrawingCacheSize;
617     }
618 
619     /**
620      * @return The maximum distance a View should overscroll by when showing edge effects.
621      */
getScaledOverscrollDistance()622     public int getScaledOverscrollDistance() {
623         return mOverscrollDistance;
624     }
625 
626     /**
627      * @return The maximum distance a View should overfling by when showing edge effects.
628      */
getScaledOverflingDistance()629     public int getScaledOverflingDistance() {
630         return mOverflingDistance;
631     }
632 
633     /**
634      * The amount of time that the zoom controls should be
635      * displayed on the screen expressed in milliseconds.
636      *
637      * @return the time the zoom controls should be visible expressed
638      * in milliseconds.
639      */
getZoomControlsTimeout()640     public static long getZoomControlsTimeout() {
641         return ZOOM_CONTROLS_TIMEOUT;
642     }
643 
644     /**
645      * The amount of time a user needs to press the relevant key to bring up
646      * the global actions dialog.
647      *
648      * @return how long a user needs to press the relevant key to bring up
649      *   the global actions dialog.
650      */
getGlobalActionKeyTimeout()651     public static long getGlobalActionKeyTimeout() {
652         return GLOBAL_ACTIONS_KEY_TIMEOUT;
653     }
654 
655     /**
656      * The amount of friction applied to scrolls and flings.
657      *
658      * @return A scalar dimensionless value representing the coefficient of
659      *         friction.
660      */
getScrollFriction()661     public static float getScrollFriction() {
662         return SCROLL_FRICTION;
663     }
664 
665     /**
666      * Report if the device has a permanent menu key available to the user.
667      *
668      * <p>As of Android 3.0, devices may not have a permanent menu key available.
669      * Apps should use the action bar to present menu options to users.
670      * However, there are some apps where the action bar is inappropriate
671      * or undesirable. This method may be used to detect if a menu key is present.
672      * If not, applications should provide another on-screen affordance to access
673      * functionality.
674      *
675      * @return true if a permanent menu key is present, false otherwise.
676      */
hasPermanentMenuKey()677     public boolean hasPermanentMenuKey() {
678         return sHasPermanentMenuKey;
679     }
680 
681     /**
682      * @hide
683      * @return Whether or not marquee should use fading edges.
684      */
isFadingMarqueeEnabled()685     public boolean isFadingMarqueeEnabled() {
686         return mFadingMarqueeEnabled;
687     }
688 }
689