NavBarTuner.java revision a9f128832f3fc16c2f435f2d0c999a5154200b4c
1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 * except in compliance with the License. You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the
10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11 * KIND, either express or implied. See the License for the specific language governing
12 * permissions and limitations under the License.
13 */
14
15package com.android.systemui.tuner;
16
17import com.android.systemui.R;
18
19import android.annotation.Nullable;
20import android.app.Activity;
21import android.app.AlertDialog;
22import android.app.Fragment;
23import android.content.Context;
24import android.content.DialogInterface;
25import android.content.Intent;
26import android.content.res.ColorStateList;
27import android.content.res.Configuration;
28import android.content.res.TypedArray;
29import android.graphics.Canvas;
30import android.graphics.drawable.Drawable;
31import android.net.Uri;
32import android.os.Bundle;
33import android.provider.Settings;
34import android.support.v7.widget.LinearLayoutManager;
35import android.support.v7.widget.RecyclerView;
36import android.support.v7.widget.helper.ItemTouchHelper;
37import android.util.DisplayMetrics;
38import android.util.TypedValue;
39import android.view.Display;
40import android.view.LayoutInflater;
41import android.view.Menu;
42import android.view.MenuInflater;
43import android.view.MenuItem;
44import android.view.MotionEvent;
45import android.view.Surface;
46import android.view.View;
47import android.view.ViewGroup;
48import android.widget.ImageView;
49import android.widget.SeekBar;
50import android.widget.TextView;
51
52import java.util.ArrayList;
53import java.util.List;
54
55import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.CLIPBOARD;
56import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.KEY;
57import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.KEY_CODE_END;
58import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.KEY_CODE_START;
59import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.KEY_IMAGE_DELIM;
60import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.SIZE_MOD_END;
61import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.SIZE_MOD_START;
62import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.extractButton;
63import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.extractSize;
64import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.BACK;
65import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.BUTTON_SEPARATOR;
66import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.GRAVITY_SEPARATOR;
67import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.HOME;
68import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.MENU_IME;
69import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.NAVSPACE;
70import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.NAV_BAR_VIEWS;
71import static com.android.systemui.statusbar.phone.NavigationBarInflaterView.RECENT;
72
73public class NavBarTuner extends Fragment implements TunerService.Tunable {
74
75    private static final int SAVE = Menu.FIRST + 1;
76    private static final int RESET = Menu.FIRST + 2;
77    private static final int READ_REQUEST = 42;
78
79    private static final float PREVIEW_SCALE = .95f;
80    private static final float PREVIEW_SCALE_LANDSCAPE = .75f;
81
82    private NavBarAdapter mNavBarAdapter;
83    private PreviewNavInflater mPreview;
84
85    @Override
86    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
87            Bundle savedInstanceState) {
88        final View view = inflater.inflate(R.layout.nav_bar_tuner, container, false);
89        inflatePreview((ViewGroup) view.findViewById(R.id.nav_preview_frame));
90        return view;
91    }
92
93    private void inflatePreview(ViewGroup view) {
94        Display display = getActivity().getWindowManager().getDefaultDisplay();
95        boolean isRotated = display.getRotation() == Surface.ROTATION_90
96                || display.getRotation() == Surface.ROTATION_270;
97
98        Configuration config = new Configuration(getContext().getResources().getConfiguration());
99        boolean isPhoneLandscape = isRotated && (config.smallestScreenWidthDp < 600);
100        final float scale = isPhoneLandscape ? PREVIEW_SCALE_LANDSCAPE : PREVIEW_SCALE;
101        config.densityDpi = (int) (config.densityDpi * scale);
102
103        mPreview = (PreviewNavInflater) LayoutInflater.from(getContext().createConfigurationContext(
104                config)).inflate(R.layout.nav_bar_tuner_inflater, view, false);
105        final ViewGroup.LayoutParams layoutParams = mPreview.getLayoutParams();
106        layoutParams.width = (int) ((isPhoneLandscape ? display.getHeight() : display.getWidth())
107                * scale);
108        // Not sure why, but the height dimen is not being scaled with the dp, set it manually
109        // for now.
110        layoutParams.height = (int) (layoutParams.height * scale);
111        if (isPhoneLandscape) {
112            int width = layoutParams.width;
113            layoutParams.width = layoutParams.height;
114            layoutParams.height = width;
115        }
116        view.addView(mPreview);
117
118        if (isRotated) {
119            mPreview.findViewById(R.id.rot0).setVisibility(View.GONE);
120            final View rot90 = mPreview.findViewById(R.id.rot90);
121            rot90.findViewById(R.id.ends_group_lightsout).setVisibility(View.GONE);
122            rot90.findViewById(R.id.center_group_lightsout).setVisibility(View.GONE);
123        } else {
124            mPreview.findViewById(R.id.rot90).setVisibility(View.GONE);
125            final View rot0 = mPreview.findViewById(R.id.rot0);
126            rot0.findViewById(R.id.ends_group_lightsout).setVisibility(View.GONE);
127            rot0.findViewById(R.id.center_group_lightsout).setVisibility(View.GONE);
128        }
129    }
130
131    private void notifyChanged() {
132        mPreview.onTuningChanged(NAV_BAR_VIEWS, mNavBarAdapter.getNavString());
133    }
134
135    @Override
136    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
137        super.onViewCreated(view, savedInstanceState);
138        RecyclerView recyclerView = (RecyclerView) view.findViewById(android.R.id.list);
139        final Context context = getContext();
140        recyclerView.setLayoutManager(new LinearLayoutManager(context));
141        mNavBarAdapter = new NavBarAdapter(context);
142        recyclerView.setAdapter(mNavBarAdapter);
143        recyclerView.addItemDecoration(new Dividers(context));
144        final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mNavBarAdapter.mCallbacks);
145        mNavBarAdapter.setTouchHelper(itemTouchHelper);
146        itemTouchHelper.attachToRecyclerView(recyclerView);
147
148        TunerService.get(getContext()).addTunable(this, NAV_BAR_VIEWS);
149    }
150
151    @Override
152    public void onDestroyView() {
153        super.onDestroyView();
154        TunerService.get(getContext()).removeTunable(this);
155    }
156
157    @Override
158    public void onTuningChanged(String key, String navLayout) {
159        if (!NAV_BAR_VIEWS.equals(key)) return;
160        Context context = getContext();
161        if (navLayout == null) {
162            navLayout = context.getString(R.string.config_navBarLayout);
163        }
164        String[] views = navLayout.split(GRAVITY_SEPARATOR);
165        String[] groups = new String[] { NavBarAdapter.START, NavBarAdapter.CENTER,
166                NavBarAdapter.END};
167        CharSequence[] groupLabels = new String[] { getString(R.string.start),
168                getString(R.string.center), getString(R.string.end) };
169        mNavBarAdapter.clear();
170        for (int i = 0; i < 3; i++) {
171            mNavBarAdapter.addButton(groups[i], groupLabels[i]);
172            for (String button : views[i].split(BUTTON_SEPARATOR)) {
173                mNavBarAdapter.addButton(button, getLabel(button, context));
174            }
175        }
176        mNavBarAdapter.addButton(NavBarAdapter.ADD, getString(R.string.add_button));
177        setHasOptionsMenu(true);
178    }
179
180    @Override
181    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
182        super.onCreateOptionsMenu(menu, inflater);
183        // TODO: Show save button conditionally, only when there are changes.
184        menu.add(Menu.NONE, SAVE, Menu.NONE, getString(R.string.save))
185                .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
186        menu.add(Menu.NONE, RESET, Menu.NONE, getString(R.string.reset));
187    }
188
189    @Override
190    public boolean onOptionsItemSelected(MenuItem item) {
191        if (item.getItemId() == SAVE) {
192            if (!mNavBarAdapter.hasHomeButton()) {
193                new AlertDialog.Builder(getContext())
194                        .setTitle(R.string.no_home_title)
195                        .setMessage(R.string.no_home_message)
196                        .setPositiveButton(android.R.string.ok, null)
197                        .show();
198            } else {
199                Settings.Secure.putString(getContext().getContentResolver(),
200                        NAV_BAR_VIEWS, mNavBarAdapter.getNavString());
201            }
202            return true;
203        } else if (item.getItemId() == RESET) {
204            Settings.Secure.putString(getContext().getContentResolver(),
205                    NAV_BAR_VIEWS, null);
206            return true;
207        }
208        return super.onOptionsItemSelected(item);
209    }
210
211    private static CharSequence getLabel(String button, Context context) {
212        if (button.startsWith(HOME)) {
213            return context.getString(R.string.accessibility_home);
214        } else if (button.startsWith(BACK)) {
215            return context.getString(R.string.accessibility_back);
216        } else if (button.startsWith(RECENT)) {
217            return context.getString(R.string.accessibility_recent);
218        } else if (button.startsWith(NAVSPACE)) {
219            return context.getString(R.string.space);
220        } else if (button.startsWith(MENU_IME)) {
221            return context.getString(R.string.menu_ime);
222        } else if (button.startsWith(CLIPBOARD)) {
223            return context.getString(R.string.clipboard);
224        } else if (button.startsWith(KEY)) {
225            return context.getString(R.string.keycode);
226        }
227        return button;
228    }
229
230    private static class Holder extends RecyclerView.ViewHolder {
231        private TextView title;
232
233        public Holder(View itemView) {
234            super(itemView);
235            title = (TextView) itemView.findViewById(android.R.id.title);
236        }
237    }
238
239    private static class Dividers extends RecyclerView.ItemDecoration {
240        private final Drawable mDivider;
241
242        public Dividers(Context context) {
243            TypedValue value = new TypedValue();
244            context.getTheme().resolveAttribute(android.R.attr.listDivider, value, true);
245            mDivider = context.getDrawable(value.resourceId);
246        }
247
248        @Override
249        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
250            super.onDraw(c, parent, state);
251            final int left = parent.getPaddingLeft();
252            final int right = parent.getWidth() - parent.getPaddingRight();
253
254            final int childCount = parent.getChildCount();
255            for (int i = 0; i < childCount; i++) {
256                final View child = parent.getChildAt(i);
257                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
258                        .getLayoutParams();
259                final int top = child.getBottom() + params.bottomMargin;
260                final int bottom = top + mDivider.getIntrinsicHeight();
261                mDivider.setBounds(left, top, right, bottom);
262                mDivider.draw(c);
263            }
264        }
265    }
266
267    private void selectImage() {
268        startActivityForResult(KeycodeSelectionHelper.getSelectImageIntent(), READ_REQUEST);
269    }
270
271    @Override
272    public void onActivityResult(int requestCode, int resultCode, Intent data) {
273        if (requestCode == READ_REQUEST && resultCode == Activity.RESULT_OK && data != null) {
274            final Uri uri = data.getData();
275            final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION);
276            getContext().getContentResolver().takePersistableUriPermission(uri, takeFlags);
277            mNavBarAdapter.onImageSelected(uri);
278        } else {
279            super.onActivityResult(requestCode, resultCode, data);
280        }
281    }
282
283    private class NavBarAdapter extends RecyclerView.Adapter<Holder>
284            implements View.OnClickListener {
285
286        private static final String START = "start";
287        private static final String CENTER = "center";
288        private static final String END = "end";
289        private static final String ADD = "add";
290
291        private static final int ADD_ID = 0;
292        private static final int BUTTON_ID = 1;
293        private static final int CATEGORY_ID = 2;
294
295        private List<String> mButtons = new ArrayList<>();
296        private List<CharSequence> mLabels = new ArrayList<>();
297        private int mCategoryLayout;
298        private int mButtonLayout;
299        private ItemTouchHelper mTouchHelper;
300
301        // Stored keycode while we wait for image selection on a KEY.
302        private int mKeycode;
303
304        public NavBarAdapter(Context context) {
305            TypedArray attrs = context.getTheme().obtainStyledAttributes(null,
306                    android.R.styleable.Preference, android.R.attr.preferenceStyle, 0);
307            mButtonLayout = attrs.getResourceId(android.R.styleable.Preference_layout, 0);
308            attrs = context.getTheme().obtainStyledAttributes(null,
309                    android.R.styleable.Preference, android.R.attr.preferenceCategoryStyle, 0);
310            mCategoryLayout = attrs.getResourceId(android.R.styleable.Preference_layout, 0);
311        }
312
313        public void setTouchHelper(ItemTouchHelper itemTouchHelper) {
314            mTouchHelper = itemTouchHelper;
315        }
316
317        public void clear() {
318            mButtons.clear();
319            mLabels.clear();
320            notifyDataSetChanged();
321        }
322
323        public void addButton(String button, CharSequence label) {
324            mButtons.add(button);
325            mLabels.add(label);
326            notifyItemInserted(mLabels.size() - 1);
327            notifyChanged();
328        }
329
330        public boolean hasHomeButton() {
331            final int N = mButtons.size();
332            for (int i = 0; i < N; i++) {
333                if (mButtons.get(i).startsWith(HOME)) {
334                    return true;
335                }
336            }
337            return false;
338        }
339
340        public String getNavString() {
341            StringBuilder builder = new StringBuilder();
342            for (int i = 1; i < mButtons.size() - 1; i++) {
343                String button = mButtons.get(i);
344                if (button.equals(CENTER) || button.equals(END)) {
345                    if (builder.length() == 0 || builder.toString().endsWith(GRAVITY_SEPARATOR)) {
346                        // No start or center buttons, fill with a space.
347                        builder.append(NAVSPACE);
348                    }
349                    builder.append(GRAVITY_SEPARATOR);
350                    continue;
351                } else if (builder.length() != 0 && !builder.toString().endsWith(
352                        GRAVITY_SEPARATOR)) {
353                    builder.append(BUTTON_SEPARATOR);
354                }
355                builder.append(button);
356            }
357            if (builder.toString().endsWith(GRAVITY_SEPARATOR)) {
358                // No end buttons, fill with space.
359                builder.append(NAVSPACE);
360            }
361            return builder.toString();
362        }
363
364        @Override
365        public int getItemViewType(int position) {
366            String button = mButtons.get(position);
367            if (button.equals(START) || button.equals(CENTER) || button.equals(END)) {
368                return CATEGORY_ID;
369            }
370            if (button.equals(ADD)) {
371                return ADD_ID;
372            }
373            return BUTTON_ID;
374        }
375
376        @Override
377        public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
378            final Context context = parent.getContext();
379            final LayoutInflater inflater = LayoutInflater.from(context);
380            final View view = inflater.inflate(getLayoutId(viewType), parent, false);
381            if (viewType == BUTTON_ID) {
382                inflater.inflate(R.layout.nav_control_widget,
383                        (ViewGroup) view.findViewById(android.R.id.widget_frame));
384            }
385            return new Holder(view);
386        }
387
388        private int getLayoutId(int viewType) {
389            if (viewType == CATEGORY_ID) {
390                return mCategoryLayout;
391            }
392            return mButtonLayout;
393        }
394
395        @Override
396        public void onBindViewHolder(Holder holder, int position) {
397            holder.title.setText(mLabels.get(position));
398            if (holder.getItemViewType() == BUTTON_ID) {
399                bindButton(holder, position);
400            } else if (holder.getItemViewType() == ADD_ID) {
401                bindAdd(holder);
402            }
403        }
404
405        private void bindAdd(Holder holder) {
406            TypedValue value = new TypedValue();
407            final Context context = holder.itemView.getContext();
408            context.getTheme().resolveAttribute(android.R.attr.colorAccent, value, true);
409            final ImageView icon = (ImageView) holder.itemView.findViewById(android.R.id.icon);
410            icon.setImageResource(R.drawable.ic_add);
411            icon.setImageTintList(ColorStateList.valueOf(context.getColor(value.resourceId)));
412            holder.itemView.findViewById(android.R.id.summary).setVisibility(View.GONE);
413            holder.itemView.setClickable(true);
414            holder.itemView.setOnClickListener(new View.OnClickListener() {
415                @Override
416                public void onClick(View v) {
417                    showAddDialog(v.getContext());
418                }
419            });
420        }
421
422        private void bindButton(final Holder holder, int position) {
423            holder.itemView.findViewById(android.R.id.icon_frame).setVisibility(View.GONE);
424            holder.itemView.findViewById(android.R.id.summary).setVisibility(View.GONE);
425            bindClick(holder.itemView.findViewById(R.id.close), holder);
426            bindClick(holder.itemView.findViewById(R.id.width), holder);
427            holder.itemView.findViewById(R.id.drag).setOnTouchListener(new View.OnTouchListener() {
428                @Override
429                public boolean onTouch(View v, MotionEvent event) {
430                    mTouchHelper.startDrag(holder);
431                    return true;
432                }
433            });
434        }
435
436        private void showAddDialog(final Context context) {
437            final String[] options = new String[] {
438                    BACK, HOME, RECENT, MENU_IME, NAVSPACE, CLIPBOARD, KEY,
439            };
440            final CharSequence[] labels = new CharSequence[options.length];
441            for (int i = 0; i < options.length; i++) {
442                labels[i] = getLabel(options[i], context);
443            }
444            new AlertDialog.Builder(context)
445                    .setTitle(R.string.select_button)
446                    .setItems(labels, new DialogInterface.OnClickListener() {
447                        @Override
448                        public void onClick(DialogInterface dialog, int which) {
449                            if (KEY.equals(options[which])) {
450                                showKeyDialogs(context);
451                            } else {
452                                int index = mButtons.size() - 1;
453                                showAddedMessage(context, options[which]);
454                                mButtons.add(index, options[which]);
455                                mLabels.add(index, labels[which]);
456
457                                notifyItemInserted(index);
458                                notifyChanged();
459                            }
460                        }
461                    }).setNegativeButton(android.R.string.cancel, null)
462                    .show();
463        }
464
465        private void onImageSelected(Uri uri) {
466            int index = mButtons.size() - 1;
467            mButtons.add(index, KEY + KEY_CODE_START + mKeycode + KEY_IMAGE_DELIM + uri.toString()
468                    + KEY_CODE_END);
469            mLabels.add(index, getLabel(KEY, getContext()));
470
471            notifyItemInserted(index);
472            notifyChanged();
473        }
474
475        private void showKeyDialogs(final Context context) {
476            final KeycodeSelectionHelper.OnSelectionComplete listener =
477                    new KeycodeSelectionHelper.OnSelectionComplete() {
478                        @Override
479                        public void onSelectionComplete(int code) {
480                            mKeycode = code;
481                            selectImage();
482                        }
483                    };
484            new AlertDialog.Builder(context)
485                    .setTitle(R.string.keycode)
486                    .setMessage(R.string.keycode_description)
487                    .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
488                        @Override
489                        public void onClick(DialogInterface dialog, int which) {
490                            KeycodeSelectionHelper.showKeycodeSelect(context, listener);
491                        }
492                    }).show();
493        }
494
495        private void showAddedMessage(Context context, String button) {
496            if (CLIPBOARD.equals(button)) {
497                new AlertDialog.Builder(context)
498                        .setTitle(R.string.clipboard)
499                        .setMessage(R.string.clipboard_description)
500                        .setPositiveButton(android.R.string.ok, null)
501                        .show();
502            }
503        }
504
505        private void bindClick(View view, Holder holder) {
506            view.setOnClickListener(this);
507            view.setTag(holder);
508        }
509
510        @Override
511        public void onClick(View v) {
512            Holder holder = (Holder) v.getTag();
513            if (v.getId() == R.id.width) {
514                showWidthDialog(holder, v.getContext());
515            } else if (v.getId() == R.id.close) {
516                int position = holder.getAdapterPosition();
517                mButtons.remove(position);
518                mLabels.remove(position);
519                notifyItemRemoved(position);
520                notifyChanged();
521            }
522        }
523
524        private void showWidthDialog(final Holder holder, Context context) {
525            final String buttonSpec = mButtons.get(holder.getAdapterPosition());
526            float amount = extractSize(buttonSpec);
527            final AlertDialog dialog = new AlertDialog.Builder(context)
528                    .setTitle(R.string.adjust_button_width)
529                    .setView(R.layout.nav_width_view)
530                    .setNegativeButton(android.R.string.cancel, null).create();
531            dialog.setButton(DialogInterface.BUTTON_POSITIVE,
532                    context.getString(android.R.string.ok),
533                    new DialogInterface.OnClickListener() {
534                        @Override
535                        public void onClick(DialogInterface d, int which) {
536                            final String button = extractButton(buttonSpec);
537                            SeekBar seekBar = (SeekBar) dialog.findViewById(R.id.seekbar);
538                            if (seekBar.getProgress() == 75) {
539                                mButtons.set(holder.getAdapterPosition(), button);
540                            } else {
541                                float amount = (seekBar.getProgress() + 25) / 100f;
542                                mButtons.set(holder.getAdapterPosition(), button
543                                        + SIZE_MOD_START + amount + SIZE_MOD_END);
544                            }
545                            notifyChanged();
546                        }
547                    });
548            dialog.show();
549            SeekBar seekBar = (SeekBar) dialog.findViewById(R.id.seekbar);
550            // Range is .25 - 1.75.
551            seekBar.setMax(150);
552            seekBar.setProgress((int) ((amount - .25f) * 100));
553        }
554
555        @Override
556        public int getItemCount() {
557            return mButtons.size();
558        }
559
560        private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() {
561            @Override
562            public boolean isLongPressDragEnabled() {
563                return false;
564            }
565
566            @Override
567            public boolean isItemViewSwipeEnabled() {
568                return false;
569            }
570
571            @Override
572            public int getMovementFlags(RecyclerView recyclerView,
573                    RecyclerView.ViewHolder viewHolder) {
574                if (viewHolder.getItemViewType() != BUTTON_ID) {
575                    return makeMovementFlags(0, 0);
576                }
577                int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
578                return makeMovementFlags(dragFlags, 0);
579            }
580
581            @Override
582            public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
583                    RecyclerView.ViewHolder target) {
584                int from = viewHolder.getAdapterPosition();
585                int to = target.getAdapterPosition();
586                if (to == 0) {
587                    // Can't go above the top.
588                    return false;
589                }
590                move(from, to, mButtons);
591                move(from, to, mLabels);
592                notifyChanged();
593                notifyItemMoved(from, to);
594                return true;
595            }
596
597            private <T> void move(int from, int to, List<T> list) {
598                list.add(from > to ? to : to + 1, list.get(from));
599                list.remove(from > to ? from + 1 : from);
600            }
601
602            @Override
603            public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
604                // Don't care.
605            }
606        };
607    }
608}
609