Home | History | Annotate | Download | only in sample
      1 /*
      2  * Copyright (C) 2016 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.car.cluster.sample;
     18 
     19 import static com.android.car.cluster.sample.BitmapUtils.generateMediaIcon;
     20 
     21 import android.animation.Animator;
     22 import android.animation.AnimatorListenerAdapter;
     23 import android.content.Context;
     24 import android.graphics.Bitmap;
     25 import android.graphics.BitmapFactory;
     26 import android.os.Handler;
     27 import android.os.Looper;
     28 import android.util.AttributeSet;
     29 import android.util.Log;
     30 import android.view.View;
     31 import android.view.ViewAnimationUtils;
     32 import android.view.ViewGroup;
     33 import android.view.animation.AccelerateInterpolator;
     34 import android.view.animation.AlphaAnimation;
     35 import android.view.animation.Animation;
     36 import android.view.animation.Animation.AnimationListener;
     37 import android.view.animation.DecelerateInterpolator;
     38 import android.view.animation.Interpolator;
     39 import android.view.animation.Transformation;
     40 import android.view.animation.TranslateAnimation;
     41 import android.widget.FrameLayout;
     42 import android.widget.RelativeLayout;
     43 
     44 import com.android.car.cluster.sample.cards.CallCard;
     45 import com.android.car.cluster.sample.cards.CallCard.CallStatus;
     46 import com.android.car.cluster.sample.cards.CardView;
     47 import com.android.car.cluster.sample.cards.CardView.CardType;
     48 import com.android.car.cluster.sample.cards.MessageCard;
     49 import com.android.car.cluster.sample.cards.MediaCard;
     50 import com.android.car.cluster.sample.cards.NavCard;
     51 import com.android.car.cluster.sample.cards.WeatherCard;
     52 
     53 import java.util.PriorityQueue;
     54 
     55 /**
     56  * Class that represents cluster view. It is responsible of ranking cards and play animations
     57  * during card transitions.
     58  */
     59 public class ClusterView extends FrameLayout implements CardView.PriorityChangedListener{
     60     private static final String TAG = DebugUtil.getTag(ClusterView.class);
     61 
     62     private CardPanel mCardPanel;
     63 
     64     private final Handler mHandler = new Handler(Looper.getMainLooper());
     65     private final PriorityQueue<CardView> mQueue = new PriorityQueue<>(8);
     66 
     67     public ClusterView(Context context) {
     68         this(context, null);
     69     }
     70 
     71     public ClusterView(Context context, AttributeSet attrs) {
     72         super(context, attrs);
     73 
     74         inflate(getContext(), R.layout.cluster_view, this);
     75         mCardPanel = (CardPanel) findViewById(R.id.card_panel);
     76     }
     77 
     78     private <E extends CardView> E createCard(@CardType int cardType) {
     79         CardView card;
     80         switch (cardType)
     81         {
     82             case CardType.WEATHER:
     83                 card = new WeatherCard(getContext(), this /* priority listener */);
     84                 break;
     85             case CardType.MEDIA:
     86                 card = new MediaCard(getContext(), this /* priority listener */);
     87                 break;
     88             case CardType.PHONE_CALL:
     89                 card = new CallCard(getContext(), this /* priority listener */);
     90                 break;
     91             case CardType.NAV:
     92                 card = new NavCard(getContext(), this /* priority listener */);
     93                 break;
     94             case CardType.HANGOUT:
     95                 card = new MessageCard(getContext(), this /* priority listener */);
     96                 break;
     97             default:
     98                 card = new CardView(getContext(), cardType, this /* priority listener */);
     99         }
    100         RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
    101                 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    102         card.setLayoutParams(params);
    103         return (E) card;
    104     }
    105 
    106     public void handleIncomingCall(Bitmap contactImage, String contactName) {
    107         if (contactImage == null) {
    108             contactImage = BitmapFactory.decodeResource(getResources(),
    109                     R.drawable.unknown_contact_480);
    110         }
    111         CardView card = createIncomingCallCard(contactImage, contactName);
    112         enqueueCard(card);
    113     }
    114 
    115     public void handleDialingCall(Bitmap contactImage, String contactName) {
    116         if (contactImage == null) {
    117             contactImage = BitmapFactory.decodeResource(getResources(),
    118                     R.drawable.unknown_contact_480);
    119         }
    120         CardView card = createDialingCallCard(contactImage, contactName);
    121         enqueueCard(card);
    122     }
    123 
    124     public void handleUpdateContactName(String contactName) {
    125         CallCard card = getCallCard();
    126         if (card != null) {
    127             card.setContactName(contactName);
    128         }
    129     }
    130 
    131     public void handleUpdateContactImage(Bitmap image) {
    132         CallCard card = getCallCard();
    133         if (card != null) {
    134             updateContactImage(card, image);
    135         }
    136     }
    137 
    138     public void handleHangoutMessage(Bitmap contactImage, String contactName) {
    139         if (getCardOrNull(MessageCard.class) != null) {
    140             return; // Deduplicate.
    141         }
    142 
    143         if (contactImage == null) {
    144             contactImage = BitmapFactory.decodeResource(getResources(),
    145                     R.drawable.unknown_contact_480);
    146         }
    147         MessageCard card = createCard(CardType.HANGOUT);
    148         card.setContactName(contactName);
    149 
    150         int c = getResources().getColor(R.color.hangout_background, null);
    151         card.setBackgroundColor(c);
    152         card.setLeftIcon(BitmapUtils.circleCropBitmap(contactImage));
    153 
    154         enqueueCard(card);
    155     }
    156 
    157     public void handleCallConnected(long connectedTimestamp) {
    158         if (DebugUtil.DEBUG) {
    159             Log.d(TAG, "handleCallConnected, connectedTimestamp: " + connectedTimestamp);
    160         }
    161         CallCard card = getCallCard();
    162         if (card != null) {
    163             if (DebugUtil.DEBUG) {
    164                 Log.d(TAG, "handleCallConnected, call status: " + card.getCallStatus());
    165             }
    166 
    167             if (card.getCallStatus() == CallStatus.INCOMING_OR_DIALING) {
    168                 card.animateCallConnected(connectedTimestamp);
    169             }
    170         }
    171     }
    172 
    173     public void handleCallDisconnected() {
    174         final CallCard callCard = getCallCard();
    175         if (callCard != null) {
    176             if (DebugUtil.DEBUG) {
    177                 Log.d(TAG, "handleCallDisconnected, callCard status: " + callCard.getCallStatus());
    178             }
    179 
    180             if (callCard == getCurrentCard()
    181                     && callCard.getCallStatus() == CallStatus.ACTIVE) {
    182                 callCard.animateCallDisconnected();
    183             } else {
    184                 removeCard(callCard);
    185             }
    186         }
    187     }
    188 
    189     public void runDelayed(long delay, final Runnable task) {
    190         mHandler.postDelayed(task, delay);
    191     }
    192 
    193     public MediaCard createMediaCard(Bitmap albumCover, String title, String subtitle,
    194             int appColor) {
    195         MediaCard card = createCard(CardType.MEDIA);
    196         int iconSize = card.getIconSize();
    197 
    198         if (albumCover != null) {
    199             Bitmap albumIcon = BitmapUtils.scaleBitmap(albumCover, iconSize, iconSize);
    200             albumIcon = BitmapUtils.circleCropBitmap(albumIcon);
    201             card.setLeftIcon(albumIcon);
    202 
    203             Bitmap backgroundImage = BitmapUtils.scaleBitmapColors(albumCover,
    204                     getResources().getColor(R.color.media_background_dark, null),
    205                     getResources().getColor(R.color.media_background, null));
    206 
    207             backgroundImage = BitmapUtils.scaleBitmap(backgroundImage,
    208                     (int) (getResources().getDimension(R.dimen.card_width)),
    209                     (int) getResources().getDimension(R.dimen.card_height));
    210 
    211             int c = getResources().getColor(R.color.phone_background, null);
    212             card.setBackground(backgroundImage, c);
    213         }
    214 
    215         Bitmap mediaApp = generateMediaIcon(iconSize,
    216                 appColor,
    217                 getResources().getColor(R.color.media_icon_foreground, null));
    218         card.setRightIcon(mediaApp);
    219         card.setProgressColor(appColor);
    220         card.setTitle(title);
    221         card.setSubtitle(subtitle);
    222         return card;
    223     }
    224 
    225     public CardView getCurrentCard() {
    226         return (CardView) mCardPanel.getTopVisibleChild();
    227     }
    228 
    229     public CardView createWeatherCard() {
    230         CardView card;
    231         card = createCard(CardType.WEATHER);
    232         card.setBackgroundColor(getResources().getColor(R.color.weather_blue_sky, null));
    233         return card;
    234     }
    235 
    236     public CallCard createIncomingCallCard(Bitmap contactImage, String contactName) {
    237         CallCard card = createCard(CardType.PHONE_CALL);
    238         updateContactImage(card, contactImage);
    239         card.setContactName(contactName);
    240         card.setStatusLabel(getContext().getString(R.string.incoming_call));
    241         return card;
    242     }
    243 
    244     public CallCard createDialingCallCard(Bitmap contactImage, String contactName) {
    245         CallCard card = createCard(CardType.PHONE_CALL);
    246 
    247         updateContactImage(card, contactImage);
    248         card.setContactName(contactName);
    249         card.setStatusLabel(getContext().getString(R.string.dialing));
    250         return card;
    251     }
    252 
    253     public NavCard createNavCard() {
    254         return createCard(CardType.NAV);
    255     }
    256 
    257     private void updateContactImage(CardView card, Bitmap contactImage) {
    258         int iconSize = (int)getResources().getDimension(R.dimen.card_icon_size);
    259         Bitmap contactImageCircle = BitmapUtils.circleCropBitmap(
    260                 BitmapUtils.scaleBitmap(contactImage, iconSize, iconSize));
    261         card.setLeftIcon(contactImageCircle);
    262 
    263         contactImage = BitmapUtils.scaleBitmapColors(contactImage,
    264                 getResources().getColor(R.color.phone_background_dark, null),
    265                 getResources().getColor(R.color.phone_background, null));
    266 
    267         contactImage = BitmapUtils.scaleBitmap(contactImage,
    268                 (int) getResources().getDimension(R.dimen.card_width),
    269                 (int) getResources().getDimension(R.dimen.card_height));
    270 
    271         int c = getResources().getColor(R.color.phone_background, null);
    272         card.setBackground(contactImage, c);
    273     }
    274 
    275     public boolean cardExists(CardView card) {
    276         return mCardPanel.childViewExists(card);
    277     }
    278 
    279     public void removeCard(final CardView card) {
    280         if (DebugUtil.DEBUG) {
    281             Log.d(TAG, "removeCard, card: " + card);
    282         }
    283         final CardView currentlyShownCard = getCurrentCard();
    284         if (currentlyShownCard == card) {
    285             // Card is on the screen, play nice animation and then remove it.
    286             mQueue.remove(card);
    287             mCardPanel.markViewToBeRemoved(card);
    288             CardView cardToShow = mQueue.peek();
    289             if (cardToShow != null) {
    290                 Animation animation = cardToShow.getAnimation();
    291                 if (DebugUtil.DEBUG) {
    292                     Log.d(TAG, "card to show: " + cardToShow + ", animation: " + animation);
    293                 }
    294                 if (animation != null) {
    295                     cardToShow.getAnimation().cancel();
    296                 }
    297 
    298                 cardToShow.setVisibility(VISIBLE);
    299                 mCardPanel.moveChildBehindTheTop(cardToShow);
    300             }
    301             playUnrevealAnimation(card, new Runnable() {
    302                 @Override
    303                 public void run() {
    304                     removeCardInternal(card);
    305                 }
    306             });
    307         } else {
    308             // Card is not on the screen, just remove it.
    309             if (DebugUtil.DEBUG) {
    310                 Log.d(TAG, "removeCard, card is not on the screen, remove it immediately");
    311                 removeCardInternal(card);
    312             }
    313         }
    314     }
    315 
    316     public void enqueueCard(final CardView card) {
    317         if (DebugUtil.DEBUG) {
    318             Log.d(TAG, "enqueueCard, card: " + card);
    319         }
    320         final CardView currentCard = getCurrentCard();
    321         boolean cardIsOnTheScreen = card == currentCard;
    322 
    323         boolean cardExisted = mQueue.remove(card);
    324         mQueue.offer(card);
    325 
    326         CardView activeCard = mQueue.peek();
    327         boolean shouldDisplayCard = activeCard == card;
    328 
    329         if (DebugUtil.DEBUG) {
    330             Log.d(TAG, "enqueueCard, card: " + card + ", onScreen: "
    331                     + cardIsOnTheScreen + ", cardExisted: " + cardExisted + ", shouldDisplayCard: "
    332                     + shouldDisplayCard + ", activeCard: " + activeCard
    333                     + ", currentCard: " + currentCard);
    334         }
    335 
    336         if (cardIsOnTheScreen) {
    337             if (!shouldDisplayCard) {
    338                 // Card priority was decreased, but it still active. Need to reverse reveal
    339                 // animation and show underlying card.
    340                 showCardWithFadeoutAnimation(activeCard);
    341             }
    342         } else {
    343             // Card is not on the screen right now.
    344             if (cardExisted) {
    345                 if (shouldDisplayCard) {
    346                     // Card was created in the past and is in the queue, need to show
    347                     // this card using unreveal animation.
    348                     showCardWithUnrevealAnimation(card);
    349                 }
    350             } else {
    351                 if (shouldDisplayCard) {
    352                     // Card doesn't exist, but we want to show it.
    353                     showCardWithRevealAnimation(card);
    354                 } else {
    355                     // We want to add the card to the panel, but do not want to show it.
    356                     if (DebugUtil.DEBUG) {
    357                         Log.d(TAG, "Adding hidden card");
    358                     }
    359                     card.setVisibility(GONE);
    360                     mCardPanel.addView(card, 0);
    361                 }
    362             }
    363         }
    364 
    365         removeInvisibleDuplicatedCard(card);  // Remove invisible cards with the same card type.
    366 
    367         dumpCardsToLog();
    368     }
    369 
    370     private void showCardWithRevealAnimation(final CardView cardToShow) {
    371         if (DebugUtil.DEBUG) {
    372             Log.d(TAG, "showCardWithRevealAnimation, card: " + cardToShow);
    373         }
    374 
    375         removeInvisibleDuplicatedCard(cardToShow);
    376 
    377         CardView currentCard = getCurrentCard();
    378         mCardPanel.addView(cardToShow);
    379         playRevealAnimation(cardToShow, currentCard, new RemoveOrHideCard(this, currentCard));
    380     }
    381 
    382     private void removeInvisibleDuplicatedCard(CardView card) {
    383         Log.d(TAG, "removeInvisibleDuplicatedCard, card: " + card);
    384         // Remove cards that has the same card type and not visible on the screen.
    385         for (int i = mCardPanel.getChildCount() - 1; i >= 0; i--) {
    386             CardView child = (CardView) mCardPanel.getChildAt(i);
    387             if (child.getCardType() == card.getCardType()
    388                     && child != card
    389                     && child.getVisibility() != VISIBLE) {
    390                 Log.d(TAG, "removeInvisibleDuplicatedCard, found dup: " + child);
    391                 removeCardInternal(child);
    392             }
    393         }
    394     }
    395 
    396     private void showCardWithFadeoutAnimation(final CardView cardToShow) {
    397         if (DebugUtil.DEBUG) {
    398             Log.d(TAG, "showCardWithFadeoutAnimation, card: " + cardToShow);
    399         }
    400         // Place card behind the top card, it will become visible once fade out animation for the
    401         // top card starts to play.
    402         mCardPanel.moveChildBehindTheTop(cardToShow);
    403         cardToShow.setVisibility(VISIBLE);
    404 
    405         // Hide top card with animation.
    406         playFadeOutAndSlideOutAnimation((CardView) mCardPanel.getTopVisibleChild());
    407     }
    408 
    409     private void showCardWithUnrevealAnimation(CardView cardToShow) {
    410         if (DebugUtil.DEBUG) {
    411             Log.d(TAG, "showCardWithUnrevealAnimation, card: " + cardToShow);
    412         }
    413         final CardView currentCard = (CardView) mCardPanel.getTopVisibleChild();
    414         // Card was created in the past and is in the queue, need to show reverse reveal
    415         // animation to unreveal this card.
    416         mCardPanel.moveChildBehindTheTop(cardToShow);
    417         cardToShow.setVisibility(VISIBLE);
    418         playUnrevealAnimation(currentCard, new RemoveOrHideCard(this, currentCard));
    419     }
    420 
    421     private static class RemoveOrHideCard implements Runnable {
    422         private final CardView mCard;
    423 
    424         private final ClusterView mClusterView;
    425 
    426         RemoveOrHideCard(ClusterView clusterView, CardView card) {
    427             mCard = card;
    428             mClusterView = clusterView;
    429         }
    430 
    431         @Override
    432         public void run() {
    433             if (DebugUtil.DEBUG) {
    434                 Log.d(TAG, "RemoveOrHideCard: " + mCard);
    435             }
    436 
    437             mClusterView.removeInvisibleDuplicatedCard(mCard);
    438 
    439             if (mCard.isGarbage()) {
    440                 if (DebugUtil.DEBUG) {
    441                     Log.d(TAG, "RemoveOrHideCard, card has garbage priority");
    442                 }
    443                 mClusterView.removeCardInternal(mCard);
    444             } else {
    445                 if (DebugUtil.DEBUG) {
    446                     Log.d(TAG, "RemoveOrHideCard, hiding card: " + mCard);
    447                 }
    448                 mCard.setVisibility(GONE);
    449                 mCard.setAlpha(1);  // Restore alpha after fade-out animation, it's gone anyway.
    450                 mClusterView.dumpCardsToLog();
    451             }
    452         }
    453     }
    454 
    455     private void removeCardInternal(CardView card) {
    456         if (DebugUtil.DEBUG) {
    457             Log.d(TAG, "removeCardInternal, card: " + card);
    458         }
    459         mCardPanel.removeView(card);
    460         mQueue.remove(card);
    461 
    462         dumpCardsToLog();
    463     }
    464 
    465     @Override
    466     public void onPriorityChanged(CardView card, int priority) {
    467         if (DebugUtil.DEBUG) {
    468             Log.d(TAG, "onPriorityChanged, card: " + card + ", priority: " + priority);
    469         }
    470         if (cardExists(card)) {
    471             enqueueCard(card);
    472         }
    473     }
    474 
    475     private void playRevealAnimation(final CardView cardToShow, final CardView currentCard,
    476             final Runnable oldCardDissapearedAction) {
    477 
    478         if (currentCard == cardToShow) {
    479             return;
    480         }
    481 
    482         if (currentCard != null) {
    483             playAlphaAnimation(currentCard,
    484                     0,    // Target alpha
    485                     400,  // Duration
    486                     new DecelerateInterpolator(0.5f),
    487                     oldCardDissapearedAction);
    488         }
    489 
    490         cardToShow.addOnLayoutChangeListener(new OnLayoutChangeListener() {
    491             @Override
    492             public void onLayoutChange(View v, int left, int top, int right, int bottom,
    493                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
    494                 cardToShow.removeOnLayoutChangeListener(this);  // Just need it once.
    495 
    496                 createRevealAnimator(cardToShow, new DecelerateInterpolator(1f), true)
    497                         .start();
    498             }
    499         });
    500         cardToShow.onPlayRevealAnimation();
    501     }
    502 
    503     private void playAlphaAnimation(final CardView card, float targetAlpha, long duration,
    504             Interpolator interpolator, final Runnable endAction) {
    505         Animation animation = new AlphaAnimation(card.getAlpha(), targetAlpha);
    506         animation.setDuration(duration * DebugUtil.ANIMATION_FACTOR);
    507         animation.setInterpolator(interpolator);
    508 
    509         animation.setAnimationListener(new AnimationListener() {
    510             @Override
    511             public void onAnimationStart(Animation animation) { }
    512 
    513             @Override
    514             public void onAnimationEnd(Animation animation) {
    515                 // For some reason, cancelled animation hasEnded() == true here.
    516                 // Check for start time instead.
    517                 if (endAction != null && animation.getStartTime() != Long.MIN_VALUE) {
    518                     endAction.run();
    519                 }
    520             }
    521 
    522             @Override
    523             public void onAnimationRepeat(Animation animation) { }
    524         });
    525         card.setAnimation(animation);
    526         animation.start();
    527     }
    528 
    529     private static boolean isAnimationCancelled(Animation animation) {
    530         return (animation.getStartTime() == Long.MIN_VALUE) || (!animation.hasEnded());
    531     }
    532 
    533     private void playFadeOutAndSlideOutAnimation(final CardView card) {
    534         Animation animation = new TranslateAnimation(0, card.getWidth(), 0, 0) {
    535             private final float mFromAlpha = card.getAlpha();
    536             private final float mToAlpha = 0;
    537 
    538             @Override
    539             protected void applyTransformation(float interpolatedTime, Transformation t) {
    540                 super.applyTransformation(interpolatedTime, t);
    541 
    542                 final float alpha = mFromAlpha;
    543                 t.setAlpha(alpha + ((mToAlpha - alpha) * interpolatedTime));
    544             }
    545         };
    546         animation.setDuration(600 * DebugUtil.ANIMATION_FACTOR);
    547         animation.setAnimationListener(new AnimationListener() {
    548             @Override
    549             public void onAnimationStart(Animation animation) { }
    550 
    551             @Override
    552             public void onAnimationEnd(Animation animation) {
    553                 if (DebugUtil.DEBUG) {
    554                     Log.d(TAG, "playFadeOutAndSlideOutAnimation, onAnimationEnd " + animation
    555                             + ", startTime: " + animation.getStartTime());
    556                 }
    557                 if (!isAnimationCancelled(animation)) {
    558                     new RemoveOrHideCard(ClusterView.this, card)
    559                             .run();
    560                 }
    561                 // Reset X position.
    562                 card.setTranslationX(0);
    563             }
    564 
    565             @Override
    566             public void onAnimationRepeat(Animation animation) { }
    567         });
    568         card.setAnimation(animation);
    569         animation.start();
    570     }
    571 
    572     /** Hides given card and reveals underlying card */
    573     private void playUnrevealAnimation(final CardView card, final Runnable unrevealCompleteAction) {
    574         if (DebugUtil.DEBUG) {
    575             Log.d(TAG, "playUnrevealAnimation, card: " + card);
    576         }
    577 
    578         final Animator anim = createRevealAnimator(card,
    579                 new AccelerateInterpolator(2f), false /* hide */);
    580 
    581         anim.addListener(new AnimatorListenerAdapter() {
    582             private boolean cancelled = false;
    583 
    584             @Override
    585             public void onAnimationCancel(Animator animation) {
    586                 if (DebugUtil.DEBUG) {
    587                     Log.d(TAG, "onAnimationCancel, animation: " + animation);
    588                 }
    589                 cancelled = true;
    590             }
    591 
    592             @Override
    593             public void onAnimationEnd(Animator animation) {
    594                 if (cancelled) {
    595                     return;
    596                 }
    597 
    598                 if (DebugUtil.DEBUG) {
    599                     Log.d(TAG, "onAnimationEnd, animation: " + animation);
    600                 }
    601                 unrevealCompleteAction.run();
    602             }
    603         });
    604 
    605         anim.start();
    606         card.onPlayUnrevealAnimation();
    607     }
    608 
    609     private Animator createRevealAnimator(CardView card, Interpolator interpolator, boolean show) {
    610         int cardWidth = (int) getResources().getDimension(R.dimen.card_width);
    611         int radius = (int) (cardWidth * 1.2f);
    612         int centerY = (int) (getResources().getDimension(R.dimen.card_height) / 2);
    613 
    614         Animator anim = ViewAnimationUtils.createCircularReveal(card, radius, centerY,
    615                 show ? 0 : radius /* start radius */,
    616                 show ? radius : 0 /* end radius */);
    617 
    618         anim.setInterpolator(interpolator);
    619         anim.setDuration(600 * DebugUtil.ANIMATION_FACTOR);
    620         return anim;
    621     }
    622 
    623     public <E> E getCardOrNull(Class<E> clazz) {
    624         return mCardPanel.getChildOrNull(clazz);
    625     }
    626 
    627     public <E> E getVisibleCardOrNull(Class<E> clazz) {
    628         return mCardPanel.getVisibleChildOrNull(clazz);
    629     }
    630 
    631     private CallCard getCallCard() {
    632         return getCardOrNull(CallCard.class);
    633     }
    634 
    635     private void dumpCardsToLog() {
    636         if (DebugUtil.DEBUG) {
    637             Log.d(TAG, "Cards in layout: " + mCardPanel.getChildCount() + ", cards in queue: "
    638                     + mQueue.size());
    639             for (int i = 0; i < mCardPanel.getChildCount(); i++) {
    640                 Log.d(TAG, "child: " + mCardPanel.getChildAt(i));
    641             }
    642         }
    643     }
    644 }
    645