StatusBarHeaderView.java revision 07f6d3b6dd41ff9716bab9c0fbc689650fc7c24f
1/*
2 * Copyright (C) 2014 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.systemui.statusbar.phone;
18
19import android.content.Context;
20import android.content.Intent;
21import android.graphics.Outline;
22import android.graphics.Rect;
23import android.graphics.drawable.Drawable;
24import android.util.AttributeSet;
25import android.view.View;
26import android.view.ViewGroup;
27import android.view.ViewOutlineProvider;
28import android.widget.ImageView;
29import android.widget.LinearLayout;
30import android.widget.RelativeLayout;
31import android.widget.Switch;
32import android.widget.TextView;
33
34import com.android.systemui.R;
35import com.android.systemui.qs.QSPanel;
36import com.android.systemui.qs.QSTile;
37import com.android.systemui.statusbar.policy.BatteryController;
38import com.android.systemui.statusbar.policy.UserInfoController;
39
40/**
41 * The view to manage the header area in the expanded status bar.
42 */
43public class StatusBarHeaderView extends RelativeLayout implements View.OnClickListener,
44        BatteryController.BatteryStateChangeCallback {
45
46    private boolean mExpanded;
47    private boolean mListening;
48    private boolean mOverscrolled;
49    private boolean mKeyguardShowing;
50    private boolean mCharging;
51
52    private ViewGroup mSystemIconsContainer;
53    private View mSystemIconsSuperContainer;
54    private View mDateTime;
55    private View mTime;
56    private View mAmPm;
57    private View mKeyguardCarrierText;
58    private MultiUserSwitch mMultiUserSwitch;
59    private ImageView mMultiUserAvatar;
60    private View mDateCollapsed;
61    private View mDateExpanded;
62    private View mStatusIcons;
63    private View mSignalCluster;
64    private View mSettingsButton;
65    private View mQsDetailHeader;
66    private TextView mQsDetailHeaderTitle;
67    private Switch mQsDetailHeaderSwitch;
68    private View mEmergencyCallsOnly;
69    private TextView mBatteryLevel;
70
71    private boolean mShowEmergencyCallsOnly;
72    private boolean mKeyguardUserSwitcherShowing;
73
74    private int mCollapsedHeight;
75    private int mExpandedHeight;
76    private int mKeyguardHeight;
77
78    private int mKeyguardWidth = ViewGroup.LayoutParams.MATCH_PARENT;
79    private int mNormalWidth;
80    private int mPadding;
81    private int mMultiUserExpandedMargin;
82    private int mMultiUserCollapsedMargin;
83    private int mMultiUserKeyguardMargin;
84    private int mSystemIconsSwitcherHiddenExpandedMargin;
85    private int mClockMarginBottomExpanded;
86    private int mMultiUserSwitchWidthCollapsed;
87    private int mMultiUserSwitchWidthExpanded;
88    private int mBatteryPaddingEnd;
89
90    /**
91     * In collapsed QS, the clock and avatar are scaled down a bit post-layout to allow for a nice
92     * transition. These values determine that factor.
93     */
94    private float mClockCollapsedScaleFactor;
95    private float mAvatarCollapsedScaleFactor;
96
97    private ActivityStarter mActivityStarter;
98    private BatteryController mBatteryController;
99    private QSPanel mQSPanel;
100
101    private final Rect mClipBounds = new Rect();
102
103    public StatusBarHeaderView(Context context, AttributeSet attrs) {
104        super(context, attrs);
105    }
106
107    @Override
108    protected void onFinishInflate() {
109        super.onFinishInflate();
110        mSystemIconsSuperContainer = findViewById(R.id.system_icons_super_container);
111        mSystemIconsContainer = (ViewGroup) findViewById(R.id.system_icons_container);
112        mSystemIconsSuperContainer.setOnClickListener(this);
113        mDateTime = findViewById(R.id.datetime);
114        mTime = findViewById(R.id.time_view);
115        mAmPm = findViewById(R.id.am_pm_view);
116        mKeyguardCarrierText = findViewById(R.id.keyguard_carrier_text);
117        mMultiUserSwitch = (MultiUserSwitch) findViewById(R.id.multi_user_switch);
118        mMultiUserAvatar = (ImageView) findViewById(R.id.multi_user_avatar);
119        mDateCollapsed = findViewById(R.id.date_collapsed);
120        mDateExpanded = findViewById(R.id.date_expanded);
121        mSettingsButton = findViewById(R.id.settings_button);
122        mSettingsButton.setOnClickListener(this);
123        mQsDetailHeader = findViewById(R.id.qs_detail_header);
124        mQsDetailHeader.setAlpha(0);
125        mQsDetailHeaderTitle = (TextView) mQsDetailHeader.findViewById(android.R.id.title);
126        mQsDetailHeaderSwitch = (Switch) mQsDetailHeader.findViewById(android.R.id.toggle);
127        mEmergencyCallsOnly = findViewById(R.id.header_emergency_calls_only);
128        mBatteryLevel = (TextView) findViewById(R.id.battery_level);
129        loadDimens();
130        updateVisibilities();
131        updateClockScale();
132        updateAvatarScale();
133        addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
134            @Override
135            public void onLayoutChange(View v, int left, int top, int right,
136                    int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
137                if ((right - left) != (oldRight - oldLeft)) {
138                    // width changed, update clipping
139                    setClipping(getHeight());
140                }
141                boolean rtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
142                mTime.setPivotX(rtl ? mTime.getWidth() : 0);
143                mTime.setPivotY(mTime.getBaseline());
144                mAmPm.setPivotX(rtl ? mAmPm.getWidth() : 0);
145                mAmPm.setPivotY(mAmPm.getBaseline());
146                updateAmPmTranslation();
147            }
148        });
149        setOutlineProvider(new ViewOutlineProvider() {
150            @Override
151            public boolean getOutline(View view, Outline outline) {
152                outline.setRect(mClipBounds);
153                return true;
154            }
155        });
156    }
157
158    private void loadDimens() {
159        mCollapsedHeight = getResources().getDimensionPixelSize(R.dimen.status_bar_header_height);
160        mExpandedHeight = getResources().getDimensionPixelSize(
161                R.dimen.status_bar_header_height_expanded);
162        mKeyguardHeight = getResources().getDimensionPixelSize(
163                R.dimen.status_bar_header_height_keyguard);
164        mNormalWidth = getLayoutParams().width;
165        mPadding = getResources().getDimensionPixelSize(R.dimen.notification_side_padding);
166        mMultiUserExpandedMargin =
167                getResources().getDimensionPixelSize(R.dimen.multi_user_switch_expanded_margin);
168        mMultiUserCollapsedMargin =
169                getResources().getDimensionPixelSize(R.dimen.multi_user_switch_collapsed_margin);
170        mMultiUserKeyguardMargin =
171                getResources().getDimensionPixelSize(R.dimen.multi_user_switch_keyguard_margin);
172        mSystemIconsSwitcherHiddenExpandedMargin = getResources().getDimensionPixelSize(
173                R.dimen.system_icons_switcher_hidden_expanded_margin);
174        mClockMarginBottomExpanded =
175                getResources().getDimensionPixelSize(R.dimen.clock_expanded_bottom_margin);
176        mMultiUserSwitchWidthCollapsed =
177                getResources().getDimensionPixelSize(R.dimen.multi_user_switch_width_collapsed);
178        mMultiUserSwitchWidthExpanded =
179                getResources().getDimensionPixelSize(R.dimen.multi_user_switch_width_expanded);
180        mAvatarCollapsedScaleFactor =
181                getResources().getDimensionPixelSize(R.dimen.multi_user_avatar_collapsed_size)
182                / (float) mMultiUserAvatar.getLayoutParams().width;
183        mClockCollapsedScaleFactor =
184                (float) getResources().getDimensionPixelSize(R.dimen.qs_time_collapsed_size)
185                / (float) getResources().getDimensionPixelSize(R.dimen.qs_time_expanded_size);
186        mBatteryPaddingEnd =
187                getResources().getDimensionPixelSize(R.dimen.battery_level_padding_end);
188    }
189
190    public void setActivityStarter(ActivityStarter activityStarter) {
191        mActivityStarter = activityStarter;
192    }
193
194    public void setBatteryController(BatteryController batteryController) {
195        mBatteryController = batteryController;
196    }
197
198    public int getCollapsedHeight() {
199        return mKeyguardShowing ? mKeyguardHeight : mCollapsedHeight;
200    }
201
202    public int getExpandedHeight() {
203        return mExpandedHeight;
204    }
205
206    public void setListening(boolean listening) {
207        if (listening == mListening) {
208            return;
209        }
210        mListening = listening;
211        updateBatteryListening();
212    }
213
214    public void setExpanded(boolean expanded, boolean overscrolled) {
215        boolean changed = expanded != mExpanded;
216        boolean overscrollChanged = overscrolled != mOverscrolled;
217        mExpanded = expanded;
218        mOverscrolled = overscrolled;
219        if (changed || overscrollChanged) {
220            updateHeights();
221            updateVisibilities();
222            updateSystemIconsLayoutParams();
223            updateZTranslation();
224            updateClickTargets();
225            updateWidth();
226            updatePadding();
227            updateMultiUserSwitch();
228            if (mQSPanel != null) {
229                mQSPanel.setExpanded(expanded && !overscrolled);
230            }
231            updateClockScale();
232            updateAvatarScale();
233            updateClockLp();
234            updateBatteryLevelPaddingEnd();
235        }
236    }
237
238    private void updateHeights() {
239        boolean onKeyguardAndCollapsed = mKeyguardShowing && !mExpanded;
240        int height;
241        if (mExpanded) {
242            height = mExpandedHeight;
243        } else if (onKeyguardAndCollapsed) {
244            height = mKeyguardHeight;
245        } else {
246            height = mCollapsedHeight;
247        }
248        ViewGroup.LayoutParams lp = getLayoutParams();
249        if (lp.height != height) {
250            lp.height = height;
251            setLayoutParams(lp);
252        }
253        int systemIconsContainerHeight = onKeyguardAndCollapsed ? mKeyguardHeight : mCollapsedHeight;
254        lp = mSystemIconsSuperContainer.getLayoutParams();
255        if (lp.height != systemIconsContainerHeight) {
256            lp.height = systemIconsContainerHeight;
257            mSystemIconsSuperContainer.setLayoutParams(lp);
258        }
259        lp = mMultiUserSwitch.getLayoutParams();
260        if (lp.height != systemIconsContainerHeight) {
261            lp.height = systemIconsContainerHeight;
262            mMultiUserSwitch.setLayoutParams(lp);
263        }
264    }
265
266    private void updateWidth() {
267        int width = (mKeyguardShowing && !mExpanded) ? mKeyguardWidth : mNormalWidth;
268        ViewGroup.LayoutParams lp = getLayoutParams();
269        if (width != lp.width) {
270            lp.width = width;
271            setLayoutParams(lp);
272        }
273    }
274
275    private void updateVisibilities() {
276        boolean onKeyguardAndCollapsed = mKeyguardShowing && !mExpanded;
277        if (onKeyguardAndCollapsed) {
278            setBackground(null);
279        } else {
280            setBackgroundResource(R.drawable.notification_header_bg);
281        }
282        mDateTime.setVisibility(onKeyguardAndCollapsed ? View.INVISIBLE : View.VISIBLE);
283        mKeyguardCarrierText.setVisibility(onKeyguardAndCollapsed ? View.VISIBLE : View.GONE);
284        mDateCollapsed.setVisibility(mExpanded && !mOverscrolled ? View.GONE : View.VISIBLE);
285        mDateExpanded.setVisibility(mExpanded && !mOverscrolled ? View.VISIBLE : View.GONE);
286        mSettingsButton.setVisibility(mExpanded && !mOverscrolled ? View.VISIBLE : View.GONE);
287        mQsDetailHeader.setVisibility(mExpanded ? View.VISIBLE : View.GONE);
288        if (mSignalCluster != null) {
289            mSignalCluster.setVisibility(!mExpanded || mOverscrolled ? View.VISIBLE : View.GONE);
290        }
291        mEmergencyCallsOnly.setVisibility(mExpanded && !mOverscrolled && mShowEmergencyCallsOnly
292                ? VISIBLE : GONE);
293        mMultiUserSwitch.setVisibility(mExpanded || !mKeyguardUserSwitcherShowing
294                ? VISIBLE : GONE);
295        mBatteryLevel.setVisibility(mKeyguardShowing && mCharging || mExpanded && !mOverscrolled
296                ? View.VISIBLE : View.GONE);
297    }
298
299    private void updateSystemIconsLayoutParams() {
300        RelativeLayout.LayoutParams lp = (LayoutParams) mSystemIconsSuperContainer.getLayoutParams();
301        lp.addRule(RelativeLayout.START_OF, mExpanded && !mOverscrolled
302                ? mSettingsButton.getId()
303                : mMultiUserSwitch.getId());
304        lp.removeRule(ALIGN_PARENT_START);
305        if (mMultiUserSwitch.getVisibility() == GONE) {
306            lp.setMarginEnd(mSystemIconsSwitcherHiddenExpandedMargin);
307        } else {
308            lp.setMarginEnd(0);
309        }
310        mSystemIconsSuperContainer.setLayoutParams(lp);
311    }
312
313    private void updateBatteryListening() {
314        if (mListening) {
315            mBatteryController.addStateChangedCallback(this);
316        } else {
317            mBatteryController.removeStateChangedCallback(this);
318        }
319    }
320
321    private void updateAvatarScale() {
322        if (!mExpanded || mOverscrolled) {
323            mMultiUserSwitch.setScaleX(mAvatarCollapsedScaleFactor);
324            mMultiUserSwitch.setScaleY(mAvatarCollapsedScaleFactor);
325        } else {
326            mMultiUserSwitch.setScaleX(1f);
327            mMultiUserSwitch.setScaleY(1f);
328        }
329    }
330
331    private void updateClockScale() {
332        mAmPm.setScaleX(mClockCollapsedScaleFactor);
333        mAmPm.setScaleY(mClockCollapsedScaleFactor);
334        if (!mExpanded || mOverscrolled) {
335            mTime.setScaleX(mClockCollapsedScaleFactor);
336            mTime.setScaleY(mClockCollapsedScaleFactor);
337        } else {
338            mTime.setScaleX(1f);
339            mTime.setScaleY(1f);
340        }
341        updateAmPmTranslation();
342    }
343
344    private void updateAmPmTranslation() {
345        boolean rtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
346        mAmPm.setTranslationX((rtl ? 1 : -1) * mTime.getWidth() * (1 - mTime.getScaleX()));
347    }
348
349    private void updateBatteryLevelPaddingEnd() {
350        mBatteryLevel.setPaddingRelative(0, 0,
351                mKeyguardShowing && !mExpanded ? 0 : mBatteryPaddingEnd, 0);
352    }
353
354    @Override
355    public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
356        mBatteryLevel.setText(getResources().getString(R.string.battery_level_template, level));
357        boolean changed = mCharging != charging;
358        mCharging = charging;
359        if (changed) {
360            updateVisibilities();
361        }
362    }
363
364    private void updateClickTargets() {
365        setClickable(!mKeyguardShowing || mExpanded);
366        mDateTime.setClickable(mExpanded);
367        mMultiUserSwitch.setClickable(mExpanded);
368        mSystemIconsSuperContainer.setClickable(mExpanded);
369    }
370
371    private void updateZTranslation() {
372
373        // If we are on the Keyguard, we need to set our z position to zero, so we don't get
374        // shadows.
375        if (mKeyguardShowing && !mExpanded) {
376            setZ(0);
377        } else {
378            setTranslationZ(0);
379        }
380    }
381
382    private void updatePadding() {
383        boolean padded = !mKeyguardShowing || mExpanded;
384        int padding = padded ? mPadding : 0;
385        setPaddingRelative(padding, 0, padding, 0);
386    }
387
388    private void updateClockLp() {
389        int marginBottom = mExpanded && !mOverscrolled ? mClockMarginBottomExpanded : 0;
390        LayoutParams lp = (LayoutParams) mDateTime.getLayoutParams();
391        int rule = mExpanded && !mOverscrolled ? TRUE : 0;
392        if (marginBottom != lp.bottomMargin
393                || lp.getRules()[RelativeLayout.ALIGN_PARENT_BOTTOM] != rule) {
394            lp.bottomMargin = marginBottom;
395            lp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, rule);
396            mDateTime.setLayoutParams(lp);
397        }
398    }
399
400    private void updateMultiUserSwitch() {
401        int marginEnd;
402        if (mExpanded && !mOverscrolled) {
403            marginEnd = mMultiUserExpandedMargin;
404        } else if (mKeyguardShowing) {
405            marginEnd = mMultiUserKeyguardMargin;
406        } else {
407            marginEnd = mMultiUserCollapsedMargin;
408        }
409        int width = mExpanded && !mOverscrolled
410                ? mMultiUserSwitchWidthExpanded
411                : mMultiUserSwitchWidthCollapsed;
412        MarginLayoutParams lp = (MarginLayoutParams) mMultiUserSwitch.getLayoutParams();
413        if (marginEnd != lp.getMarginEnd() || lp.width != width) {
414            lp.setMarginEnd(marginEnd);
415            lp.width = width;
416            mMultiUserSwitch.setLayoutParams(lp);
417        }
418    }
419
420    public void setExpansion(float t) {
421        float height = mCollapsedHeight + t * (mExpandedHeight - mCollapsedHeight);
422        if (height < mCollapsedHeight) {
423            height = mCollapsedHeight;
424        }
425        if (height > mExpandedHeight) {
426            height = mExpandedHeight;
427        }
428        setClipping(height);
429    }
430
431    private void setClipping(float height) {
432        mClipBounds.set(getPaddingLeft(), 0, getWidth() - getPaddingRight(), (int) height);
433        setClipBounds(mClipBounds);
434        invalidateOutline();
435    }
436
437    public void attachSystemIcons(LinearLayout systemIcons) {
438        mSystemIconsContainer.addView(systemIcons);
439        mStatusIcons = systemIcons.findViewById(R.id.statusIcons);
440        mStatusIcons.addOnLayoutChangeListener(mStatusIconsChanged);
441        mSignalCluster = systemIcons.findViewById(R.id.signal_cluster);
442    }
443
444    public void onSystemIconsDetached() {
445        if (mStatusIcons != null) {
446            mStatusIcons.removeOnLayoutChangeListener(mStatusIconsChanged);
447        }
448        if (mSignalCluster != null) {
449            mSignalCluster.setVisibility(View.VISIBLE);
450        }
451        mStatusIcons = null;
452        mSignalCluster = null;
453    }
454
455    public void setKeyguardShowing(boolean keyguardShowing) {
456        mKeyguardShowing = keyguardShowing;
457        updateHeights();
458        updateWidth();
459        updateVisibilities();
460        updateZTranslation();
461        updatePadding();
462        updateMultiUserSwitch();
463        updateClickTargets();
464        updateBatteryLevelPaddingEnd();
465    }
466
467    public void setUserInfoController(UserInfoController userInfoController) {
468        userInfoController.addListener(new UserInfoController.OnUserInfoChangedListener() {
469            @Override
470            public void onUserInfoChanged(String name, Drawable picture) {
471                mMultiUserAvatar.setImageDrawable(picture);
472            }
473        });
474    }
475
476    @Override
477    public void onClick(View v) {
478        if (v == mSettingsButton) {
479            startSettingsActivity();
480        } else if (v == mSystemIconsSuperContainer) {
481            startBatteryActivity();
482        }
483    }
484
485    private void startSettingsActivity() {
486        mActivityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS));
487    }
488
489    private void startBatteryActivity() {
490        mActivityStarter.startActivity(new Intent(Intent.ACTION_POWER_USAGE_SUMMARY));
491    }
492
493    public void setQSPanel(QSPanel qsp) {
494        mQSPanel = qsp;
495        if (mQSPanel != null) {
496            mQSPanel.setCallback(mQsPanelCallback);
497        }
498        mMultiUserSwitch.setQsPanel(qsp);
499    }
500
501    @Override
502    public boolean shouldDelayChildPressedState() {
503        return true;
504    }
505
506    public void setShowEmergencyCallsOnly(boolean show) {
507        mShowEmergencyCallsOnly = show;
508        if (mExpanded) {
509            updateVisibilities();
510        }
511    }
512
513    public void setKeyguardUserSwitcherShowing(boolean showing) {
514        // STOPSHIP: NOT CALLED PROPERLY WHEN GOING TO FULL SHADE AND RETURNING!?!
515        mKeyguardUserSwitcherShowing = showing;
516        updateVisibilities();
517        updateSystemIconsLayoutParams();
518    }
519
520    @Override
521    public boolean hasOverlappingRendering() {
522        return !mKeyguardShowing || mExpanded;
523    }
524
525    @Override
526    protected void dispatchSetPressed(boolean pressed) {
527        // We don't want that everything lights up when we click on the header, so block the request
528        // here.
529    }
530
531    private final OnLayoutChangeListener mStatusIconsChanged = new OnLayoutChangeListener() {
532        private final Rect mClipBounds = new Rect();
533
534        @Override
535        public void onLayoutChange(View v, int left, int top, int right,
536                int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
537            // Hide the statusIcons in the header by clipping them.  Can't touch visibility since
538            // they are mirrored to the real status bar.
539            final Rect r = mSystemIconsContainer.getClipBounds();
540            if (r == null || r.left != right) {
541                mClipBounds.set(right, 0, mSystemIconsContainer.getWidth(),
542                        mSystemIconsContainer.getHeight());
543                mSystemIconsContainer.setClipBounds(mClipBounds);
544            }
545        }
546    };
547
548    private final QSPanel.Callback mQsPanelCallback = new QSPanel.Callback() {
549        @Override
550        public void onToggleStateChanged(final boolean state) {
551            post(new Runnable() {
552                @Override
553                public void run() {
554                    handleToggleStateChanged(state);
555                }
556            });
557        }
558
559        @Override
560        public void onShowingDetail(final QSTile.DetailAdapter detail) {
561            post(new Runnable() {
562                @Override
563                public void run() {
564                    handleShowingDetail(detail);
565                }
566            });
567        }
568
569        @Override
570        public void onScanStateChanged(final boolean state) {
571            post(new Runnable() {
572                @Override
573                public void run() {
574                    handleScanStateChanged(state);
575                }
576            });
577        }
578
579        private void handleToggleStateChanged(boolean state) {
580            mQsDetailHeaderSwitch.setChecked(state);
581        }
582
583        private void handleScanStateChanged(boolean state) {
584            // TODO - waiting on framework asset
585        }
586
587        private void handleShowingDetail(final QSTile.DetailAdapter detail) {
588            final boolean showingDetail = detail != null;
589            transition(mDateTime, !showingDetail);
590            transition(mQsDetailHeader, showingDetail);
591            if (showingDetail) {
592                mQsDetailHeaderTitle.setText(detail.getTitle());
593                final Boolean toggleState = detail.getToggleState();
594                if (toggleState == null) {
595                    mQsDetailHeaderSwitch.setVisibility(INVISIBLE);
596                    mQsDetailHeader.setClickable(false);
597                } else {
598                    mQsDetailHeaderSwitch.setVisibility(VISIBLE);
599                    mQsDetailHeaderSwitch.setChecked(toggleState);
600                    mQsDetailHeader.setClickable(true);
601                    mQsDetailHeader.setOnClickListener(new OnClickListener() {
602                        @Override
603                        public void onClick(View v) {
604                            detail.setToggleState(!mQsDetailHeaderSwitch.isChecked());
605                        }
606                    });
607                }
608            } else {
609                mQsDetailHeader.setClickable(false);
610            }
611        }
612
613        private void transition(final View v, final boolean in) {
614            if (in) {
615                v.bringToFront();
616            }
617            v.animate().alpha(in ? 1 : 0).withLayer().start();
618        }
619    };
620}
621