• 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.launcher3.dragndrop;
18 
19 import static android.view.View.MeasureSpec.EXACTLY;
20 import static android.view.View.MeasureSpec.makeMeasureSpec;
21 
22 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
23 import static com.android.launcher3.Utilities.getBadge;
24 import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
25 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
26 
27 import android.animation.Animator;
28 import android.animation.AnimatorListenerAdapter;
29 import android.animation.AnimatorSet;
30 import android.animation.ObjectAnimator;
31 import android.animation.ValueAnimator;
32 import android.animation.ValueAnimator.AnimatorUpdateListener;
33 import android.annotation.TargetApi;
34 import android.content.Context;
35 import android.graphics.Canvas;
36 import android.graphics.Color;
37 import android.graphics.ColorFilter;
38 import android.graphics.Path;
39 import android.graphics.Picture;
40 import android.graphics.Point;
41 import android.graphics.Rect;
42 import android.graphics.drawable.AdaptiveIconDrawable;
43 import android.graphics.drawable.ColorDrawable;
44 import android.graphics.drawable.Drawable;
45 import android.graphics.drawable.PictureDrawable;
46 import android.os.Build;
47 import android.os.Handler;
48 import android.os.Looper;
49 import android.view.View;
50 import android.view.ViewGroup;
51 import android.widget.FrameLayout;
52 import android.widget.ImageView;
53 
54 import androidx.annotation.Nullable;
55 import androidx.dynamicanimation.animation.FloatPropertyCompat;
56 import androidx.dynamicanimation.animation.SpringAnimation;
57 import androidx.dynamicanimation.animation.SpringForce;
58 
59 import com.android.launcher3.R;
60 import com.android.launcher3.Utilities;
61 import com.android.launcher3.anim.Interpolators;
62 import com.android.launcher3.icons.FastBitmapDrawable;
63 import com.android.launcher3.icons.LauncherIcons;
64 import com.android.launcher3.model.data.ItemInfo;
65 import com.android.launcher3.util.RunnableList;
66 import com.android.launcher3.views.ActivityContext;
67 import com.android.launcher3.views.BaseDragLayer;
68 
69 /** A custom view for rendering an icon, folder, shortcut or widget during drag-n-drop. */
70 public abstract class DragView<T extends Context & ActivityContext> extends FrameLayout {
71 
72     public static final int VIEW_ZOOM_DURATION = 150;
73 
74     private final View mContent;
75     // The following are only used for rendering mContent directly during drag-n-drop.
76     @Nullable private ViewGroup.LayoutParams mContentViewLayoutParams;
77     @Nullable private ViewGroup mContentViewParent;
78     private int mContentViewInParentViewIndex = -1;
79     private final int mWidth;
80     private final int mHeight;
81 
82     private final int mBlurSizeOutline;
83     protected final int mRegistrationX;
84     protected final int mRegistrationY;
85     private final float mInitialScale;
86     private final float mEndScale;
87     protected final float mScaleOnDrop;
88     protected final int[] mTempLoc = new int[2];
89 
90     private final RunnableList mOnDragStartCallback = new RunnableList();
91 
92     private Point mDragVisualizeOffset = null;
93     private Rect mDragRegion = null;
94     protected final T mActivity;
95     private final BaseDragLayer<T> mDragLayer;
96     private boolean mHasDrawn = false;
97 
98     final ValueAnimator mAnim;
99     // Whether mAnim has started. Unlike mAnim.isStarted(), this is true even after mAnim ends.
100     private boolean mAnimStarted;
101     private Runnable mOnAnimEndCallback = null;
102 
103     private int mLastTouchX;
104     private int mLastTouchY;
105     private int mAnimatedShiftX;
106     private int mAnimatedShiftY;
107 
108     // Below variable only needed IF FeatureFlags.LAUNCHER3_SPRING_ICONS is {@code true}
109     private Drawable mBgSpringDrawable, mFgSpringDrawable;
110     private SpringFloatValue mTranslateX, mTranslateY;
111     private Path mScaledMaskPath;
112     private Drawable mBadge;
113 
DragView(T launcher, Drawable drawable, int registrationX, int registrationY, final float initialScale, final float scaleOnDrop, final float finalScaleDps)114     public DragView(T launcher, Drawable drawable, int registrationX,
115             int registrationY, final float initialScale, final float scaleOnDrop,
116             final float finalScaleDps) {
117         this(launcher, getViewFromDrawable(launcher, drawable),
118                 drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
119                 registrationX, registrationY, initialScale, scaleOnDrop, finalScaleDps);
120     }
121 
122     /**
123      * Construct the drag view.
124      * <p>
125      * The registration point is the point inside our view that the touch events should
126      * be centered upon.
127      * @param activity The Launcher instance/ActivityContext this DragView is in.
128      * @param content the view content that is attached to the drag view.
129      * @param width the width of the dragView
130      * @param height the height of the dragView
131      * @param initialScale The view that we're dragging around.  We scale it up when we draw it.
132      * @param registrationX The x coordinate of the registration point.
133      * @param registrationY The y coordinate of the registration point.
134      * @param scaleOnDrop the scale used in the drop animation.
135      * @param finalScaleDps the scale used in the zoom out animation when the drag view is shown.
136      */
DragView(T activity, View content, int width, int height, int registrationX, int registrationY, final float initialScale, final float scaleOnDrop, final float finalScaleDps)137     public DragView(T activity, View content, int width, int height, int registrationX,
138             int registrationY, final float initialScale, final float scaleOnDrop,
139             final float finalScaleDps) {
140         super(activity);
141         mActivity = activity;
142         mDragLayer = activity.getDragLayer();
143 
144         mContent = content;
145         mWidth = width;
146         mHeight = height;
147         mContentViewLayoutParams = mContent.getLayoutParams();
148         if (mContent.getParent() instanceof ViewGroup) {
149             mContentViewParent = (ViewGroup) mContent.getParent();
150             mContentViewInParentViewIndex = mContentViewParent.indexOfChild(mContent);
151             mContentViewParent.removeView(mContent);
152         }
153 
154         addView(content, new LayoutParams(width, height));
155 
156         // If there is already a scale set on the content, we don't want to clip the children.
157         if (content.getScaleX() != 1 || content.getScaleY() != 1) {
158             setClipChildren(false);
159             setClipToPadding(false);
160         }
161 
162         mEndScale = (width + finalScaleDps) / width;
163 
164         // Set the initial scale to avoid any jumps
165         setScaleX(initialScale);
166         setScaleY(initialScale);
167 
168         // Animate the view into the correct position
169         mAnim = ValueAnimator.ofFloat(0f, 1f);
170         mAnim.setDuration(VIEW_ZOOM_DURATION);
171         mAnim.addUpdateListener(animation -> {
172             final float value = (Float) animation.getAnimatedValue();
173             setScaleX(Utilities.mapRange(value, initialScale, mEndScale));
174             setScaleY(Utilities.mapRange(value, initialScale, mEndScale));
175             if (!isAttachedToWindow()) {
176                 animation.cancel();
177             }
178         });
179         mAnim.addListener(new AnimatorListenerAdapter() {
180             @Override
181             public void onAnimationStart(Animator animation) {
182                 mAnimStarted = true;
183             }
184 
185             @Override
186             public void onAnimationEnd(Animator animation) {
187                 super.onAnimationEnd(animation);
188                 if (mOnAnimEndCallback != null) {
189                     mOnAnimEndCallback.run();
190                 }
191             }
192         });
193 
194         setDragRegion(new Rect(0, 0, width, height));
195 
196         // The point in our scaled bitmap that the touch events are located
197         mRegistrationX = registrationX;
198         mRegistrationY = registrationY;
199 
200         mInitialScale = initialScale;
201         mScaleOnDrop = scaleOnDrop;
202 
203         // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass
204         measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
205 
206         mBlurSizeOutline = getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline);
207         setElevation(getResources().getDimension(R.dimen.drag_elevation));
208         setWillNotDraw(false);
209     }
210 
setOnAnimationEndCallback(Runnable callback)211     public void setOnAnimationEndCallback(Runnable callback) {
212         mOnAnimEndCallback = callback;
213     }
214 
215     /**
216      * Initialize {@code #mIconDrawable} if the item can be represented using
217      * an {@link AdaptiveIconDrawable} or {@link FolderAdaptiveIcon}.
218      */
219     @TargetApi(Build.VERSION_CODES.O)
setItemInfo(final ItemInfo info)220     public void setItemInfo(final ItemInfo info) {
221         // Load the adaptive icon on a background thread and add the view in ui thread.
222         MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
223             Object[] outObj = new Object[1];
224             int w = mWidth;
225             int h = mHeight;
226             Drawable dr = Utilities.getFullDrawable(mActivity, info, w, h,
227                     true /* shouldThemeIcon */, outObj);
228 
229             if (dr instanceof AdaptiveIconDrawable) {
230                 int blurMargin = (int) mActivity.getResources()
231                         .getDimension(R.dimen.blur_size_medium_outline) / 2;
232 
233                 Rect bounds = new Rect(0, 0, w, h);
234                 bounds.inset(blurMargin, blurMargin);
235                 // Badge is applied after icon normalization so the bounds for badge should not
236                 // be scaled down due to icon normalization.
237                 mBadge = getBadge(mActivity, info, outObj[0]);
238                 FastBitmapDrawable.setBadgeBounds(mBadge, bounds);
239 
240                 // Do not draw the background in case of folder as its translucent
241                 final boolean shouldDrawBackground = !(dr instanceof FolderAdaptiveIcon);
242 
243                 try (LauncherIcons li = LauncherIcons.obtain(mActivity)) {
244                     Drawable nDr; // drawable to be normalized
245                     if (shouldDrawBackground) {
246                         nDr = dr;
247                     } else {
248                         // Since we just want the scale, avoid heavy drawing operations
249                         nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
250                     }
251                     Utilities.scaleRectAboutCenter(bounds,
252                             li.getNormalizer().getScale(nDr, null, null, null));
253                 }
254                 AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr;
255 
256                 // Shrink very tiny bit so that the clip path is smaller than the original bitmap
257                 // that has anti aliased edges and shadows.
258                 Rect shrunkBounds = new Rect(bounds);
259                 Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
260                 adaptiveIcon.setBounds(shrunkBounds);
261                 final Path mask = adaptiveIcon.getIconMask();
262 
263                 mTranslateX = new SpringFloatValue(DragView.this,
264                         w * AdaptiveIconDrawable.getExtraInsetFraction());
265                 mTranslateY = new SpringFloatValue(DragView.this,
266                         h * AdaptiveIconDrawable.getExtraInsetFraction());
267 
268                 bounds.inset(
269                         (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
270                         (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction())
271                 );
272                 mBgSpringDrawable = adaptiveIcon.getBackground();
273                 if (mBgSpringDrawable == null) {
274                     mBgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
275                 }
276                 mBgSpringDrawable.setBounds(bounds);
277                 mFgSpringDrawable = adaptiveIcon.getForeground();
278                 if (mFgSpringDrawable == null) {
279                     mFgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
280                 }
281                 mFgSpringDrawable.setBounds(bounds);
282 
283                 new Handler(Looper.getMainLooper()).post(() -> mOnDragStartCallback.add(() -> {
284                     // TODO: Consider fade-in animation
285                     // Assign the variable on the UI thread to avoid race conditions.
286                     mScaledMaskPath = mask;
287                     // Avoid relayout as we do not care about children affecting layout
288                     removeAllViewsInLayout();
289 
290                     if (info.isDisabled()) {
291                         ColorFilter filter = getDisabledColorFilter();
292                         mBgSpringDrawable.setColorFilter(filter);
293                         mFgSpringDrawable.setColorFilter(filter);
294                         mBadge.setColorFilter(filter);
295                     }
296                     invalidate();
297                 }));
298             }
299         });
300     }
301 
302     /**
303      * Called when pre-drag finishes for an icon
304      */
onDragStart()305     public void onDragStart() {
306         mOnDragStartCallback.executeAllAndDestroy();
307     }
308 
309     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)310     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
311         super.onMeasure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
312     }
313 
getDragRegionWidth()314     public int getDragRegionWidth() {
315         return mDragRegion.width();
316     }
317 
getDragRegionHeight()318     public int getDragRegionHeight() {
319         return mDragRegion.height();
320     }
321 
setDragVisualizeOffset(Point p)322     public void setDragVisualizeOffset(Point p) {
323         mDragVisualizeOffset = p;
324     }
325 
getDragVisualizeOffset()326     public Point getDragVisualizeOffset() {
327         return mDragVisualizeOffset;
328     }
329 
setDragRegion(Rect r)330     public void setDragRegion(Rect r) {
331         mDragRegion = r;
332     }
333 
getDragRegion()334     public Rect getDragRegion() {
335         return mDragRegion;
336     }
337 
338     @Override
draw(Canvas canvas)339     public void draw(Canvas canvas) {
340         super.draw(canvas);
341 
342         // Draw after the content
343         mHasDrawn = true;
344         if (mScaledMaskPath != null) {
345             int cnt = canvas.save();
346             canvas.clipPath(mScaledMaskPath);
347             mBgSpringDrawable.draw(canvas);
348             canvas.translate(mTranslateX.mValue, mTranslateY.mValue);
349             mFgSpringDrawable.draw(canvas);
350             canvas.restoreToCount(cnt);
351             mBadge.draw(canvas);
352         }
353     }
354 
crossFadeContent(Drawable crossFadeDrawable, int duration)355     public void crossFadeContent(Drawable crossFadeDrawable, int duration) {
356         if (mContent.getParent() == null) {
357             // If the content is already removed, ignore
358             return;
359         }
360         ImageView newContent = getViewFromDrawable(getContext(), crossFadeDrawable);
361         // We need to fill the ImageView with the content, otherwise the shapes of the final view
362         // and the drag view might not match exactly
363         newContent.setScaleType(ImageView.ScaleType.FIT_XY);
364         newContent.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
365         newContent.layout(0, 0, mWidth, mHeight);
366         addViewInLayout(newContent, 0, new LayoutParams(mWidth, mHeight));
367 
368         AnimatorSet anim = new AnimatorSet();
369         anim.play(ObjectAnimator.ofFloat(newContent, VIEW_ALPHA, 0, 1));
370         anim.play(ObjectAnimator.ofFloat(mContent, VIEW_ALPHA, 0));
371         anim.setDuration(duration).setInterpolator(Interpolators.DEACCEL_1_5);
372         anim.start();
373     }
374 
hasDrawn()375     public boolean hasDrawn() {
376         return mHasDrawn;
377     }
378 
379     /**
380      * Create a window containing this view and show it.
381      *
382      * @param touchX the x coordinate the user touched in DragLayer coordinates
383      * @param touchY the y coordinate the user touched in DragLayer coordinates
384      */
show(int touchX, int touchY)385     public void show(int touchX, int touchY) {
386         mDragLayer.addView(this);
387 
388         // Start the pick-up animation
389         BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(mWidth, mHeight);
390         lp.customPosition = true;
391         setLayoutParams(lp);
392 
393         if (mContent != null) {
394             // At the drag start, the source view visibility is set to invisible.
395             mContent.setVisibility(VISIBLE);
396         }
397 
398         move(touchX, touchY);
399         // Post the animation to skip other expensive work happening on the first frame
400         post(mAnim::start);
401     }
402 
cancelAnimation()403     public void cancelAnimation() {
404         if (mAnim != null && mAnim.isRunning()) {
405             mAnim.cancel();
406         }
407     }
408 
isAnimationFinished()409     public boolean isAnimationFinished() {
410         return mAnimStarted && !mAnim.isRunning();
411     }
412 
413     /**
414      * Move the window containing this view.
415      *
416      * @param touchX the x coordinate the user touched in DragLayer coordinates
417      * @param touchY the y coordinate the user touched in DragLayer coordinates
418      */
move(int touchX, int touchY)419     public void move(int touchX, int touchY) {
420         if (touchX > 0 && touchY > 0 && mLastTouchX > 0 && mLastTouchY > 0
421                 && mScaledMaskPath != null) {
422             mTranslateX.animateToPos(mLastTouchX - touchX);
423             mTranslateY.animateToPos(mLastTouchY - touchY);
424         }
425         mLastTouchX = touchX;
426         mLastTouchY = touchY;
427         applyTranslation();
428     }
429 
430     /**
431      * Animate this DragView to the given DragLayer coordinates and then remove it.
432      */
animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration)433     public abstract void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable,
434             int duration);
435 
animateShift(final int shiftX, final int shiftY)436     public void animateShift(final int shiftX, final int shiftY) {
437         if (mAnim.isStarted()) {
438             return;
439         }
440         mAnimatedShiftX = shiftX;
441         mAnimatedShiftY = shiftY;
442         applyTranslation();
443         mAnim.addUpdateListener(new AnimatorUpdateListener() {
444             @Override
445             public void onAnimationUpdate(ValueAnimator animation) {
446                 float fraction = 1 - animation.getAnimatedFraction();
447                 mAnimatedShiftX = (int) (fraction * shiftX);
448                 mAnimatedShiftY = (int) (fraction * shiftY);
449                 applyTranslation();
450             }
451         });
452     }
453 
applyTranslation()454     private void applyTranslation() {
455         setTranslationX(mLastTouchX - mRegistrationX + mAnimatedShiftX);
456         setTranslationY(mLastTouchY - mRegistrationY + mAnimatedShiftY);
457     }
458 
459     /**
460      * Detaches {@link #mContent}, if previously attached, from this view.
461      *
462      * <p>In the case of no change in the drop position, sets {@code reattachToPreviousParent} to
463      * {@code true} to attach the {@link #mContent} back to its previous parent.
464      */
detachContentView(boolean reattachToPreviousParent)465     public void detachContentView(boolean reattachToPreviousParent) {
466         if (mContent != null && mContentViewParent != null && mContentViewInParentViewIndex >= 0) {
467             Picture picture = new Picture();
468             mContent.draw(picture.beginRecording(mWidth, mHeight));
469             picture.endRecording();
470             View view = new View(mActivity);
471             view.setBackground(new PictureDrawable(picture));
472             view.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
473             view.layout(mContent.getLeft(), mContent.getTop(),
474                     mContent.getRight(), mContent.getBottom());
475             setClipToOutline(mContent.getClipToOutline());
476             setOutlineProvider(mContent.getOutlineProvider());
477             addViewInLayout(view, indexOfChild(mContent), mContent.getLayoutParams(), true);
478 
479             removeViewInLayout(mContent);
480             mContent.setVisibility(INVISIBLE);
481             mContent.setLayoutParams(mContentViewLayoutParams);
482             if (reattachToPreviousParent) {
483                 mContentViewParent.addView(mContent, mContentViewInParentViewIndex);
484             }
485             mContentViewParent = null;
486             mContentViewInParentViewIndex = -1;
487         }
488     }
489 
490     /**
491      * Removes this view from the {@link DragLayer}.
492      *
493      * <p>If the drag content is a {@link #mContent}, this call doesn't reattach the
494      * {@link #mContent} back to its previous parent. To reattach to previous parent, the caller
495      * should call {@link #detachContentView} with {@code reattachToPreviousParent} sets to true
496      * before this call.
497      */
remove()498     public void remove() {
499         if (getParent() != null) {
500             mDragLayer.removeView(DragView.this);
501         }
502     }
503 
getBlurSizeOutline()504     public int getBlurSizeOutline() {
505         return mBlurSizeOutline;
506     }
507 
getInitialScale()508     public float getInitialScale() {
509         return mInitialScale;
510     }
511 
getEndScale()512     public float getEndScale() {
513         return mEndScale;
514     }
515 
516     @Override
hasOverlappingRendering()517     public boolean hasOverlappingRendering() {
518         return false;
519     }
520 
521     /** Returns the current content view that is rendered in the drag view. */
getContentView()522     public View getContentView() {
523         return mContent;
524     }
525 
526     /**
527      * Returns the previous {@link ViewGroup} parent of the {@link #mContent} before the drag
528      * content is attached to this view.
529      */
530     @Nullable
getContentViewParent()531     public ViewGroup getContentViewParent() {
532         return mContentViewParent;
533     }
534 
535     private static class SpringFloatValue {
536 
537         private static final FloatPropertyCompat<SpringFloatValue> VALUE =
538                 new FloatPropertyCompat<SpringFloatValue>("value") {
539                     @Override
540                     public float getValue(SpringFloatValue object) {
541                         return object.mValue;
542                     }
543 
544                     @Override
545                     public void setValue(SpringFloatValue object, float value) {
546                         object.mValue = value;
547                         object.mView.invalidate();
548                     }
549                 };
550 
551         // Following three values are fine tuned with motion ux designer
552         private static final int STIFFNESS = 4000;
553         private static final float DAMPENING_RATIO = 1f;
554         private static final int PARALLAX_MAX_IN_DP = 8;
555 
556         private final View mView;
557         private final SpringAnimation mSpring;
558         private final float mDelta;
559 
560         private float mValue;
561 
SpringFloatValue(View view, float range)562         public SpringFloatValue(View view, float range) {
563             mView = view;
564             mSpring = new SpringAnimation(this, VALUE, 0)
565                     .setMinValue(-range).setMaxValue(range)
566                     .setSpring(new SpringForce(0)
567                             .setDampingRatio(DAMPENING_RATIO)
568                             .setStiffness(STIFFNESS));
569             mDelta = Math.min(
570                     range, view.getResources().getDisplayMetrics().density * PARALLAX_MAX_IN_DP);
571         }
572 
animateToPos(float value)573         public void animateToPos(float value) {
574             mSpring.animateToFinalPosition(Utilities.boundToRange(value, -mDelta, mDelta));
575         }
576     }
577 
getViewFromDrawable(Context context, Drawable drawable)578     private static ImageView getViewFromDrawable(Context context, Drawable drawable) {
579         ImageView iv = new ImageView(context);
580         iv.setImageDrawable(drawable);
581         return iv;
582     }
583 
584     /**
585      * Removes any stray DragView from the DragLayer.
586      */
removeAllViews(ActivityContext activity)587     public static void removeAllViews(ActivityContext activity) {
588         BaseDragLayer dragLayer = activity.getDragLayer();
589         // Iterate in reverse order. DragView is added later to the dragLayer,
590         // and will be one of the last views.
591         for (int i = dragLayer.getChildCount() - 1; i >= 0; i--) {
592             View child = dragLayer.getChildAt(i);
593             if (child instanceof DragView) {
594                 dragLayer.removeView(child);
595             }
596         }
597     }
598 }
599