• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.intentresolver.grid;
18 
19 import android.animation.AnimatorSet;
20 import android.animation.ObjectAnimator;
21 import android.animation.ValueAnimator;
22 import android.app.ActivityManager;
23 import android.content.Context;
24 import android.database.DataSetObserver;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.View.MeasureSpec;
28 import android.view.View.OnClickListener;
29 import android.view.ViewGroup;
30 import android.view.ViewGroup.LayoutParams;
31 import android.view.animation.DecelerateInterpolator;
32 import android.widget.Space;
33 import android.widget.TextView;
34 
35 import androidx.recyclerview.widget.RecyclerView;
36 
37 import com.android.intentresolver.ChooserListAdapter;
38 import com.android.intentresolver.R;
39 import com.android.intentresolver.ResolverListAdapter.ViewHolder;
40 import com.android.internal.annotations.VisibleForTesting;
41 
42 import com.google.android.collect.Lists;
43 
44 /**
45  * Adapter for all types of items and targets in ShareSheet.
46  * Note that ranked sections like Direct Share - while appearing grid-like - are handled on the
47  * row level by this adapter but not on the item level. Individual targets within the row are
48  * handled by {@link ChooserListAdapter}
49  */
50 @VisibleForTesting
51 public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
52 
53     /**
54      * The transition time between placeholders for direct share to a message
55      * indicating that none are available.
56      */
57     public static final int NO_DIRECT_SHARE_ANIM_IN_MILLIS = 200;
58 
59     /**
60      * Injectable interface for any considerations that should be delegated to other components
61      * in the {@link ChooserActivity}.
62      * TODO: determine whether any of these methods return parameters that can safely be
63      * precomputed; whether any should be converted to `ChooserGridAdapter` setters to be
64      * invoked by external callbacks; and whether any reflect requirements that should be moved
65      * out of `ChooserGridAdapter` altogether.
66      */
67     public interface ChooserActivityDelegate {
68         /** @return whether we're showing a tabbed (multi-profile) UI. */
shouldShowTabs()69         boolean shouldShowTabs();
70 
71         /**
72          * @return a content preview {@link View} that's appropriate for the caller's share
73          * content, constructed for display in the provided {@code parent} group.
74          */
buildContentPreview(ViewGroup parent)75         View buildContentPreview(ViewGroup parent);
76 
77         /** Notify the client that the item with the selected {@code itemIndex} was selected. */
onTargetSelected(int itemIndex)78         void onTargetSelected(int itemIndex);
79 
80         /**
81          * Notify the client that the item with the selected {@code itemIndex} was
82          * long-pressed.
83          */
onTargetLongPressed(int itemIndex)84         void onTargetLongPressed(int itemIndex);
85 
86         /**
87          * Notify the client that the provided {@code View} should be configured as the new
88          * "profile view" button. Callers should attach their own click listeners to implement
89          * behaviors on this view.
90          */
updateProfileViewButton(View newButtonFromProfileRow)91         void updateProfileViewButton(View newButtonFromProfileRow);
92 
93         /**
94          * @return the number of "valid" targets in the active list adapter.
95          * TODO: define "valid."
96          */
getValidTargetCount()97         int getValidTargetCount();
98 
99         /**
100          * Request that the client update our {@code directShareGroup} to match their desired
101          * state for the "expansion" UI.
102          */
updateDirectShareExpansion(DirectShareViewHolder directShareGroup)103         void updateDirectShareExpansion(DirectShareViewHolder directShareGroup);
104 
105         /**
106          * Request that the client handle a scroll event that should be taken as expanding the
107          * provided {@code directShareGroup}. Note that this currently never happens due to a
108          * hard-coded condition in {@link #canExpandDirectShare()}.
109          */
handleScrollToExpandDirectShare( DirectShareViewHolder directShareGroup, int y, int oldy)110         void handleScrollToExpandDirectShare(
111                 DirectShareViewHolder directShareGroup, int y, int oldy);
112     }
113 
114     private static final int VIEW_TYPE_DIRECT_SHARE = 0;
115     private static final int VIEW_TYPE_NORMAL = 1;
116     private static final int VIEW_TYPE_CONTENT_PREVIEW = 2;
117     private static final int VIEW_TYPE_PROFILE = 3;
118     private static final int VIEW_TYPE_AZ_LABEL = 4;
119     private static final int VIEW_TYPE_CALLER_AND_RANK = 5;
120     private static final int VIEW_TYPE_FOOTER = 6;
121 
122     private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
123 
124     private final ChooserActivityDelegate mChooserActivityDelegate;
125     private final ChooserListAdapter mChooserListAdapter;
126     private final LayoutInflater mLayoutInflater;
127 
128     private final int mMaxTargetsPerRow;
129     private final boolean mShouldShowContentPreview;
130     private final int mChooserWidthPixels;
131     private final int mChooserRowTextOptionTranslatePixelSize;
132     private final boolean mShowAzLabelIfPoss;
133 
134     private DirectShareViewHolder mDirectShareViewHolder;
135     private int mChooserTargetWidth = 0;
136 
137     private int mFooterHeight = 0;
138 
ChooserGridAdapter( Context context, ChooserActivityDelegate chooserActivityDelegate, ChooserListAdapter wrappedAdapter, boolean shouldShowContentPreview, int maxTargetsPerRow, int numSheetExpansions)139     public ChooserGridAdapter(
140             Context context,
141             ChooserActivityDelegate chooserActivityDelegate,
142             ChooserListAdapter wrappedAdapter,
143             boolean shouldShowContentPreview,
144             int maxTargetsPerRow,
145             int numSheetExpansions) {
146         super();
147 
148         mChooserActivityDelegate = chooserActivityDelegate;
149 
150         mChooserListAdapter = wrappedAdapter;
151         mLayoutInflater = LayoutInflater.from(context);
152 
153         mShouldShowContentPreview = shouldShowContentPreview;
154         mMaxTargetsPerRow = maxTargetsPerRow;
155 
156         mChooserWidthPixels = context.getResources().getDimensionPixelSize(R.dimen.chooser_width);
157         mChooserRowTextOptionTranslatePixelSize = context.getResources().getDimensionPixelSize(
158                 R.dimen.chooser_row_text_option_translate);
159 
160         mShowAzLabelIfPoss = numSheetExpansions < NUM_EXPANSIONS_TO_HIDE_AZ_LABEL;
161 
162         wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
163             @Override
164             public void onChanged() {
165                 super.onChanged();
166                 notifyDataSetChanged();
167             }
168 
169             @Override
170             public void onInvalidated() {
171                 super.onInvalidated();
172                 notifyDataSetChanged();
173             }
174         });
175     }
176 
177     public void setFooterHeight(int height) {
178         mFooterHeight = height;
179     }
180 
181     /**
182      * Calculate the chooser target width to maximize space per item
183      *
184      * @param width The new row width to use for recalculation
185      * @return true if the view width has changed
186      */
187     public boolean calculateChooserTargetWidth(int width) {
188         if (width == 0) {
189             return false;
190         }
191 
192         // Limit width to the maximum width of the chooser activity
193         int maxWidth = mChooserWidthPixels;
194         width = Math.min(maxWidth, width);
195 
196         int newWidth = width / mMaxTargetsPerRow;
197         if (newWidth != mChooserTargetWidth) {
198             mChooserTargetWidth = newWidth;
199             return true;
200         }
201 
202         return false;
203     }
204 
205     public int getRowCount() {
206         return (int) (
207                 getSystemRowCount()
208                         + getProfileRowCount()
209                         + getServiceTargetRowCount()
210                         + getCallerAndRankedTargetRowCount()
211                         + getAzLabelRowCount()
212                         + Math.ceil(
213                         (float) mChooserListAdapter.getAlphaTargetCount()
214                                 / mMaxTargetsPerRow)
215             );
216     }
217 
218     /**
219      * Whether the "system" row of targets is displayed.
220      * This area includes the content preview (if present) and action row.
221      */
222     public int getSystemRowCount() {
223         // For the tabbed case we show the sticky content preview above the tabs,
224         // please refer to shouldShowStickyContentPreview
225         if (mChooserActivityDelegate.shouldShowTabs()) {
226             return 0;
227         }
228 
229         if (!mShouldShowContentPreview) {
230             return 0;
231         }
232 
233         if (mChooserListAdapter == null || mChooserListAdapter.getCount() == 0) {
234             return 0;
235         }
236 
237         return 1;
238     }
239 
240     public int getProfileRowCount() {
241         if (mChooserActivityDelegate.shouldShowTabs()) {
242             return 0;
243         }
244         return mChooserListAdapter.getOtherProfile() == null ? 0 : 1;
245     }
246 
247     public int getFooterRowCount() {
248         return 1;
249     }
250 
251     public int getCallerAndRankedTargetRowCount() {
252         return (int) Math.ceil(
253                 ((float) mChooserListAdapter.getCallerTargetCount()
254                         + mChooserListAdapter.getRankedTargetCount()) / mMaxTargetsPerRow);
255     }
256 
257     // There can be at most one row in the listview, that is internally
258     // a ViewGroup with 2 rows
259     public int getServiceTargetRowCount() {
260         if (mShouldShowContentPreview && !ActivityManager.isLowRamDeviceStatic()) {
261             return 1;
262         }
263         return 0;
264     }
265 
266     public int getAzLabelRowCount() {
267         // Only show a label if the a-z list is showing
268         return (mShowAzLabelIfPoss && mChooserListAdapter.getAlphaTargetCount() > 0) ? 1 : 0;
269     }
270 
271     @Override
getItemCount()272     public int getItemCount() {
273         return (int) (
274                 getSystemRowCount()
275                         + getProfileRowCount()
276                         + getServiceTargetRowCount()
277                         + getCallerAndRankedTargetRowCount()
278                         + getAzLabelRowCount()
279                         + mChooserListAdapter.getAlphaTargetCount()
280                         + getFooterRowCount()
281             );
282     }
283 
284     @Override
onCreateViewHolder(ViewGroup parent, int viewType)285     public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
286         switch (viewType) {
287             case VIEW_TYPE_CONTENT_PREVIEW:
288                 return new ItemViewHolder(
289                         mChooserActivityDelegate.buildContentPreview(parent),
290                         viewType,
291                         null,
292                         null);
293             case VIEW_TYPE_PROFILE:
294                 return new ItemViewHolder(
295                         createProfileView(parent),
296                         viewType,
297                         null,
298                         null);
299             case VIEW_TYPE_AZ_LABEL:
300                 return new ItemViewHolder(
301                         createAzLabelView(parent),
302                         viewType,
303                         null,
304                         null);
305             case VIEW_TYPE_NORMAL:
306                 return new ItemViewHolder(
307                         mChooserListAdapter.createView(parent),
308                         viewType,
309                         mChooserActivityDelegate::onTargetSelected,
310                         mChooserActivityDelegate::onTargetLongPressed);
311             case VIEW_TYPE_DIRECT_SHARE:
312             case VIEW_TYPE_CALLER_AND_RANK:
313                 return createItemGroupViewHolder(viewType, parent);
314             case VIEW_TYPE_FOOTER:
315                 Space sp = new Space(parent.getContext());
316                 sp.setLayoutParams(new RecyclerView.LayoutParams(
317                         LayoutParams.MATCH_PARENT, mFooterHeight));
318                 return new FooterViewHolder(sp, viewType);
319             default:
320                 // Since we catch all possible viewTypes above, no chance this is being called.
321                 return null;
322         }
323     }
324 
325     @Override
onBindViewHolder(RecyclerView.ViewHolder holder, int position)326     public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
327         int viewType = ((ViewHolderBase) holder).getViewType();
328         switch (viewType) {
329             case VIEW_TYPE_DIRECT_SHARE:
330             case VIEW_TYPE_CALLER_AND_RANK:
331                 bindItemGroupViewHolder(position, (ItemGroupViewHolder) holder);
332                 break;
333             case VIEW_TYPE_NORMAL:
334                 bindItemViewHolder(position, (ItemViewHolder) holder);
335                 break;
336             default:
337         }
338     }
339 
340     @Override
getItemViewType(int position)341     public int getItemViewType(int position) {
342         int count;
343 
344         int countSum = (count = getSystemRowCount());
345         if (count > 0 && position < countSum) return VIEW_TYPE_CONTENT_PREVIEW;
346 
347         countSum += (count = getProfileRowCount());
348         if (count > 0 && position < countSum) return VIEW_TYPE_PROFILE;
349 
350         countSum += (count = getServiceTargetRowCount());
351         if (count > 0 && position < countSum) return VIEW_TYPE_DIRECT_SHARE;
352 
353         countSum += (count = getCallerAndRankedTargetRowCount());
354         if (count > 0 && position < countSum) return VIEW_TYPE_CALLER_AND_RANK;
355 
356         countSum += (count = getAzLabelRowCount());
357         if (count > 0 && position < countSum) return VIEW_TYPE_AZ_LABEL;
358 
359         if (position == getItemCount() - 1) return VIEW_TYPE_FOOTER;
360 
361         return VIEW_TYPE_NORMAL;
362     }
363 
getTargetType(int position)364     public int getTargetType(int position) {
365         return mChooserListAdapter.getPositionTargetType(getListPosition(position));
366     }
367 
createProfileView(ViewGroup parent)368     private View createProfileView(ViewGroup parent) {
369         View profileRow = mLayoutInflater.inflate(R.layout.chooser_profile_row, parent, false);
370         mChooserActivityDelegate.updateProfileViewButton(profileRow);
371         return profileRow;
372     }
373 
createAzLabelView(ViewGroup parent)374     private View createAzLabelView(ViewGroup parent) {
375         return mLayoutInflater.inflate(R.layout.chooser_az_label_row, parent, false);
376     }
377 
loadViewsIntoGroup(ItemGroupViewHolder holder)378     private ItemGroupViewHolder loadViewsIntoGroup(ItemGroupViewHolder holder) {
379         final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
380         final int exactSpec = MeasureSpec.makeMeasureSpec(mChooserTargetWidth, MeasureSpec.EXACTLY);
381         int columnCount = holder.getColumnCount();
382 
383         final boolean isDirectShare = holder instanceof DirectShareViewHolder;
384 
385         for (int i = 0; i < columnCount; i++) {
386             final View v = mChooserListAdapter.createView(holder.getRowByIndex(i));
387             final int column = i;
388             v.setOnClickListener(new OnClickListener() {
389                 @Override
390                 public void onClick(View v) {
391                     mChooserActivityDelegate.onTargetSelected(holder.getItemIndex(column));
392                 }
393             });
394 
395             // Show menu for both direct share and app share targets after long click.
396             v.setOnLongClickListener(v1 -> {
397                 mChooserActivityDelegate.onTargetLongPressed(holder.getItemIndex(column));
398                 return true;
399             });
400 
401             holder.addView(i, v);
402 
403             // Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll =
404             // false. TextView#setHorizontallyScrolling must be reset after #setLines. Must be
405             // done before measuring.
406             if (isDirectShare) {
407                 final ViewHolder vh = (ViewHolder) v.getTag();
408                 vh.text.setLines(2);
409                 vh.text.setHorizontallyScrolling(false);
410                 vh.text2.setVisibility(View.GONE);
411             }
412 
413             // Force height to be a given so we don't have visual disruption during scaling.
414             v.measure(exactSpec, spec);
415             setViewBounds(v, v.getMeasuredWidth(), v.getMeasuredHeight());
416         }
417 
418         final ViewGroup viewGroup = holder.getViewGroup();
419 
420         // Pre-measure and fix height so we can scale later.
421         holder.measure();
422         setViewBounds(viewGroup, LayoutParams.MATCH_PARENT, holder.getMeasuredRowHeight());
423 
424         if (isDirectShare) {
425             DirectShareViewHolder dsvh = (DirectShareViewHolder) holder;
426             setViewBounds(dsvh.getRow(0), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
427             setViewBounds(dsvh.getRow(1), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
428         }
429 
430         viewGroup.setTag(holder);
431         return holder;
432     }
433 
setViewBounds(View view, int widthPx, int heightPx)434     private void setViewBounds(View view, int widthPx, int heightPx) {
435         LayoutParams lp = view.getLayoutParams();
436         if (lp == null) {
437             lp = new LayoutParams(widthPx, heightPx);
438             view.setLayoutParams(lp);
439         } else {
440             lp.height = heightPx;
441             lp.width = widthPx;
442         }
443     }
444 
createItemGroupViewHolder(int viewType, ViewGroup parent)445     ItemGroupViewHolder createItemGroupViewHolder(int viewType, ViewGroup parent) {
446         if (viewType == VIEW_TYPE_DIRECT_SHARE) {
447             ViewGroup parentGroup = (ViewGroup) mLayoutInflater.inflate(
448                     R.layout.chooser_row_direct_share, parent, false);
449             ViewGroup row1 = (ViewGroup) mLayoutInflater.inflate(
450                     R.layout.chooser_row, parentGroup, false);
451             ViewGroup row2 = (ViewGroup) mLayoutInflater.inflate(
452                     R.layout.chooser_row, parentGroup, false);
453             parentGroup.addView(row1);
454             parentGroup.addView(row2);
455 
456             mDirectShareViewHolder = new DirectShareViewHolder(parentGroup,
457                     Lists.newArrayList(row1, row2), mMaxTargetsPerRow, viewType,
458                     mChooserActivityDelegate::getValidTargetCount);
459             loadViewsIntoGroup(mDirectShareViewHolder);
460 
461             return mDirectShareViewHolder;
462         } else {
463             ViewGroup row = (ViewGroup) mLayoutInflater.inflate(
464                     R.layout.chooser_row, parent, false);
465             ItemGroupViewHolder holder =
466                     new SingleRowViewHolder(row, mMaxTargetsPerRow, viewType);
467             loadViewsIntoGroup(holder);
468 
469             return holder;
470         }
471     }
472 
473     /**
474      * Need to merge CALLER + ranked STANDARD into a single row and prevent a separator from
475      * showing on top of the AZ list if the AZ label is visible. All other types are placed into
476      * their own row as determined by their target type, and dividers are added in the list to
477      * separate each type.
478      */
getRowType(int rowPosition)479     int getRowType(int rowPosition) {
480         // Merge caller and ranked standard into a single row
481         int positionType = mChooserListAdapter.getPositionTargetType(rowPosition);
482         if (positionType == ChooserListAdapter.TARGET_CALLER) {
483             return ChooserListAdapter.TARGET_STANDARD;
484         }
485 
486         // If an A-Z label is shown, prevent a separator from appearing by making the A-Z
487         // row type the same as the suggestion row type
488         if (getAzLabelRowCount() > 0 && positionType == ChooserListAdapter.TARGET_STANDARD_AZ) {
489             return ChooserListAdapter.TARGET_STANDARD;
490         }
491 
492         return positionType;
493     }
494 
bindItemViewHolder(int position, ItemViewHolder holder)495     void bindItemViewHolder(int position, ItemViewHolder holder) {
496         View v = holder.itemView;
497         int listPosition = getListPosition(position);
498         holder.setListPosition(listPosition);
499         mChooserListAdapter.bindView(listPosition, v);
500     }
501 
bindItemGroupViewHolder(int position, ItemGroupViewHolder holder)502     void bindItemGroupViewHolder(int position, ItemGroupViewHolder holder) {
503         final ViewGroup viewGroup = (ViewGroup) holder.itemView;
504         int start = getListPosition(position);
505         int startType = getRowType(start);
506 
507         int columnCount = holder.getColumnCount();
508         int end = start + columnCount - 1;
509         while (getRowType(end) != startType && end >= start) {
510             end--;
511         }
512 
513         if (end == start && mChooserListAdapter.getItem(start).isEmptyTargetInfo()) {
514             final TextView textView = viewGroup.findViewById(
515                     com.android.internal.R.id.chooser_row_text_option);
516 
517             if (textView.getVisibility() != View.VISIBLE) {
518                 textView.setAlpha(0.0f);
519                 textView.setVisibility(View.VISIBLE);
520                 textView.setText(R.string.chooser_no_direct_share_targets);
521 
522                 ValueAnimator fadeAnim = ObjectAnimator.ofFloat(textView, "alpha", 0.0f, 1.0f);
523                 fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
524 
525                 textView.setTranslationY(mChooserRowTextOptionTranslatePixelSize);
526                 ValueAnimator translateAnim =
527                         ObjectAnimator.ofFloat(textView, "translationY", 0.0f);
528                 translateAnim.setInterpolator(new DecelerateInterpolator(1.0f));
529 
530                 AnimatorSet animSet = new AnimatorSet();
531                 animSet.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
532                 animSet.setStartDelay(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
533                 animSet.playTogether(fadeAnim, translateAnim);
534                 animSet.start();
535             }
536         }
537 
538         for (int i = 0; i < columnCount; i++) {
539             final View v = holder.getView(i);
540 
541             if (start + i <= end) {
542                 holder.setViewVisibility(i, View.VISIBLE);
543                 holder.setItemIndex(i, start + i);
544                 mChooserListAdapter.bindView(holder.getItemIndex(i), v);
545             } else {
546                 holder.setViewVisibility(i, View.INVISIBLE);
547             }
548         }
549     }
550 
getListPosition(int position)551     int getListPosition(int position) {
552         position -= getSystemRowCount() + getProfileRowCount();
553 
554         final int serviceCount = mChooserListAdapter.getServiceTargetCount();
555         final int serviceRows = (int) Math.ceil((float) serviceCount / mMaxTargetsPerRow);
556         if (position < serviceRows) {
557             return position * mMaxTargetsPerRow;
558         }
559 
560         position -= serviceRows;
561 
562         final int callerAndRankedCount =
563                 mChooserListAdapter.getCallerTargetCount()
564                 + mChooserListAdapter.getRankedTargetCount();
565         final int callerAndRankedRows = getCallerAndRankedTargetRowCount();
566         if (position < callerAndRankedRows) {
567             return serviceCount + position * mMaxTargetsPerRow;
568         }
569 
570         position -= getAzLabelRowCount() + callerAndRankedRows;
571 
572         return callerAndRankedCount + serviceCount + position;
573     }
574 
handleScroll(View v, int y, int oldy)575     public void handleScroll(View v, int y, int oldy) {
576         boolean canExpandDirectShare = canExpandDirectShare();
577         if (mDirectShareViewHolder != null && canExpandDirectShare) {
578             mChooserActivityDelegate.handleScrollToExpandDirectShare(
579                     mDirectShareViewHolder, y, oldy);
580         }
581     }
582 
583     /** Only expand direct share area if there is a minimum number of targets. */
canExpandDirectShare()584     private boolean canExpandDirectShare() {
585         // Do not enable until we have confirmed more apps are using sharing shortcuts
586         // Check git history for enablement logic
587         return false;
588     }
589 
getListAdapter()590     public ChooserListAdapter getListAdapter() {
591         return mChooserListAdapter;
592     }
593 
shouldCellSpan(int position)594     public boolean shouldCellSpan(int position) {
595         return getItemViewType(position) == VIEW_TYPE_NORMAL;
596     }
597 
updateDirectShareExpansion()598     public void updateDirectShareExpansion() {
599         if (mDirectShareViewHolder == null || !canExpandDirectShare()) {
600             return;
601         }
602         mChooserActivityDelegate.updateDirectShareExpansion(mDirectShareViewHolder);
603     }
604 }
605