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  */
16 package com.android.messaging.ui.conversationlist;
17 
18 import android.content.Context;
19 import android.content.res.Resources;
20 import android.database.Cursor;
21 import android.graphics.Color;
22 import android.graphics.Rect;
23 import android.graphics.Typeface;
24 import android.net.Uri;
25 import androidx.core.text.BidiFormatter;
26 import androidx.core.text.TextDirectionHeuristicsCompat;
27 import android.text.TextPaint;
28 import android.text.TextUtils;
29 import android.util.AttributeSet;
30 import android.view.View;
31 import android.view.View.OnClickListener;
32 import android.view.View.OnLayoutChangeListener;
33 import android.view.View.OnLongClickListener;
34 import android.view.ViewGroup;
35 import android.widget.FrameLayout;
36 import android.widget.ImageView;
37 import android.widget.TextView;
38 
39 import com.android.messaging.Factory;
40 import com.android.messaging.R;
41 import com.android.messaging.annotation.VisibleForAnimation;
42 import com.android.messaging.datamodel.MessagingContentProvider;
43 import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
44 import com.android.messaging.datamodel.data.ConversationListItemData;
45 import com.android.messaging.datamodel.data.MessageData;
46 import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
47 import com.android.messaging.sms.MmsUtils;
48 import com.android.messaging.ui.AsyncImageView;
49 import com.android.messaging.ui.AudioAttachmentView;
50 import com.android.messaging.ui.ContactIconView;
51 import com.android.messaging.ui.SnackBar;
52 import com.android.messaging.ui.SnackBarInteraction;
53 import com.android.messaging.util.Assert;
54 import com.android.messaging.util.ContentType;
55 import com.android.messaging.util.ImageUtils;
56 import com.android.messaging.util.OsUtil;
57 import com.android.messaging.util.PhoneUtils;
58 import com.android.messaging.util.Typefaces;
59 import com.android.messaging.util.UiUtils;
60 import com.android.messaging.util.UriUtil;
61 
62 import java.util.List;
63 
64 /**
65  * The view for a single entry in a conversation list.
66  */
67 public 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 {
isConversationSelected(final String conversationId)79         boolean isConversationSelected(final String conversationId);
onConversationClicked(final ConversationListItemData conversationListItemData, boolean isLongClick, final ConversationListItemView conversationView)80         void onConversationClicked(final ConversationListItemData conversationListItemData,
81                 boolean isLongClick, final ConversationListItemView conversationView);
isSwipeAnimatable()82         boolean isSwipeAnimatable();
getSnackBarInteractions()83         List<SnackBarInteraction> getSnackBarInteractions();
startFullScreenPhotoViewer(final Uri initialPhoto, final Rect initialPhotoBounds, final Uri photosUri)84         void startFullScreenPhotoViewer(final Uri initialPhoto, final Rect initialPhotoBounds,
85                 final Uri photosUri);
startFullScreenVideoViewer(final Uri videoUri)86         void startFullScreenVideoViewer(final Uri videoUri);
isSelectionMode()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 
ConversationListItemView(final Context context, final AttributeSet attrs)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
onFinishInflate()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
onLayoutChange(final View v, final int left, final int top, final int right, final int bottom, final int oldLeft, final int oldTop, final int oldRight, final int oldBottom)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 
setWorkProfileIcon()188     private void setWorkProfileIcon() {
189         mWorkProfileIconView.setVisibility(mData.isEnterprise() ? View.VISIBLE : View.GONE);
190     }
191 
setConversationName()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 
getPlusOneString()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 
getPlusNString()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 
setSubject()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 
setSnippet()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 
buildContentDescription(final Resources resources, final ConversationListItemData data, final TextPaint conversationNameViewPaint)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      */
bind(final Cursor cursor, final HostInterface hostInterface)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 
isSwipeAnimatable()507     public boolean isSwipeAnimatable() {
508         return mHostInterface.isSwipeAnimatable();
509     }
510 
511     @VisibleForAnimation
getSwipeTranslationX()512     public float getSwipeTranslationX() {
513         return mSwipeableContainer.getTranslationX();
514     }
515 
516     @VisibleForAnimation
setSwipeTranslationX(final float translationX)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 
onSwipeComplete()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 
setShortAndLongClickable(final boolean clickable)554     private void setShortAndLongClickable(final boolean clickable) {
555         setClickable(clickable);
556         setLongClickable(clickable);
557     }
558 
resetAnimatingState()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      */
setAnimating(final boolean animating)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 
isAnimating()592     public boolean isAnimating() {
593         return mAnimatingCount > 0;
594     }
595 
596     /**
597      * {@inheritDoc} from OnClickListener
598      */
599     @Override
onClick(final View v)600     public void onClick(final View v) {
601         processClick(v, false);
602     }
603 
604     /**
605      * {@inheritDoc} from OnLongClickListener
606      */
607     @Override
onLongClick(final View v)608     public boolean onLongClick(final View v) {
609         return processClick(v, true);
610     }
611 
processClick(final View v, final boolean isLongClick)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 
getSwipeableContent()623     public View getSwipeableContent() {
624         return mSwipeableContent;
625     }
626 
getContactIconView()627     public View getContactIconView() {
628         return mContactIconView;
629     }
630 
getSnippetText()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