Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / android / java / src / org / chromium / chrome / browser / banners / AppBannerView.java
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package org.chromium.chrome.browser.banners;
6
7 import android.animation.ObjectAnimator;
8 import android.app.Activity;
9 import android.app.PendingIntent;
10 import android.content.ActivityNotFoundException;
11 import android.content.ContentResolver;
12 import android.content.Context;
13 import android.content.Intent;
14 import android.content.IntentSender;
15 import android.content.pm.PackageManager;
16 import android.content.res.Configuration;
17 import android.content.res.Resources;
18 import android.graphics.Rect;
19 import android.os.Looper;
20 import android.util.AttributeSet;
21 import android.util.Log;
22 import android.view.LayoutInflater;
23 import android.view.MotionEvent;
24 import android.view.View;
25 import android.view.ViewConfiguration;
26 import android.view.ViewGroup;
27 import android.widget.Button;
28 import android.widget.ImageButton;
29 import android.widget.ImageView;
30 import android.widget.TextView;
31
32 import org.chromium.base.ApiCompatibilityUtils;
33 import org.chromium.chrome.R;
34 import org.chromium.content.browser.ContentViewCore;
35 import org.chromium.ui.base.LocalizationUtils;
36 import org.chromium.ui.base.WindowAndroid;
37 import org.chromium.ui.base.WindowAndroid.IntentCallback;
38
39 /**
40  * Lays out a banner for showing info about an app on the Play Store.
41  * The banner mimics the appearance of a Google Now card using a background Drawable with a shadow.
42  *
43  * PADDING CALCULATIONS
44  * The banner has three different types of padding that need to be accounted for:
45  * 1) The background Drawable of the banner looks like card with a drop shadow.  The Drawable
46  *    defines a padding around the card that solely encompasses the space occupied by the drop
47  *    shadow.
48  * 2) The card itself needs to have padding so that the widgets don't abut the borders of the card.
49  *    This is defined as mPaddingCard, and is equally applied to all four sides.
50  * 3) Controls other than the icon are further constrained by mPaddingControls, which applies only
51  *    to the bottom and end margins.
52  * See {@link #AppBannerView.onMeasure(int, int)} for details.
53  *
54  * MARGIN CALCULATIONS
55  * Margin calculations for the banner are complicated by the background Drawable's drop shadows,
56  * since the drop shadows are meant to be counted as being part of the margin.  To deal with this,
57  * the margins are calculated by deducting the background Drawable's padding from the margins
58  * defined by the XML files.
59  *
60  * EVEN MORE LAYOUT QUIRKS
61  * The layout of the banner, which includes its widget sizes, may change when the screen is rotated
62  * to account for less screen real estate.  This means that all of the View's widgets and cached
63  * dimensions must be rebuilt from scratch.
64  */
65 public class AppBannerView extends SwipableOverlayView
66         implements View.OnClickListener, InstallerDelegate.Observer, IntentCallback {
67     private static final String TAG = "AppBannerView";
68
69     /**
70      * Class that is alerted about things happening to the BannerView.
71      */
72     public static interface Observer {
73         /**
74          * Called when the banner is removed from the hierarchy.
75          * @param banner Banner being dismissed.
76          */
77         public void onBannerRemoved(AppBannerView banner);
78
79         /**
80          * Called when the user manually closes a banner.
81          * @param banner      Banner being blocked.
82          * @param url         URL of the page that requested the banner.
83          * @param packageName Name of the app's package.
84          */
85         public void onBannerBlocked(AppBannerView banner, String url, String packageName);
86
87         /**
88          * Called when the banner begins to be dismissed.
89          * @param banner      Banner being closed.
90          * @param dismissType Type of dismissal performed.
91          */
92         public void onBannerDismissEvent(AppBannerView banner, int dismissType);
93
94         /**
95          * Called when an install event has occurred.
96          */
97         public void onBannerInstallEvent(AppBannerView banner, int eventType);
98
99         /**
100          * Called when the banner needs to have an Activity started for a result.
101          * @param banner Banner firing the event.
102          * @param intent Intent to fire.
103          */
104         public boolean onFireIntent(AppBannerView banner, PendingIntent intent);
105     }
106
107     // Installation states.
108     private static final int INSTALL_STATE_NOT_INSTALLED = 0;
109     private static final int INSTALL_STATE_INSTALLING = 1;
110     private static final int INSTALL_STATE_INSTALLED = 2;
111
112     // XML layout for the BannerView.
113     private static final int BANNER_LAYOUT = R.layout.app_banner_view;
114
115     // True if the layout is in left-to-right layout mode (regular mode).
116     private final boolean mIsLayoutLTR;
117
118     // Class to alert about BannerView events.
119     private AppBannerView.Observer mObserver;
120
121     // Information about the package.  Shouldn't ever be null after calling {@link #initialize()}.
122     private AppData mAppData;
123
124     // Views comprising the app banner.
125     private ImageView mIconView;
126     private TextView mTitleView;
127     private Button mInstallButtonView;
128     private RatingView mRatingView;
129     private View mLogoView;
130     private View mBannerHighlightView;
131     private ImageButton mCloseButtonView;
132
133     // Dimension values.
134     private int mDefinedMaxWidth;
135     private int mPaddingCard;
136     private int mPaddingControls;
137     private int mMarginLeft;
138     private int mMarginRight;
139     private int mMarginBottom;
140     private int mTouchSlop;
141
142     // Highlight variables.
143     private boolean mIsBannerPressed;
144     private float mInitialXForHighlight;
145
146     // Initial padding values.
147     private final Rect mBackgroundDrawablePadding;
148
149     // Install tracking.
150     private boolean mWasInstallDialogShown;
151     private InstallerDelegate mInstallTask;
152     private int mInstallState;
153
154     /**
155      * Creates a BannerView and adds it to the given ContentViewCore.
156      * @param contentViewCore ContentViewCore to display the AppBannerView for.
157      * @param observer    Class that is alerted for AppBannerView events.
158      * @param data        Data about the app.
159      * @return            The created banner.
160      */
161     public static AppBannerView create(
162             ContentViewCore contentViewCore, Observer observer,AppData data) {
163         Context context = contentViewCore.getContext().getApplicationContext();
164         AppBannerView banner =
165                 (AppBannerView) LayoutInflater.from(context).inflate(BANNER_LAYOUT, null);
166         banner.initialize(observer, data);
167         banner.addToView(contentViewCore);
168         return banner;
169     }
170
171     /**
172      * Creates a BannerView from an XML layout.
173      */
174     public AppBannerView(Context context, AttributeSet attrs) {
175         super(context, attrs);
176         mIsLayoutLTR = !LocalizationUtils.isLayoutRtl();
177
178         // Store the background Drawable's padding.  The background used for banners is a 9-patch,
179         // which means that it already defines padding.  We need to take it into account when adding
180         // even more padding to the inside of it.
181         mBackgroundDrawablePadding = new Rect();
182         mBackgroundDrawablePadding.left = ApiCompatibilityUtils.getPaddingStart(this);
183         mBackgroundDrawablePadding.right = ApiCompatibilityUtils.getPaddingEnd(this);
184         mBackgroundDrawablePadding.top = getPaddingTop();
185         mBackgroundDrawablePadding.bottom = getPaddingBottom();
186
187         mInstallState = INSTALL_STATE_NOT_INSTALLED;
188     }
189
190     /**
191      * Initialize the banner with information about the package.
192      * @param observer Class to alert about changes to the banner.
193      * @param data     Information about the app being advertised.
194      */
195     private void initialize(Observer observer, AppData data) {
196         mObserver = observer;
197         mAppData = data;
198         initializeControls();
199     }
200
201     private void initializeControls() {
202         // Cache the banner dimensions, adjusting margins for drop shadows defined in the background
203         // Drawable.
204         Resources res = getResources();
205         mDefinedMaxWidth = res.getDimensionPixelSize(R.dimen.app_banner_max_width);
206         mPaddingCard = res.getDimensionPixelSize(R.dimen.app_banner_padding);
207         mPaddingControls = res.getDimensionPixelSize(R.dimen.app_banner_padding_controls);
208         mMarginLeft = res.getDimensionPixelSize(R.dimen.app_banner_margin_sides)
209                 - mBackgroundDrawablePadding.left;
210         mMarginRight = res.getDimensionPixelSize(R.dimen.app_banner_margin_sides)
211                 - mBackgroundDrawablePadding.right;
212         mMarginBottom = res.getDimensionPixelSize(R.dimen.app_banner_margin_bottom)
213                 - mBackgroundDrawablePadding.bottom;
214         if (getLayoutParams() != null) {
215             MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
216             params.leftMargin = mMarginLeft;
217             params.rightMargin = mMarginRight;
218             params.bottomMargin = mMarginBottom;
219         }
220
221         // Pull out all of the controls we are expecting.
222         mIconView = (ImageView) findViewById(R.id.app_icon);
223         mTitleView = (TextView) findViewById(R.id.app_title);
224         mInstallButtonView = (Button) findViewById(R.id.app_install_button);
225         mRatingView = (RatingView) findViewById(R.id.app_rating);
226         mLogoView = findViewById(R.id.store_logo);
227         mBannerHighlightView = findViewById(R.id.banner_highlight);
228         mCloseButtonView = (ImageButton) findViewById(R.id.close_button);
229
230         assert mIconView != null;
231         assert mTitleView != null;
232         assert mInstallButtonView != null;
233         assert mLogoView != null;
234         assert mRatingView != null;
235         assert mBannerHighlightView != null;
236         assert mCloseButtonView != null;
237
238         // Set up the buttons to fire an event.
239         mInstallButtonView.setOnClickListener(this);
240         mCloseButtonView.setOnClickListener(this);
241
242         // Configure the controls with the package information.
243         mTitleView.setText(mAppData.title());
244         mIconView.setImageDrawable(mAppData.icon());
245         mRatingView.initialize(mAppData.rating());
246         setAccessibilityInformation();
247
248         // Determine how much the user can drag sideways before their touch is considered a scroll.
249         mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
250
251         // Set up the install button.
252         updateButtonStatus();
253     }
254
255     /**
256      * Creates a succinct description about the app being advertised.
257      */
258     private void setAccessibilityInformation() {
259         String bannerText = getContext().getString(
260                 R.string.app_banner_view_accessibility, mAppData.title(), mAppData.rating());
261         setContentDescription(bannerText);
262     }
263
264     @Override
265     public void onClick(View view) {
266         if (mObserver == null) return;
267
268         // Only allow the button to be clicked when the banner's in a neutral position.
269         if (Math.abs(getTranslationX()) > ZERO_THRESHOLD
270                 || Math.abs(getTranslationY()) > ZERO_THRESHOLD) {
271             return;
272         }
273
274         if (view == mInstallButtonView) {
275             // Check that nothing happened in the background to change the install state of the app.
276             int previousState = mInstallState;
277             updateButtonStatus();
278             if (mInstallState != previousState) return;
279
280             // Ignore button clicks when the app is installing.
281             if (mInstallState == INSTALL_STATE_INSTALLING) return;
282
283             mInstallButtonView.setEnabled(false);
284
285             if (mInstallState == INSTALL_STATE_NOT_INSTALLED) {
286                 // The user initiated an install. Track it happening only once.
287                 if (!mWasInstallDialogShown) {
288                     mObserver.onBannerInstallEvent(this, AppBannerMetricsIds.INSTALL_TRIGGERED);
289                     mWasInstallDialogShown = true;
290                 }
291
292                 if (mObserver.onFireIntent(this, mAppData.installIntent())) {
293                     // Temporarily hide the banner.
294                     createVerticalSnapAnimation(false);
295                 } else {
296                     Log.e(TAG, "Failed to fire install intent.");
297                     dismiss(AppBannerMetricsIds.DISMISS_ERROR);
298                 }
299             } else if (mInstallState == INSTALL_STATE_INSTALLED) {
300                 // The app is installed. Open it.
301                 try {
302                     Intent appIntent = getAppLaunchIntent();
303                     if (appIntent != null) getContext().startActivity(appIntent);
304                 } catch (ActivityNotFoundException e) {
305                     Log.e(TAG, "Failed to find app package: " + mAppData.packageName());
306                 }
307
308                 dismiss(AppBannerMetricsIds.DISMISS_APP_OPEN);
309             }
310         } else if (view == mCloseButtonView) {
311             if (mObserver != null) {
312                 mObserver.onBannerBlocked(this, mAppData.siteUrl(), mAppData.packageName());
313             }
314
315             dismiss(AppBannerMetricsIds.DISMISS_CLOSE_BUTTON);
316         }
317     }
318
319     @Override
320     protected void onViewSwipedAway() {
321         if (mObserver == null) return;
322         mObserver.onBannerDismissEvent(this, AppBannerMetricsIds.DISMISS_BANNER_SWIPE);
323         mObserver.onBannerBlocked(this, mAppData.siteUrl(), mAppData.packageName());
324     }
325
326     @Override
327     protected void onViewClicked() {
328         // Send the user to the app's Play store page.
329         try {
330             IntentSender sender = mAppData.detailsIntent().getIntentSender();
331             getContext().startIntentSender(sender, new Intent(), 0, 0, 0);
332         } catch (IntentSender.SendIntentException e) {
333             Log.e(TAG, "Failed to launch details intent.");
334         }
335
336         dismiss(AppBannerMetricsIds.DISMISS_BANNER_CLICK);
337     }
338
339     @Override
340     protected void onViewPressed(MotionEvent event) {
341         // Highlight the banner when the user has held it for long enough and doesn't move.
342         mInitialXForHighlight = event.getRawX();
343         mIsBannerPressed = true;
344         mBannerHighlightView.setVisibility(View.VISIBLE);
345     }
346
347     @Override
348     public void onIntentCompleted(WindowAndroid window, int resultCode,
349             ContentResolver contentResolver, Intent data) {
350         if (isDismissed()) return;
351
352         createVerticalSnapAnimation(true);
353         if (resultCode == Activity.RESULT_OK) {
354             // The user chose to install the app. Watch the PackageManager to see when it finishes
355             // installing it.
356             mObserver.onBannerInstallEvent(this, AppBannerMetricsIds.INSTALL_STARTED);
357
358             PackageManager pm = getContext().getPackageManager();
359             mInstallTask =
360                     new InstallerDelegate(Looper.getMainLooper(), pm, this, mAppData.packageName());
361             mInstallTask.start();
362             mInstallState = INSTALL_STATE_INSTALLING;
363         }
364         updateButtonStatus();
365     }
366
367
368     @Override
369     public void onInstallFinished(InstallerDelegate monitor, boolean success) {
370         if (isDismissed() || mInstallTask != monitor) return;
371
372         if (success) {
373             // Let the user open the app from here.
374             mObserver.onBannerInstallEvent(this, AppBannerMetricsIds.INSTALL_COMPLETED);
375             mInstallState = INSTALL_STATE_INSTALLED;
376             updateButtonStatus();
377         } else {
378             dismiss(AppBannerMetricsIds.DISMISS_INSTALL_TIMEOUT);
379         }
380     }
381
382     @Override
383     protected ViewGroup.MarginLayoutParams createLayoutParams() {
384         // Define the margin around the entire banner that accounts for the drop shadow.
385         ViewGroup.MarginLayoutParams params = super.createLayoutParams();
386         params.setMargins(mMarginLeft, 0, mMarginRight, mMarginBottom);
387         return params;
388     }
389
390     /**
391      * Removes this View from its parent and alerts any observers of the dismissal.
392      * @return Whether or not the View was successfully dismissed.
393      */
394     @Override
395     boolean removeFromParent() {
396         if (super.removeFromParent()) {
397             mObserver.onBannerRemoved(this);
398             destroy();
399             return true;
400         }
401
402         return false;
403     }
404
405     /**
406      * Dismisses the banner.
407      * @param eventType Event that triggered the dismissal.  See {@link AppBannerMetricsIds}.
408      */
409     public void dismiss(int eventType) {
410         if (isDismissed() || mObserver == null) return;
411
412         dismiss(eventType == AppBannerMetricsIds.DISMISS_CLOSE_BUTTON);
413         mObserver.onBannerDismissEvent(this, eventType);
414     }
415
416     /**
417      * Destroys the Banner.
418      */
419     public void destroy() {
420         if (!isDismissed()) dismiss(AppBannerMetricsIds.DISMISS_ERROR);
421
422         if (mInstallTask != null) {
423             mInstallTask.cancel();
424             mInstallTask = null;
425         }
426     }
427
428     /**
429      * Updates the install button (install state, text, color, etc.).
430      */
431     void updateButtonStatus() {
432         if (mInstallButtonView == null) return;
433
434         // Determine if the saved install status of the app is out of date.
435         // It is not easily possible to detect if an app is in the process of being installed, so we
436         // can't properly transition to that state from here.
437         if (getAppLaunchIntent() == null) {
438             if (mInstallState == INSTALL_STATE_INSTALLED) {
439                 mInstallState = INSTALL_STATE_NOT_INSTALLED;
440             }
441         } else {
442             mInstallState = INSTALL_STATE_INSTALLED;
443         }
444
445         // Update what the button looks like.
446         Resources res = getResources();
447         int fgColor;
448         String text;
449         if (mInstallState == INSTALL_STATE_INSTALLED) {
450             ApiCompatibilityUtils.setBackgroundForView(mInstallButtonView,
451                     res.getDrawable(R.drawable.app_banner_button_open));
452             fgColor = res.getColor(R.color.app_banner_open_button_fg);
453             text = res.getString(R.string.app_banner_open);
454         } else {
455             ApiCompatibilityUtils.setBackgroundForView(mInstallButtonView,
456                     res.getDrawable(R.drawable.app_banner_button_install));
457             fgColor = res.getColor(R.color.app_banner_install_button_fg);
458             if (mInstallState == INSTALL_STATE_NOT_INSTALLED) {
459                 text = mAppData.installButtonText();
460                 mInstallButtonView.setContentDescription(
461                         getContext().getString(R.string.app_banner_install_accessibility, text));
462             } else {
463                 text = res.getString(R.string.app_banner_installing);
464             }
465         }
466
467         mInstallButtonView.setTextColor(fgColor);
468         mInstallButtonView.setText(text);
469         mInstallButtonView.setEnabled(mInstallState != INSTALL_STATE_INSTALLING);
470     }
471
472     /**
473      * Determine how big an icon needs to be for the Layout.
474      * @param context Context to grab resources from.
475      * @return        How big the icon is expected to be, in pixels.
476      */
477     static int getIconSize(Context context) {
478         return context.getResources().getDimensionPixelSize(R.dimen.app_banner_icon_size);
479     }
480
481     /**
482      * Passes all touch events through to the parent.
483      */
484     @Override
485     public boolean onTouchEvent(MotionEvent event) {
486         int action = event.getActionMasked();
487         if (mIsBannerPressed) {
488             // Mimic Google Now card behavior, where the card stops being highlighted if the user
489             // scrolls a bit to the side.
490             float xDifference = Math.abs(event.getRawX() - mInitialXForHighlight);
491             if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
492                     || (action == MotionEvent.ACTION_MOVE && xDifference > mTouchSlop)) {
493                 mIsBannerPressed = false;
494                 mBannerHighlightView.setVisibility(View.INVISIBLE);
495             }
496         }
497
498         return super.onTouchEvent(event);
499     }
500
501     /**
502      * Fade the banner back into view.
503      */
504     @Override
505     protected void onAttachedToWindow() {
506         super.onAttachedToWindow();
507         ObjectAnimator.ofFloat(this, "alpha", getAlpha(), 1.f).setDuration(
508                 MS_ANIMATION_DURATION).start();
509         setVisibility(VISIBLE);
510     }
511
512     /**
513      * Immediately hide the banner to avoid having them show up in snapshots.
514      */
515     @Override
516     protected void onDetachedFromWindow() {
517         super.onDetachedFromWindow();
518         setAlpha(0.0f);
519         setVisibility(INVISIBLE);
520     }
521
522     /**
523      * Watch for changes in the available screen height, which triggers a complete recreation of the
524      * banner widgets.  This is mainly due to the fact that the Nexus 7 has a smaller banner defined
525      * for its landscape versus its portrait layouts.
526      */
527     @Override
528     protected void onConfigurationChanged(Configuration config) {
529         super.onConfigurationChanged(config);
530
531         if (isDismissed()) return;
532
533         // If the card's maximum width hasn't changed, the individual views can't have, either.
534         int newDefinedWidth = getResources().getDimensionPixelSize(R.dimen.app_banner_max_width);
535         if (mDefinedMaxWidth == newDefinedWidth) return;
536
537         // Cannibalize another version of this layout to get Views using the new resources and
538         // sizes.
539         while (getChildCount() > 0) removeViewAt(0);
540         mIconView = null;
541         mTitleView = null;
542         mInstallButtonView = null;
543         mRatingView = null;
544         mLogoView = null;
545         mBannerHighlightView = null;
546
547         AppBannerView cannibalized =
548                 (AppBannerView) LayoutInflater.from(getContext()).inflate(BANNER_LAYOUT, null);
549         while (cannibalized.getChildCount() > 0) {
550             View child = cannibalized.getChildAt(0);
551             cannibalized.removeViewAt(0);
552             addView(child);
553         }
554         initializeControls();
555         requestLayout();
556     }
557
558     @Override
559     public void onWindowFocusChanged(boolean hasWindowFocus) {
560         if (hasWindowFocus) updateButtonStatus();
561     }
562
563     /**
564      * @return Intent to launch the app that is being promoted.
565      */
566     private Intent getAppLaunchIntent() {
567         String packageName = mAppData.packageName();
568         PackageManager packageManager = getContext().getPackageManager();
569         return packageManager.getLaunchIntentForPackage(packageName);
570     }
571
572     /**
573      * Measures the banner and its children Views for the given space.
574      *
575      * DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
576      * DPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPD
577      * DP......                               cPD
578      * DP...... TITLE----------------------- XcPD
579      * DP.ICON. *****                         cPD
580      * DP...... LOGO                    BUTTONcPD
581      * DP...... cccccccccccccccccccccccccccccccPD
582      * DPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPD
583      * DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
584      *
585      * The three paddings mentioned in the class Javadoc are denoted by:
586      * D) Drop shadow padding.
587      * P) Inner card padding.
588      * c) Control padding.
589      *
590      * Measurement for components of the banner are performed assuming that components are laid out
591      * inside of the banner's background as follows:
592      * 1) A maximum width is enforced on the banner to keep the whole thing on screen and keep it a
593      *    reasonable size.
594      * 2) The icon takes up the left side of the banner.
595      * 3) The install button occupies the bottom-right of the banner.
596      * 4) The Google Play logo occupies the space to the left of the button.
597      * 5) The rating is assigned space above the logo and below the title.
598      * 6) The close button (if visible) sits in the top right of the banner.
599      * 7) The title is assigned whatever space is left and sits on top of the tallest stack of
600      *    controls.
601      *
602      * See {@link #android.view.View.onMeasure(int, int)} for the parameters.
603      */
604     @Override
605     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
606         // Enforce a maximum width on the banner, which is defined as the smallest of:
607         // 1) The smallest width for the device (in either landscape or portrait mode).
608         // 2) The defined maximum width in the dimens.xml files.
609         // 3) The width passed in through the MeasureSpec.
610         Resources res = getResources();
611         float density = res.getDisplayMetrics().density;
612         int screenSmallestWidth = (int) (res.getConfiguration().smallestScreenWidthDp * density);
613         int specWidth = MeasureSpec.getSize(widthMeasureSpec);
614         int bannerWidth = Math.min(Math.min(specWidth, mDefinedMaxWidth), screenSmallestWidth);
615
616         // Track how much space is available inside the banner's card-shaped background Drawable.
617         // To calculate this, we need to account for both the padding of the background (which
618         // is occupied by the card's drop shadows) as well as the padding defined on the inside of
619         // the card.
620         int bgPaddingWidth = mBackgroundDrawablePadding.left + mBackgroundDrawablePadding.right;
621         int bgPaddingHeight = mBackgroundDrawablePadding.top + mBackgroundDrawablePadding.bottom;
622         final int maxControlWidth = bannerWidth - bgPaddingWidth - (mPaddingCard * 2);
623
624         // Control height is constrained to provide a reasonable aspect ratio.
625         // In practice, the only controls which can cause an issue are the title and the install
626         // button, since they have strings that can change size according to user preference.  The
627         // other controls are all defined to be a certain height.
628         int specHeight = MeasureSpec.getSize(heightMeasureSpec);
629         int reasonableHeight = maxControlWidth / 4;
630         int paddingHeight = bgPaddingHeight + (mPaddingCard * 2);
631         final int maxControlHeight = Math.min(specHeight, reasonableHeight) - paddingHeight;
632         final int maxStackedControlHeight = maxControlWidth / 3;
633
634         // Determine how big each component wants to be.  The icon is measured separately because
635         // it is not stacked with the other controls.
636         measureChildForSpace(mIconView, maxControlWidth, maxControlHeight);
637         for (int i = 0; i < getChildCount(); i++) {
638             if (getChildAt(i) != mIconView) {
639                 measureChildForSpace(getChildAt(i), maxControlWidth, maxStackedControlHeight);
640             }
641         }
642
643         // Determine how tall the banner needs to be to fit everything by calculating the combined
644         // height of the stacked controls.  There are three competing stacks to measure:
645         // 1) The icon.
646         // 2) The app title + control padding + star rating + store logo.
647         // 3) The app title + control padding + install button.
648         // The control padding is extra padding that applies only to the non-icon widgets.
649         // The close button does not get counted as part of a stack.
650         int iconStackHeight = getHeightWithMargins(mIconView);
651         int logoStackHeight = getHeightWithMargins(mTitleView) + mPaddingControls
652                 + getHeightWithMargins(mRatingView) + getHeightWithMargins(mLogoView);
653         int buttonStackHeight = getHeightWithMargins(mTitleView) + mPaddingControls
654                 + getHeightWithMargins(mInstallButtonView);
655         int biggestStackHeight =
656                 Math.max(iconStackHeight, Math.max(logoStackHeight, buttonStackHeight));
657
658         // The icon hugs the banner's starting edge, from the top of the banner to the bottom.
659         final int iconSize = biggestStackHeight;
660         measureChildForSpaceExactly(mIconView, iconSize, iconSize);
661
662         // The rest of the content is laid out to the right of the icon.
663         // Additional padding is defined for non-icon content on the end and bottom.
664         final int contentWidth =
665                 maxControlWidth - getWidthWithMargins(mIconView) - mPaddingControls;
666         final int contentHeight = biggestStackHeight - mPaddingControls;
667         measureChildForSpace(mLogoView, contentWidth, contentHeight);
668
669         // Restrict the button size to prevent overrunning the Google Play logo.
670         int remainingButtonWidth =
671                 maxControlWidth - getWidthWithMargins(mLogoView) - getWidthWithMargins(mIconView);
672         mInstallButtonView.setMaxWidth(remainingButtonWidth);
673         measureChildForSpace(mInstallButtonView, contentWidth, contentHeight);
674
675         // Measure the star rating, which sits below the title and above the logo.
676         final int ratingWidth = contentWidth;
677         final int ratingHeight = contentHeight - getHeightWithMargins(mLogoView);
678         measureChildForSpace(mRatingView, ratingWidth, ratingHeight);
679
680         // The close button sits to the right of the title and above the install button.
681         final int closeWidth = contentWidth;
682         final int closeHeight = contentHeight - getHeightWithMargins(mInstallButtonView);
683         measureChildForSpace(mCloseButtonView, closeWidth, closeHeight);
684
685         // The app title spans the top of the banner and sits on top of the other controls, and to
686         // the left of the close button. The computation for the width available to the title is
687         // complicated by how the button sits in the corner and absorbs the padding that would
688         // normally be there.
689         int biggerStack = Math.max(getHeightWithMargins(mInstallButtonView),
690                 getHeightWithMargins(mLogoView) + getHeightWithMargins(mRatingView));
691         final int titleWidth = contentWidth - getWidthWithMargins(mCloseButtonView) + mPaddingCard;
692         final int titleHeight = contentHeight - biggerStack;
693         measureChildForSpace(mTitleView, titleWidth, titleHeight);
694
695         // Set the measured dimensions for the banner.  The banner's height is defined by the
696         // tallest stack of components, the padding of the banner's card background, and the extra
697         // padding around the banner's components.
698         int bannerPadding = mBackgroundDrawablePadding.top + mBackgroundDrawablePadding.bottom
699                 + (mPaddingCard * 2);
700         int bannerHeight = biggestStackHeight + bannerPadding;
701         setMeasuredDimension(bannerWidth, bannerHeight);
702
703         // Make the banner highlight view be the exact same size as the banner's card background.
704         final int cardWidth = bannerWidth - bgPaddingWidth;
705         final int cardHeight = bannerHeight - bgPaddingHeight;
706         measureChildForSpaceExactly(mBannerHighlightView, cardWidth, cardHeight);
707     }
708
709     /**
710      * Lays out the controls according to the algorithm in {@link #onMeasure}.
711      * See {@link #android.view.View.onLayout(boolean, int, int, int, int)} for the parameters.
712      */
713     @Override
714     protected void onLayout(boolean changed, int l, int t, int r, int b) {
715         super.onLayout(changed, l, t, r, b);
716         int top = mBackgroundDrawablePadding.top;
717         int bottom = getMeasuredHeight() - mBackgroundDrawablePadding.bottom;
718         int start = mBackgroundDrawablePadding.left;
719         int end = getMeasuredWidth() - mBackgroundDrawablePadding.right;
720
721         // The highlight overlay covers the entire banner (minus drop shadow padding).
722         mBannerHighlightView.layout(start, top, end, bottom);
723
724         // Lay out the close button in the top-right corner.  Padding that would normally go to the
725         // card is applied to the close button so that it has a bigger touch target.
726         if (mCloseButtonView.getVisibility() == VISIBLE) {
727             int closeWidth = mCloseButtonView.getMeasuredWidth();
728             int closeTop =
729                     top + ((MarginLayoutParams) mCloseButtonView.getLayoutParams()).topMargin;
730             int closeBottom = closeTop + mCloseButtonView.getMeasuredHeight();
731             int closeRight = mIsLayoutLTR ? end : (getMeasuredWidth() - end + closeWidth);
732             int closeLeft = closeRight - closeWidth;
733             mCloseButtonView.layout(closeLeft, closeTop, closeRight, closeBottom);
734         }
735
736         // Apply the padding for the rest of the widgets.
737         top += mPaddingCard;
738         bottom -= mPaddingCard;
739         start += mPaddingCard;
740         end -= mPaddingCard;
741
742         // Lay out the icon.
743         int iconWidth = mIconView.getMeasuredWidth();
744         int iconLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - iconWidth);
745         mIconView.layout(iconLeft, top, iconLeft + iconWidth, top + mIconView.getMeasuredHeight());
746         start += getWidthWithMargins(mIconView);
747
748         // Factor in the additional padding, which is only tacked onto the end and bottom.
749         end -= mPaddingControls;
750         bottom -= mPaddingControls;
751
752         // Lay out the app title text.
753         int titleWidth = mTitleView.getMeasuredWidth();
754         int titleTop = top + ((MarginLayoutParams) mTitleView.getLayoutParams()).topMargin;
755         int titleBottom = titleTop + mTitleView.getMeasuredHeight();
756         int titleLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - titleWidth);
757         mTitleView.layout(titleLeft, titleTop, titleLeft + titleWidth, titleBottom);
758
759         // The mock shows the margin eating into the descender area of the TextView.
760         int textBaseline = mTitleView.getLineBounds(mTitleView.getLineCount() - 1, null);
761         top = titleTop + textBaseline
762                 + ((MarginLayoutParams) mTitleView.getLayoutParams()).bottomMargin;
763
764         // Lay out the app rating below the title.
765         int starWidth = mRatingView.getMeasuredWidth();
766         int starTop = top + ((MarginLayoutParams) mRatingView.getLayoutParams()).topMargin;
767         int starBottom = starTop + mRatingView.getMeasuredHeight();
768         int starLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - starWidth);
769         mRatingView.layout(starLeft, starTop, starLeft + starWidth, starBottom);
770
771         // Lay out the logo in the bottom-left.
772         int logoWidth = mLogoView.getMeasuredWidth();
773         int logoBottom = bottom - ((MarginLayoutParams) mLogoView.getLayoutParams()).bottomMargin;
774         int logoTop = logoBottom - mLogoView.getMeasuredHeight();
775         int logoLeft = mIsLayoutLTR ? start : (getMeasuredWidth() - start - logoWidth);
776         mLogoView.layout(logoLeft, logoTop, logoLeft + logoWidth, logoBottom);
777
778         // Lay out the install button in the bottom-right corner.
779         int buttonHeight = mInstallButtonView.getMeasuredHeight();
780         int buttonWidth = mInstallButtonView.getMeasuredWidth();
781         int buttonRight = mIsLayoutLTR ? end : (getMeasuredWidth() - end + buttonWidth);
782         int buttonLeft = buttonRight - buttonWidth;
783         mInstallButtonView.layout(buttonLeft, bottom - buttonHeight, buttonRight, bottom);
784     }
785
786     /**
787      * Measures a child for the given space, accounting for defined heights and margins.
788      * @param child           View to measure.
789      * @param availableWidth  Available width for the view.
790      * @param availableHeight Available height for the view.
791      */
792     private void measureChildForSpace(View child, int availableWidth, int availableHeight) {
793         // Handle margins.
794         availableWidth -= getMarginWidth(child);
795         availableHeight -= getMarginHeight(child);
796
797         // Account for any layout-defined dimensions for the view.
798         int childWidth = child.getLayoutParams().width;
799         int childHeight = child.getLayoutParams().height;
800         if (childWidth >= 0) availableWidth = Math.min(availableWidth, childWidth);
801         if (childHeight >= 0) availableHeight = Math.min(availableHeight, childHeight);
802
803         int widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST);
804         int heightSpec = MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.AT_MOST);
805         child.measure(widthSpec, heightSpec);
806     }
807
808     /**
809      * Forces a child to exactly occupy the given space.
810      * @param child           View to measure.
811      * @param availableWidth  Available width for the view.
812      * @param availableHeight Available height for the view.
813      */
814     private void measureChildForSpaceExactly(View child, int availableWidth, int availableHeight) {
815         int widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY);
816         int heightSpec = MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY);
817         child.measure(widthSpec, heightSpec);
818     }
819
820     /**
821      * Calculates how wide the margins are for the given View.
822      * @param view View to measure.
823      * @return     Measured width of the margins.
824      */
825     private static int getMarginWidth(View view) {
826         MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams();
827         return params.leftMargin + params.rightMargin;
828     }
829
830     /**
831      * Calculates how wide the given View has been measured to be, including its margins.
832      * @param view View to measure.
833      * @return     Measured width of the view plus its margins.
834      */
835     private static int getWidthWithMargins(View view) {
836         return view.getMeasuredWidth() + getMarginWidth(view);
837     }
838
839     /**
840      * Calculates how tall the margins are for the given View.
841      * @param view View to measure.
842      * @return     Measured height of the margins.
843      */
844     private static int getMarginHeight(View view) {
845         MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams();
846         return params.topMargin + params.bottomMargin;
847     }
848
849     /**
850      * Calculates how tall the given View has been measured to be, including its margins.
851      * @param view View to measure.
852      * @return     Measured height of the view plus its margins.
853      */
854     private static int getHeightWithMargins(View view) {
855         return view.getMeasuredHeight() + getMarginHeight(view);
856     }
857 }