1/*
2 * Copyright (C) 2015 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 */
16package com.android.messaging.ui.conversationlist;
17
18import android.content.Context;
19import android.content.res.Resources;
20import android.database.Cursor;
21import android.graphics.Color;
22import android.graphics.Rect;
23import android.graphics.Typeface;
24import android.net.Uri;
25import android.support.v4.text.BidiFormatter;
26import android.support.v4.text.TextDirectionHeuristicsCompat;
27import android.text.TextPaint;
28import android.text.TextUtils;
29import android.util.AttributeSet;
30import android.view.View;
31import android.view.View.OnClickListener;
32import android.view.View.OnLayoutChangeListener;
33import android.view.View.OnLongClickListener;
34import android.view.ViewGroup;
35import android.widget.FrameLayout;
36import android.widget.ImageView;
37import android.widget.TextView;
38
39import com.android.messaging.Factory;
40import com.android.messaging.R;
41import com.android.messaging.annotation.VisibleForAnimation;
42import com.android.messaging.datamodel.MessagingContentProvider;
43import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
44import com.android.messaging.datamodel.data.ConversationListItemData;
45import com.android.messaging.datamodel.data.MessageData;
46import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
47import com.android.messaging.sms.MmsUtils;
48import com.android.messaging.ui.AsyncImageView;
49import com.android.messaging.ui.AudioAttachmentView;
50import com.android.messaging.ui.ContactIconView;
51import com.android.messaging.ui.SnackBar;
52import com.android.messaging.ui.SnackBarInteraction;
53import com.android.messaging.util.Assert;
54import com.android.messaging.util.ContentType;
55import com.android.messaging.util.ImageUtils;
56import com.android.messaging.util.OsUtil;
57import com.android.messaging.util.PhoneUtils;
58import com.android.messaging.util.Typefaces;
59import com.android.messaging.util.UiUtils;
60import com.android.messaging.util.UriUtil;
61
62import java.util.List;
63
64/**
65 * The view for a single entry in a conversation list.
66 */
67public class ConversationListItemView extends FrameLayout implements OnClickListener,
68        OnLongClickListener, OnLayoutChangeListener {
69    static final int UNREAD_SNIPPET_LINE_COUNT = 3;
70    static final int NO_UNREAD_SNIPPET_LINE_COUNT = 1;
71    private int mListItemReadColor;
72    private int mListItemUnreadColor;
73    private Typeface mListItemReadTypeface;
74    private Typeface mListItemUnreadTypeface;
75    private static String sPlusOneString;
76    private static String sPlusNString;
77
78    public interface HostInterface {
79        boolean isConversationSelected(final String conversationId);
80        void onConversationClicked(final ConversationListItemData conversationListItemData,
81                boolean isLongClick, final ConversationListItemView conversationView);
82        boolean isSwipeAnimatable();
83        List<SnackBarInteraction> getSnackBarInteractions();
84        void startFullScreenPhotoViewer(final Uri initialPhoto, final Rect initialPhotoBounds,
85                final Uri photosUri);
86        void startFullScreenVideoViewer(final Uri videoUri);
87        boolean isSelectionMode();
88    }
89
90    private final OnClickListener fullScreenPreviewClickListener = new OnClickListener() {
91        @Override
92        public void onClick(final View v) {
93            final String previewType = mData.getShowDraft() ?
94                    mData.getDraftPreviewContentType() : mData.getPreviewContentType();
95            Assert.isTrue(ContentType.isImageType(previewType) ||
96                    ContentType.isVideoType(previewType));
97
98            final Uri previewUri = mData.getShowDraft() ?
99                    mData.getDraftPreviewUri() : mData.getPreviewUri();
100            if (ContentType.isImageType(previewType)) {
101                final Uri imagesUri = mData.getShowDraft() ?
102                        MessagingContentProvider.buildDraftImagesUri(mData.getConversationId()) :
103                        MessagingContentProvider
104                                .buildConversationImagesUri(mData.getConversationId());
105                final Rect previewImageBounds = UiUtils.getMeasuredBoundsOnScreen(v);
106                mHostInterface.startFullScreenPhotoViewer(
107                        previewUri, previewImageBounds, imagesUri);
108            } else {
109                mHostInterface.startFullScreenVideoViewer(previewUri);
110            }
111        }
112    };
113
114    private final ConversationListItemData mData;
115
116    private int mAnimatingCount;
117    private ViewGroup mSwipeableContainer;
118    private ViewGroup mCrossSwipeBackground;
119    private ViewGroup mSwipeableContent;
120    private TextView mConversationNameView;
121    private ImageView mWorkProfileIconView;
122    private TextView mSnippetTextView;
123    private TextView mSubjectTextView;
124    private TextView mTimestampTextView;
125    private ContactIconView mContactIconView;
126    private ImageView mContactCheckmarkView;
127    private ImageView mNotificationBellView;
128    private ImageView mFailedStatusIconView;
129    private ImageView mCrossSwipeArchiveLeftImageView;
130    private ImageView mCrossSwipeArchiveRightImageView;
131    private AsyncImageView mImagePreviewView;
132    private AudioAttachmentView mAudioAttachmentView;
133    private HostInterface mHostInterface;
134
135    public ConversationListItemView(final Context context, final AttributeSet attrs) {
136        super(context, attrs);
137        mData = new ConversationListItemData();
138        final Resources res = context.getResources();
139    }
140
141    @Override
142    protected void onFinishInflate() {
143        mSwipeableContainer = (ViewGroup) findViewById(R.id.swipeableContainer);
144        mCrossSwipeBackground = (ViewGroup) findViewById(R.id.crossSwipeBackground);
145        mSwipeableContent = (ViewGroup) findViewById(R.id.swipeableContent);
146        mConversationNameView = (TextView) findViewById(R.id.conversation_name);
147        mSnippetTextView = (TextView) findViewById(R.id.conversation_snippet);
148        mSubjectTextView = (TextView) findViewById(R.id.conversation_subject);
149        mWorkProfileIconView = (ImageView) findViewById(R.id.work_profile_icon);
150        mTimestampTextView = (TextView) findViewById(R.id.conversation_timestamp);
151        mContactIconView = (ContactIconView) findViewById(R.id.conversation_icon);
152        mContactCheckmarkView = (ImageView) findViewById(R.id.conversation_checkmark);
153        mNotificationBellView = (ImageView) findViewById(R.id.conversation_notification_bell);
154        mFailedStatusIconView = (ImageView) findViewById(R.id.conversation_failed_status_icon);
155        mCrossSwipeArchiveLeftImageView = (ImageView) findViewById(R.id.crossSwipeArchiveIconLeft);
156        mCrossSwipeArchiveRightImageView =
157                (ImageView) findViewById(R.id.crossSwipeArchiveIconRight);
158        mImagePreviewView = (AsyncImageView) findViewById(R.id.conversation_image_preview);
159        mAudioAttachmentView = (AudioAttachmentView) findViewById(R.id.audio_attachment_view);
160        mConversationNameView.addOnLayoutChangeListener(this);
161        mSnippetTextView.addOnLayoutChangeListener(this);
162
163        final Resources resources = getContext().getResources();
164        mListItemReadColor = resources.getColor(R.color.conversation_list_item_read);
165        mListItemUnreadColor = resources.getColor(R.color.conversation_list_item_unread);
166
167        mListItemReadTypeface = Typefaces.getRobotoNormal();
168        mListItemUnreadTypeface = Typefaces.getRobotoBold();
169
170        if (OsUtil.isAtLeastL()) {
171            setTransitionGroup(true);
172        }
173    }
174
175    @Override
176    public void onLayoutChange(final View v, final int left, final int top, final int right,
177            final int bottom, final int oldLeft, final int oldTop, final int oldRight,
178            final int oldBottom) {
179        if (v == mConversationNameView) {
180            setConversationName();
181        } else if (v == mSnippetTextView) {
182            setSnippet();
183        } else if (v == mSubjectTextView) {
184            setSubject();
185        }
186    }
187
188    private void setWorkProfileIcon() {
189        mWorkProfileIconView.setVisibility(mData.isEnterprise() ? View.VISIBLE : View.GONE);
190    }
191
192    private void setConversationName() {
193        if (mData.getIsRead() || mData.getShowDraft()) {
194            mConversationNameView.setTextColor(mListItemReadColor);
195            mConversationNameView.setTypeface(mListItemReadTypeface);
196        } else {
197            mConversationNameView.setTextColor(mListItemUnreadColor);
198            mConversationNameView.setTypeface(mListItemUnreadTypeface);
199        }
200
201        final String conversationName = mData.getName();
202
203        // For group conversations, ellipsize the group members that do not fit
204        final CharSequence ellipsizedName = UiUtils.commaEllipsize(
205                conversationName,
206                mConversationNameView.getPaint(),
207                mConversationNameView.getMeasuredWidth(),
208                getPlusOneString(),
209                getPlusNString());
210        // RTL : To format conversation name if it happens to be phone number.
211        final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
212        final String bidiFormattedName = bidiFormatter.unicodeWrap(
213                ellipsizedName.toString(),
214                TextDirectionHeuristicsCompat.LTR);
215
216        mConversationNameView.setText(bidiFormattedName);
217    }
218
219    private static String getPlusOneString() {
220        if (sPlusOneString == null) {
221            sPlusOneString =  Factory.get().getApplicationContext().getResources()
222                    .getString(R.string.plus_one);
223        }
224        return sPlusOneString;
225    }
226
227    private static String getPlusNString() {
228        if (sPlusNString == null) {
229            sPlusNString =  Factory.get().getApplicationContext().getResources()
230                    .getString(R.string.plus_n);
231        }
232        return sPlusNString;
233    }
234
235    private void setSubject() {
236        final String subjectText = mData.getShowDraft() ?
237                mData.getDraftSubject() :
238                    MmsUtils.cleanseMmsSubject(getContext().getResources(), mData.getSubject());
239        if (!TextUtils.isEmpty(subjectText)) {
240            final String subjectPrepend = getResources().getString(R.string.subject_label);
241            mSubjectTextView.setText(TextUtils.concat(subjectPrepend, subjectText));
242            mSubjectTextView.setVisibility(VISIBLE);
243        } else {
244            mSubjectTextView.setVisibility(GONE);
245        }
246    }
247
248    private void setSnippet() {
249        mSnippetTextView.setText(getSnippetText());
250    }
251
252    // Resource Ids of content descriptions prefixes for different message status.
253    private static final int [][][] sPrimaryContentDescriptions = {
254        // 1:1 conversation
255        {
256            // Incoming message
257            {
258                R.string.one_on_one_incoming_failed_message_prefix,
259                R.string.one_on_one_incoming_successful_message_prefix
260            },
261            // Outgoing message
262            {
263                R.string.one_on_one_outgoing_failed_message_prefix,
264                R.string.one_on_one_outgoing_successful_message_prefix,
265                R.string.one_on_one_outgoing_draft_message_prefix,
266                R.string.one_on_one_outgoing_sending_message_prefix,
267            }
268        },
269
270        // Group conversation
271        {
272            // Incoming message
273            {
274                R.string.group_incoming_failed_message_prefix,
275                R.string.group_incoming_successful_message_prefix,
276            },
277            // Outgoing message
278            {
279                R.string.group_outgoing_failed_message_prefix,
280                R.string.group_outgoing_successful_message_prefix,
281                R.string.group_outgoing_draft_message_prefix,
282                R.string.group_outgoing_sending_message_prefix,
283            }
284        }
285    };
286
287    // Resource Id of the secondary part of the content description for an edge case of a message
288    // which is in both draft status and failed status.
289    private static final int sSecondaryContentDescription =
290                                        R.string.failed_message_content_description;
291
292    // 1:1 versus group
293    private static final int CONV_TYPE_ONE_ON_ONE_INDEX = 0;
294    private static final int CONV_TYPE_ONE_GROUP_INDEX = 1;
295    // Direction
296    private static final int DIRECTION_INCOMING_INDEX = 0;
297    private static final int DIRECTION_OUTGOING_INDEX = 1;
298    // Message status
299    private static final int MESSAGE_STATUS_FAILED_INDEX = 0;
300    private static final int MESSAGE_STATUS_SUCCESSFUL_INDEX = 1;
301    private static final int MESSAGE_STATUS_DRAFT_INDEX = 2;
302    private static final int MESSAGE_STATUS_SENDING_INDEX = 3;
303
304    private static final int WIDTH_FOR_ACCESSIBLE_CONVERSATION_NAME = 600;
305
306    public static String buildContentDescription(final Resources resources,
307            final ConversationListItemData data, final TextPaint conversationNameViewPaint) {
308        int messageStatusIndex;
309        boolean outgoingSnippet = data.getIsMessageTypeOutgoing() || data.getShowDraft();
310        if (outgoingSnippet) {
311            if (data.getShowDraft()) {
312                messageStatusIndex = MESSAGE_STATUS_DRAFT_INDEX;
313            } else if (data.getIsSendRequested()) {
314                messageStatusIndex = MESSAGE_STATUS_SENDING_INDEX;
315            } else {
316                messageStatusIndex = data.getIsFailedStatus() ? MESSAGE_STATUS_FAILED_INDEX
317                        : MESSAGE_STATUS_SUCCESSFUL_INDEX;
318            }
319        } else {
320            messageStatusIndex = data.getIsFailedStatus() ? MESSAGE_STATUS_FAILED_INDEX
321                    : MESSAGE_STATUS_SUCCESSFUL_INDEX;
322        }
323
324        int resId = sPrimaryContentDescriptions
325                [data.getIsGroup() ? CONV_TYPE_ONE_GROUP_INDEX : CONV_TYPE_ONE_ON_ONE_INDEX]
326                [outgoingSnippet ? DIRECTION_OUTGOING_INDEX : DIRECTION_INCOMING_INDEX]
327                [messageStatusIndex];
328
329        final String snippetText = data.getShowDraft() ?
330                data.getDraftSnippetText() : data.getSnippetText();
331
332        final String conversationName = data.getName();
333        String senderOrConvName = outgoingSnippet ? conversationName : data.getSnippetSenderName();
334
335        String primaryContentDescription = resources.getString(resId, senderOrConvName,
336                snippetText == null ? "" : snippetText,
337                data.getFormattedTimestamp(),
338                // This is used only for incoming group messages
339                conversationName);
340        String contentDescription = primaryContentDescription;
341
342        // An edge case : for an outgoing message, it might be in both draft status and
343        // failed status.
344        if (outgoingSnippet && data.getShowDraft() && data.getIsFailedStatus()) {
345            StringBuilder contentDescriptionBuilder = new StringBuilder();
346            contentDescriptionBuilder.append(primaryContentDescription);
347
348            String secondaryContentDescription =
349                    resources.getString(sSecondaryContentDescription);
350            contentDescriptionBuilder.append(" ");
351            contentDescriptionBuilder.append(secondaryContentDescription);
352            contentDescription = contentDescriptionBuilder.toString();
353        }
354        return contentDescription;
355    }
356
357    /**
358     * Fills in the data associated with this view.
359     *
360     * @param cursor The cursor from a ConversationList that this view is in, pointing to its
361     * entry.
362     */
363    public void bind(final Cursor cursor, final HostInterface hostInterface) {
364        // Update our UI model
365        mHostInterface = hostInterface;
366        mData.bind(cursor);
367
368        resetAnimatingState();
369
370        mSwipeableContainer.setOnClickListener(this);
371        mSwipeableContainer.setOnLongClickListener(this);
372
373        final Resources resources = getContext().getResources();
374
375        int color;
376        final int maxLines;
377        final Typeface typeface;
378        final int typefaceStyle = mData.getShowDraft() ? Typeface.ITALIC : Typeface.NORMAL;
379        final String snippetText = getSnippetText();
380
381        if (mData.getIsRead() || mData.getShowDraft()) {
382            maxLines = TextUtils.isEmpty(snippetText) ? 0 : NO_UNREAD_SNIPPET_LINE_COUNT;
383            color = mListItemReadColor;
384            typeface = mListItemReadTypeface;
385        } else {
386            maxLines = TextUtils.isEmpty(snippetText) ? 0 : UNREAD_SNIPPET_LINE_COUNT;
387            color = mListItemUnreadColor;
388            typeface = mListItemUnreadTypeface;
389        }
390
391        mSnippetTextView.setMaxLines(maxLines);
392        mSnippetTextView.setTextColor(color);
393        mSnippetTextView.setTypeface(typeface, typefaceStyle);
394        mSubjectTextView.setTextColor(color);
395        mSubjectTextView.setTypeface(typeface, typefaceStyle);
396
397        setSnippet();
398        setConversationName();
399        setSubject();
400        setWorkProfileIcon();
401        setContentDescription(buildContentDescription(resources, mData,
402                mConversationNameView.getPaint()));
403
404        final boolean isDefaultSmsApp = PhoneUtils.getDefault().isDefaultSmsApp();
405        // don't show the error state unless we're the default sms app
406        if (mData.getIsFailedStatus() && isDefaultSmsApp) {
407            mTimestampTextView.setTextColor(resources.getColor(R.color.conversation_list_error));
408            mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle);
409            int failureMessageId = R.string.message_status_download_failed;
410            if (mData.getIsMessageTypeOutgoing()) {
411                failureMessageId = MmsUtils.mapRawStatusToErrorResourceId(mData.getMessageStatus(),
412                        mData.getMessageRawTelephonyStatus());
413            }
414            mTimestampTextView.setText(resources.getString(failureMessageId));
415        } else if (mData.getShowDraft()
416                || mData.getMessageStatus() == MessageData.BUGLE_STATUS_OUTGOING_DRAFT
417                // also check for unknown status which we get because sometimes the conversation
418                // row is left with a latest_message_id of a no longer existing message and
419                // therefore the join values come back as null (or in this case zero).
420                || mData.getMessageStatus() == MessageData.BUGLE_STATUS_UNKNOWN) {
421            mTimestampTextView.setTextColor(mListItemReadColor);
422            mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle);
423            mTimestampTextView.setText(resources.getString(
424                    R.string.conversation_list_item_view_draft_message));
425         } else {
426            mTimestampTextView.setTextColor(mListItemReadColor);
427            mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle);
428            final String formattedTimestamp = mData.getFormattedTimestamp();
429            if (mData.getIsSendRequested()) {
430                mTimestampTextView.setText(R.string.message_status_sending);
431            } else {
432                mTimestampTextView.setText(formattedTimestamp);
433            }
434        }
435
436        final boolean isSelected = mHostInterface.isConversationSelected(mData.getConversationId());
437        setSelected(isSelected);
438        Uri iconUri = null;
439        int contactIconVisibility = GONE;
440        int checkmarkVisiblity = GONE;
441        int failStatusVisiblity = GONE;
442        if (isSelected) {
443            checkmarkVisiblity = VISIBLE;
444        } else {
445            contactIconVisibility = VISIBLE;
446            // Only show the fail icon if it is not a group conversation.
447            // And also require that we be the default sms app.
448            if (mData.getIsFailedStatus() && !mData.getIsGroup() && isDefaultSmsApp) {
449                failStatusVisiblity = VISIBLE;
450            }
451        }
452        if (mData.getIcon() != null) {
453            iconUri = Uri.parse(mData.getIcon());
454        }
455        mContactIconView.setImageResourceUri(iconUri, mData.getParticipantContactId(),
456                mData.getParticipantLookupKey(), mData.getOtherParticipantNormalizedDestination());
457        mContactIconView.setVisibility(contactIconVisibility);
458        mContactIconView.setOnLongClickListener(this);
459        mContactIconView.setClickable(!mHostInterface.isSelectionMode());
460        mContactIconView.setLongClickable(!mHostInterface.isSelectionMode());
461
462        mContactCheckmarkView.setVisibility(checkmarkVisiblity);
463        mFailedStatusIconView.setVisibility(failStatusVisiblity);
464
465        final Uri previewUri = mData.getShowDraft() ?
466                mData.getDraftPreviewUri() : mData.getPreviewUri();
467        final String previewContentType = mData.getShowDraft() ?
468                mData.getDraftPreviewContentType() : mData.getPreviewContentType();
469        OnClickListener previewClickListener = null;
470        Uri previewImageUri = null;
471        int previewImageVisibility = GONE;
472        int audioPreviewVisiblity = GONE;
473        if (previewUri != null && !TextUtils.isEmpty(previewContentType)) {
474            if (ContentType.isAudioType(previewContentType)) {
475                boolean incoming = !(mData.getShowDraft() || mData.getIsMessageTypeOutgoing());
476                mAudioAttachmentView.bind(previewUri, incoming, false);
477                audioPreviewVisiblity = VISIBLE;
478            } else if (ContentType.isVideoType(previewContentType)) {
479                previewImageUri = UriUtil.getUriForResourceId(
480                        getContext(), R.drawable.ic_preview_play);
481                previewClickListener = fullScreenPreviewClickListener;
482                previewImageVisibility = VISIBLE;
483            } else if (ContentType.isImageType(previewContentType)) {
484                previewImageUri = previewUri;
485                previewClickListener = fullScreenPreviewClickListener;
486                previewImageVisibility = VISIBLE;
487            }
488        }
489
490        final int imageSize = resources.getDimensionPixelSize(
491                R.dimen.conversation_list_image_preview_size);
492        mImagePreviewView.setImageResourceId(
493                new UriImageRequestDescriptor(previewImageUri, imageSize, imageSize,
494                        true /* allowCompression */, false /* isStatic */, false /*cropToCircle*/,
495                        ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
496                        ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */));
497        mImagePreviewView.setOnLongClickListener(this);
498        mImagePreviewView.setVisibility(previewImageVisibility);
499        mImagePreviewView.setOnClickListener(previewClickListener);
500        mAudioAttachmentView.setOnLongClickListener(this);
501        mAudioAttachmentView.setVisibility(audioPreviewVisiblity);
502
503        final int notificationBellVisiblity = mData.getNotificationEnabled() ? GONE : VISIBLE;
504        mNotificationBellView.setVisibility(notificationBellVisiblity);
505    }
506
507    public boolean isSwipeAnimatable() {
508        return mHostInterface.isSwipeAnimatable();
509    }
510
511    @VisibleForAnimation
512    public float getSwipeTranslationX() {
513        return mSwipeableContainer.getTranslationX();
514    }
515
516    @VisibleForAnimation
517    public void setSwipeTranslationX(final float translationX) {
518        mSwipeableContainer.setTranslationX(translationX);
519        if (translationX == 0) {
520            mCrossSwipeBackground.setVisibility(View.GONE);
521            mCrossSwipeArchiveLeftImageView.setVisibility(GONE);
522            mCrossSwipeArchiveRightImageView.setVisibility(GONE);
523
524            mSwipeableContainer.setBackgroundColor(Color.TRANSPARENT);
525        } else {
526            mCrossSwipeBackground.setVisibility(View.VISIBLE);
527            if (translationX > 0) {
528                mCrossSwipeArchiveLeftImageView.setVisibility(VISIBLE);
529                mCrossSwipeArchiveRightImageView.setVisibility(GONE);
530            } else {
531                mCrossSwipeArchiveLeftImageView.setVisibility(GONE);
532                mCrossSwipeArchiveRightImageView.setVisibility(VISIBLE);
533            }
534            mSwipeableContainer.setBackgroundResource(R.drawable.swipe_shadow_drag);
535        }
536    }
537
538    public void onSwipeComplete() {
539        final String conversationId = mData.getConversationId();
540        UpdateConversationArchiveStatusAction.archiveConversation(conversationId);
541
542        final Runnable undoRunnable = new Runnable() {
543            @Override
544            public void run() {
545                UpdateConversationArchiveStatusAction.unarchiveConversation(conversationId);
546            }
547        };
548        final String message = getResources().getString(R.string.archived_toast_message, 1);
549        UiUtils.showSnackBar(getContext(), getRootView(), message, undoRunnable,
550                SnackBar.Action.SNACK_BAR_UNDO,
551                mHostInterface.getSnackBarInteractions());
552    }
553
554    private void setShortAndLongClickable(final boolean clickable) {
555        setClickable(clickable);
556        setLongClickable(clickable);
557    }
558
559    private void resetAnimatingState() {
560        mAnimatingCount = 0;
561        setShortAndLongClickable(true);
562        setSwipeTranslationX(0);
563    }
564
565    /**
566     * Notifies this view that it is undergoing animation. This view should disable its click
567     * targets.
568     *
569     * The animating counter is used to reset the swipe controller when the counter becomes 0. A
570     * positive counter also makes the view not clickable.
571     */
572    public final void setAnimating(final boolean animating) {
573        final int oldAnimatingCount = mAnimatingCount;
574        if (animating) {
575            mAnimatingCount++;
576        } else {
577            mAnimatingCount--;
578            if (mAnimatingCount < 0) {
579                mAnimatingCount = 0;
580            }
581        }
582
583        if (mAnimatingCount == 0) {
584            // New count is 0. All animations ended.
585            setShortAndLongClickable(true);
586        } else if (oldAnimatingCount == 0) {
587            // New count is > 0. Waiting for some animations to end.
588            setShortAndLongClickable(false);
589        }
590    }
591
592    public boolean isAnimating() {
593        return mAnimatingCount > 0;
594    }
595
596    /**
597     * {@inheritDoc} from OnClickListener
598     */
599    @Override
600    public void onClick(final View v) {
601        processClick(v, false);
602    }
603
604    /**
605     * {@inheritDoc} from OnLongClickListener
606     */
607    @Override
608    public boolean onLongClick(final View v) {
609        return processClick(v, true);
610    }
611
612    private boolean processClick(final View v, final boolean isLongClick) {
613        Assert.isTrue(v == mSwipeableContainer || v == mContactIconView || v == mImagePreviewView);
614        Assert.notNull(mData.getName());
615
616        if (mHostInterface != null) {
617            mHostInterface.onConversationClicked(mData, isLongClick, this);
618            return true;
619        }
620        return false;
621    }
622
623    public View getSwipeableContent() {
624        return mSwipeableContent;
625    }
626
627    public View getContactIconView() {
628        return mContactIconView;
629    }
630
631    private String getSnippetText() {
632        String snippetText = mData.getShowDraft() ?
633                mData.getDraftSnippetText() : mData.getSnippetText();
634        final String previewContentType = mData.getShowDraft() ?
635                mData.getDraftPreviewContentType() : mData.getPreviewContentType();
636        if (TextUtils.isEmpty(snippetText)) {
637            Resources resources = getResources();
638            // Use the attachment type as a snippet so the preview doesn't look odd
639            if (ContentType.isAudioType(previewContentType)) {
640                snippetText = resources.getString(R.string.conversation_list_snippet_audio_clip);
641            } else if (ContentType.isImageType(previewContentType)) {
642                snippetText = resources.getString(R.string.conversation_list_snippet_picture);
643            } else if (ContentType.isVideoType(previewContentType)) {
644                snippetText = resources.getString(R.string.conversation_list_snippet_video);
645            } else if (ContentType.isVCardType(previewContentType)) {
646                snippetText = resources.getString(R.string.conversation_list_snippet_vcard);
647            }
648        }
649        return snippetText;
650    }
651}
652