1d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd/* 2d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * Copyright (C) 2015 The Android Open Source Project 3d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * 4d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * Licensed under the Apache License, Version 2.0 (the "License"); 5d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * you may not use this file except in compliance with the License. 6d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * You may obtain a copy of the License at 7d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * 8d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * http://www.apache.org/licenses/LICENSE-2.0 9d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * 10d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * Unless required by applicable law or agreed to in writing, software 11d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * distributed under the License is distributed on an "AS IS" BASIS, 12d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * See the License for the specific language governing permissions and 14d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * limitations under the License. 15d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd */ 16d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddpackage com.android.messaging.ui.animation; 17d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd 18d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.annotation.TargetApi; 19d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.app.Activity; 20d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.content.Context; 21d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.content.res.Resources; 22d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.graphics.Bitmap; 23d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.graphics.Canvas; 24d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.graphics.Rect; 25d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.graphics.drawable.BitmapDrawable; 26d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.graphics.drawable.ColorDrawable; 27d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.graphics.drawable.Drawable; 28d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.support.v4.view.ViewCompat; 29d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.view.View; 30d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.view.ViewGroup; 31d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.view.ViewGroupOverlay; 32d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.view.ViewOverlay; 33d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport android.widget.FrameLayout; 34d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd 35d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport com.android.messaging.R; 36d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport com.android.messaging.util.ImageUtils; 37d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport com.android.messaging.util.OsUtil; 38d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddimport com.android.messaging.util.UiUtils; 39d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd 40d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd/** 41d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * <p> 42d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * Shows a vertical "explode" animation for any view inside a view group (e.g. views inside a 43d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * ListView). During the animation, a snapshot is taken for the view to the animated and 44d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * presented in a popup window or view overlay on top of the original view group. The background 45d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * of the view (a highlight) vertically expands (explodes) during the animation. 46d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * </p> 47d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * <p> 48d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * The exact implementation of the animation depends on platform API level. For JB_MR2 and later, 49d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * the implementation utilizes ViewOverlay to perform highly performant overlay animations; for 50d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * older API levels, the implementation falls back to using a full screen popup window to stage 51d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * the animation. 52d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * </p> 53d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * <p> 54d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * To start this animation, call {@link #startAnimationForView(ViewGroup, View, View, boolean, int)} 55d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * </p> 56d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd */ 57d3b009ae55651f1e60950342468e3c37fdeb0796Mike Doddpublic class ViewGroupItemVerticalExplodeAnimation { 58d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd /** 59d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * Starts a vertical explode animation for a given view situated in a given container. 60d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * 61d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * @param container the container of the view which determines the explode animation's final 62d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * size 63d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * @param viewToAnimate the view to be animated. The view will be highlighted by the explode 64d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * highlight, which expands from the size of the view to the size of the container. 65d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * @param animationStagingView the view that stages the animation. Since viewToAnimate may be 66d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * removed from the view tree during the animation, we need a view that'll be alive 67d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * for the duration of the animation so that the animation won't get cancelled. 68d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * @param snapshotView whether a snapshot of the view to animate is needed. 69d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd */ 70d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd public static void startAnimationForView(final ViewGroup container, final View viewToAnimate, 71d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final View animationStagingView, final boolean snapshotView, final int duration) { 72d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd if (OsUtil.isAtLeastJB_MR2() && (viewToAnimate.getContext() instanceof Activity)) { 73d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd new ViewExplodeAnimationJellyBeanMR2(viewToAnimate, container, snapshotView, duration) 74d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd .startAnimation(); 75d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } else { 76d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Pre JB_MR2, this animation can cause rendering failures which causes the framework 77d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // to fall back to software rendering where camera preview isn't supported (b/18264647) 78d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // just skip the animation to avoid this case. 79d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } 80d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } 81d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd 82d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd /** 83d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * Implementation class for API level >= 18. 84d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd */ 85d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd @TargetApi(18) 86d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd private static class ViewExplodeAnimationJellyBeanMR2 { 87d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd private final View mViewToAnimate; 88d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd private final ViewGroup mContainer; 89d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd private final View mSnapshot; 90d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd private final Bitmap mViewBitmap; 91d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd private final int mDuration; 92d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd 93d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd public ViewExplodeAnimationJellyBeanMR2(final View viewToAnimate, final ViewGroup container, 94d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final boolean snapshotView, final int duration) { 95d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mViewToAnimate = viewToAnimate; 96d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mContainer = container; 97d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mDuration = duration; 98d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd if (snapshotView) { 99d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mViewBitmap = snapshotView(viewToAnimate); 100d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mSnapshot = new View(viewToAnimate.getContext()); 101d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } else { 102d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mSnapshot = null; 103d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mViewBitmap = null; 104d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } 105d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } 106d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd 107d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd public void startAnimation() { 108d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final Context context = mViewToAnimate.getContext(); 109d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final Resources resources = context.getResources(); 110d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final View decorView = ((Activity) context).getWindow().getDecorView(); 111d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final ViewOverlay viewOverlay = decorView.getOverlay(); 112d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd if (viewOverlay instanceof ViewGroupOverlay) { 113d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final ViewGroupOverlay overlay = (ViewGroupOverlay) viewOverlay; 114d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd 115d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Add a shadow layer to the overlay. 116d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final FrameLayout shadowContainerLayer = new FrameLayout(context); 117d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final Drawable oldBackground = mViewToAnimate.getBackground(); 118d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final Rect containerRect = UiUtils.getMeasuredBoundsOnScreen(mContainer); 119d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final Rect decorRect = UiUtils.getMeasuredBoundsOnScreen(decorView); 120d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Position the container rect relative to the decor rect since the decor rect 121d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // defines whether the view overlay will be positioned. 122d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd containerRect.offset(-decorRect.left, -decorRect.top); 123d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd shadowContainerLayer.setLeft(containerRect.left); 124d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd shadowContainerLayer.setTop(containerRect.top); 125d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd shadowContainerLayer.setBottom(containerRect.bottom); 126d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd shadowContainerLayer.setRight(containerRect.right); 127d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd shadowContainerLayer.setBackgroundColor(resources.getColor( 128d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd R.color.open_conversation_animation_background_shadow)); 129d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Per design request, temporarily clear out the background of the item content 130d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // to not show any ripple effects during animation. 131d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd if (!(oldBackground instanceof ColorDrawable)) { 132d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mViewToAnimate.setBackground(null); 133d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } 134d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd overlay.add(shadowContainerLayer); 135d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd 136d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Add a expand layer and position it with in the shadow background, so it can 137d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // be properly clipped to the container bounds during the animation. 138d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final View expandLayer = new View(context); 139d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final int elevation = resources.getDimensionPixelSize( 140d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd R.dimen.explode_animation_highlight_elevation); 141d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final Rect viewRect = UiUtils.getMeasuredBoundsOnScreen(mViewToAnimate); 142d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Frame viewRect from screen space to containerRect space. 143d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd viewRect.offset(-containerRect.left - decorRect.left, 144d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd -containerRect.top - decorRect.top); 145d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Since the expand layer expands at the same rate above and below, we need to 146d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // compute the expand scale using the bigger of the top/bottom distances. 147d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final int expandLayerHalfHeight = viewRect.height() / 2; 148d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final int topDist = viewRect.top; 149d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final int bottomDist = containerRect.height() - viewRect.bottom; 150d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final float scale = expandLayerHalfHeight == 0 ? 1 : 151d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd ((float) Math.max(topDist, bottomDist) + expandLayerHalfHeight) / 152d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd expandLayerHalfHeight; 153d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Position the expand layer initially to exactly match the animated item. 154d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd shadowContainerLayer.addView(expandLayer); 155d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd expandLayer.setLeft(viewRect.left); 156d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd expandLayer.setTop(viewRect.top); 157d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd expandLayer.setBottom(viewRect.bottom); 158d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd expandLayer.setRight(viewRect.right); 159d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd expandLayer.setBackgroundColor(resources.getColor( 160d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd R.color.conversation_background)); 161d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd ViewCompat.setElevation(expandLayer, elevation); 162d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd 163d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Conditionally stage the snapshot in the overlay. 164d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd if (mSnapshot != null) { 165d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd shadowContainerLayer.addView(mSnapshot); 166d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mSnapshot.setLeft(viewRect.left); 167d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mSnapshot.setTop(viewRect.top); 168d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mSnapshot.setBottom(viewRect.bottom); 169d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mSnapshot.setRight(viewRect.right); 170d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mSnapshot.setBackground(new BitmapDrawable(resources, mViewBitmap)); 171d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd ViewCompat.setElevation(mSnapshot, elevation); 172d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } 173d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd 174d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Apply a scale animation to scale to full screen. 175d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd expandLayer.animate().scaleY(scale) 176d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd .setDuration(mDuration) 177d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd .setInterpolator(UiUtils.EASE_IN_INTERPOLATOR) 178d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd .withEndAction(new Runnable() { 179d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd @Override 180d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd public void run() { 181d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Clean up the views added to overlay on animation finish. 182d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd overlay.remove(shadowContainerLayer); 183d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mViewToAnimate.setBackground(oldBackground); 184d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd if (mViewBitmap != null) { 185d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd mViewBitmap.recycle(); 186d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } 187d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } 188d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd }); 189d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } 190d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } 191d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } 192d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd 193d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd /** 194d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd * Take a snapshot of the given review, return a Bitmap object that's owned by the caller. 195d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd */ 196d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd static Bitmap snapshotView(final View view) { 197d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Save the content of the view into a bitmap. 198d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final Bitmap viewBitmap = Bitmap.createBitmap(view.getWidth(), 199d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd view.getHeight(), Bitmap.Config.ARGB_8888); 200d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // Strip the view of its background when taking a snapshot so that things like touch 201d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd // feedback don't get accidentally snapshotted. 202d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd final Drawable viewBackground = view.getBackground(); 203d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd ImageUtils.setBackgroundDrawableOnView(view, null); 204d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd view.draw(new Canvas(viewBitmap)); 205d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd ImageUtils.setBackgroundDrawableOnView(view, viewBackground); 206d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd return viewBitmap; 207d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd } 208d3b009ae55651f1e60950342468e3c37fdeb0796Mike Dodd} 209