summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/ui/conversation
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/messaging/ui/conversation')
-rw-r--r--src/com/android/messaging/ui/conversation/ComposeMessageView.java962
-rw-r--r--src/com/android/messaging/ui/conversation/ConversationActivity.java379
-rw-r--r--src/com/android/messaging/ui/conversation/ConversationActivityUiState.java306
-rw-r--r--src/com/android/messaging/ui/conversation/ConversationFastScroller.java489
-rw-r--r--src/com/android/messaging/ui/conversation/ConversationFragment.java1662
-rw-r--r--src/com/android/messaging/ui/conversation/ConversationInput.java103
-rw-r--r--src/com/android/messaging/ui/conversation/ConversationInputManager.java550
-rw-r--r--src/com/android/messaging/ui/conversation/ConversationMessageAdapter.java117
-rw-r--r--src/com/android/messaging/ui/conversation/ConversationMessageBubbleView.java132
-rw-r--r--src/com/android/messaging/ui/conversation/ConversationMessageView.java1206
-rw-r--r--src/com/android/messaging/ui/conversation/ConversationSimSelector.java128
-rw-r--r--src/com/android/messaging/ui/conversation/EnterSelfPhoneNumberDialog.java92
-rw-r--r--src/com/android/messaging/ui/conversation/LaunchConversationActivity.java134
-rw-r--r--src/com/android/messaging/ui/conversation/MessageBubbleBackground.java47
-rw-r--r--src/com/android/messaging/ui/conversation/MessageDetailsDialog.java381
-rw-r--r--src/com/android/messaging/ui/conversation/SimIconView.java51
-rw-r--r--src/com/android/messaging/ui/conversation/SimSelectorItemView.java90
-rw-r--r--src/com/android/messaging/ui/conversation/SimSelectorView.java169
18 files changed, 0 insertions, 6998 deletions
diff --git a/src/com/android/messaging/ui/conversation/ComposeMessageView.java b/src/com/android/messaging/ui/conversation/ComposeMessageView.java
deleted file mode 100644
index 17f8f74..0000000
--- a/src/com/android/messaging/ui/conversation/ComposeMessageView.java
+++ /dev/null
@@ -1,962 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.messaging.ui.conversation;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.net.Uri;
-import android.os.Bundle;
-import android.support.v7.app.ActionBar;
-import android.text.Editable;
-import android.text.Html;
-import android.text.InputFilter;
-import android.text.InputFilter.LengthFilter;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.util.AttributeSet;
-import android.view.ContextThemeWrapper;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.inputmethod.EditorInfo;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.messaging.Factory;
-import com.android.messaging.R;
-import com.android.messaging.datamodel.binding.Binding;
-import com.android.messaging.datamodel.binding.BindingBase;
-import com.android.messaging.datamodel.binding.ImmutableBindingRef;
-import com.android.messaging.datamodel.data.ConversationData;
-import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
-import com.android.messaging.datamodel.data.ConversationData.SimpleConversationDataListener;
-import com.android.messaging.datamodel.data.DraftMessageData;
-import com.android.messaging.datamodel.data.DraftMessageData.CheckDraftForSendTask;
-import com.android.messaging.datamodel.data.DraftMessageData.CheckDraftTaskCallback;
-import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener;
-import com.android.messaging.datamodel.data.MessageData;
-import com.android.messaging.datamodel.data.MessagePartData;
-import com.android.messaging.datamodel.data.ParticipantData;
-import com.android.messaging.datamodel.data.PendingAttachmentData;
-import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
-import com.android.messaging.sms.MmsConfig;
-import com.android.messaging.ui.AttachmentPreview;
-import com.android.messaging.ui.BugleActionBarActivity;
-import com.android.messaging.ui.PlainTextEditText;
-import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputSink;
-import com.android.messaging.util.AccessibilityUtil;
-import com.android.messaging.util.Assert;
-import com.android.messaging.util.AvatarUriUtil;
-import com.android.messaging.util.BuglePrefs;
-import com.android.messaging.util.ContentType;
-import com.android.messaging.util.LogUtil;
-import com.android.messaging.util.MediaUtil;
-import com.android.messaging.util.OsUtil;
-import com.android.messaging.util.UiUtils;
-
-import java.util.Collection;
-import java.util.List;
-
-/**
- * This view contains the UI required to generate and send messages.
- */
-public class ComposeMessageView extends LinearLayout
- implements TextView.OnEditorActionListener, DraftMessageDataListener, TextWatcher,
- ConversationInputSink {
-
- public interface IComposeMessageViewHost extends
- DraftMessageData.DraftMessageSubscriptionDataProvider {
- void sendMessage(MessageData message);
- void onComposeEditTextFocused();
- void onAttachmentsCleared();
- void onAttachmentsChanged(final boolean haveAttachments);
- void displayPhoto(Uri photoUri, Rect imageBounds, boolean isDraft);
- void promptForSelfPhoneNumber();
- boolean isReadyForAction();
- void warnOfMissingActionConditions(final boolean sending,
- final Runnable commandToRunAfterActionConditionResolved);
- void warnOfExceedingMessageLimit(final boolean showAttachmentChooser,
- boolean tooManyVideos);
- void notifyOfAttachmentLoadFailed();
- void showAttachmentChooser();
- boolean shouldShowSubjectEditor();
- boolean shouldHideAttachmentsWhenSimSelectorShown();
- Uri getSelfSendButtonIconUri();
- int overrideCounterColor();
- int getAttachmentsClearedFlags();
- }
-
- public static final int CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN = 10;
-
- // There is no draft and there is no need for the SIM selector
- private static final int SEND_WIDGET_MODE_SELF_AVATAR = 1;
- // There is no draft but we need to show the SIM selector
- private static final int SEND_WIDGET_MODE_SIM_SELECTOR = 2;
- // There is a draft
- private static final int SEND_WIDGET_MODE_SEND_BUTTON = 3;
-
- private PlainTextEditText mComposeEditText;
- private PlainTextEditText mComposeSubjectText;
- private TextView mCharCounter;
- private TextView mMmsIndicator;
- private SimIconView mSelfSendIcon;
- private ImageButton mSendButton;
- private View mSubjectView;
- private ImageButton mDeleteSubjectButton;
- private AttachmentPreview mAttachmentPreview;
- private ImageButton mAttachMediaButton;
-
- private final Binding<DraftMessageData> mBinding;
- private IComposeMessageViewHost mHost;
- private final Context mOriginalContext;
- private int mSendWidgetMode = SEND_WIDGET_MODE_SELF_AVATAR;
-
- // Shared data model object binding from the conversation.
- private ImmutableBindingRef<ConversationData> mConversationDataModel;
-
- // Centrally manages all the mutual exclusive UI components accepting user input, i.e.
- // media picker, IME keyboard and SIM selector.
- private ConversationInputManager mInputManager;
-
- private final ConversationDataListener mDataListener = new SimpleConversationDataListener() {
- @Override
- public void onConversationMetadataUpdated(ConversationData data) {
- mConversationDataModel.ensureBound(data);
- updateVisualsOnDraftChanged();
- }
-
- @Override
- public void onConversationParticipantDataLoaded(ConversationData data) {
- mConversationDataModel.ensureBound(data);
- updateVisualsOnDraftChanged();
- }
-
- @Override
- public void onSubscriptionListDataLoaded(ConversationData data) {
- mConversationDataModel.ensureBound(data);
- updateOnSelfSubscriptionChange();
- updateVisualsOnDraftChanged();
- }
- };
-
- public ComposeMessageView(final Context context, final AttributeSet attrs) {
- super(new ContextThemeWrapper(context, R.style.ColorAccentBlueOverrideStyle), attrs);
- mOriginalContext = context;
- mBinding = BindingBase.createBinding(this);
- }
-
- /**
- * Host calls this to bind view to DraftMessageData object
- */
- public void bind(final DraftMessageData data, final IComposeMessageViewHost host) {
- mHost = host;
- mBinding.bind(data);
- data.addListener(this);
- data.setSubscriptionDataProvider(host);
-
- final int counterColor = mHost.overrideCounterColor();
- if (counterColor != -1) {
- mCharCounter.setTextColor(counterColor);
- }
- }
-
- /**
- * Host calls this to unbind view
- */
- public void unbind() {
- mBinding.unbind();
- mHost = null;
- mInputManager.onDetach();
- }
-
- @Override
- protected void onFinishInflate() {
- mComposeEditText = (PlainTextEditText) findViewById(
- R.id.compose_message_text);
- mComposeEditText.setOnEditorActionListener(this);
- mComposeEditText.addTextChangedListener(this);
- mComposeEditText.setOnFocusChangeListener(new OnFocusChangeListener() {
- @Override
- public void onFocusChange(final View v, final boolean hasFocus) {
- if (v == mComposeEditText && hasFocus) {
- mHost.onComposeEditTextFocused();
- }
- }
- });
- mComposeEditText.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View arg0) {
- if (mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
- hideSimSelector();
- }
- }
- });
-
- // onFinishInflate() is called before self is loaded from db. We set the default text
- // limit here, and apply the real limit later in updateOnSelfSubscriptionChange().
- mComposeEditText.setFilters(new InputFilter[] {
- new LengthFilter(MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID)
- .getMaxTextLimit()) });
-
- mSelfSendIcon = (SimIconView) findViewById(R.id.self_send_icon);
- mSelfSendIcon.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- boolean shown = mInputManager.toggleSimSelector(true /* animate */,
- getSelfSubscriptionListEntry());
- hideAttachmentsWhenShowingSims(shown);
- }
- });
- mSelfSendIcon.setOnLongClickListener(new OnLongClickListener() {
- @Override
- public boolean onLongClick(final View v) {
- if (mHost.shouldShowSubjectEditor()) {
- showSubjectEditor();
- } else {
- boolean shown = mInputManager.toggleSimSelector(true /* animate */,
- getSelfSubscriptionListEntry());
- hideAttachmentsWhenShowingSims(shown);
- }
- return true;
- }
- });
-
- mComposeSubjectText = (PlainTextEditText) findViewById(
- R.id.compose_subject_text);
- // We need the listener to change the avatar to the send button when the user starts
- // typing a subject without a message.
- mComposeSubjectText.addTextChangedListener(this);
- // onFinishInflate() is called before self is loaded from db. We set the default text
- // limit here, and apply the real limit later in updateOnSelfSubscriptionChange().
- mComposeSubjectText.setFilters(new InputFilter[] {
- new LengthFilter(MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID)
- .getMaxSubjectLength())});
-
- mDeleteSubjectButton = (ImageButton) findViewById(R.id.delete_subject_button);
- mDeleteSubjectButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(final View clickView) {
- hideSubjectEditor();
- mComposeSubjectText.setText(null);
- mBinding.getData().setMessageSubject(null);
- }
- });
-
- mSubjectView = findViewById(R.id.subject_view);
-
- mSendButton = (ImageButton) findViewById(R.id.send_message_button);
- mSendButton.setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(final View clickView) {
- sendMessageInternal(true /* checkMessageSize */);
- }
- });
- mSendButton.setOnLongClickListener(new OnLongClickListener() {
- @Override
- public boolean onLongClick(final View arg0) {
- boolean shown = mInputManager.toggleSimSelector(true /* animate */,
- getSelfSubscriptionListEntry());
- hideAttachmentsWhenShowingSims(shown);
- if (mHost.shouldShowSubjectEditor()) {
- showSubjectEditor();
- }
- return true;
- }
- });
- mSendButton.setAccessibilityDelegate(new AccessibilityDelegate() {
- @Override
- public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
- super.onPopulateAccessibilityEvent(host, event);
- // When the send button is long clicked, we want TalkBack to announce the real
- // action (select SIM or edit subject), as opposed to "long press send button."
- if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_LONG_CLICKED) {
- event.getText().clear();
- event.getText().add(getResources()
- .getText(shouldShowSimSelector(mConversationDataModel.getData()) ?
- R.string.send_button_long_click_description_with_sim_selector :
- R.string.send_button_long_click_description_no_sim_selector));
- // Make this an announcement so TalkBack will read our custom message.
- event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
- }
- }
- });
-
- mAttachMediaButton =
- (ImageButton) findViewById(R.id.attach_media_button);
- mAttachMediaButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(final View clickView) {
- // Showing the media picker is treated as starting to compose the message.
- mInputManager.showHideMediaPicker(true /* show */, true /* animate */);
- }
- });
-
- mAttachmentPreview = (AttachmentPreview) findViewById(R.id.attachment_draft_view);
- mAttachmentPreview.setComposeMessageView(this);
-
- mCharCounter = (TextView) findViewById(R.id.char_counter);
- mMmsIndicator = (TextView) findViewById(R.id.mms_indicator);
- }
-
- private void hideAttachmentsWhenShowingSims(final boolean simPickerVisible) {
- if (!mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
- return;
- }
- final boolean haveAttachments = mBinding.getData().hasAttachments();
- if (simPickerVisible && haveAttachments) {
- mHost.onAttachmentsChanged(false);
- mAttachmentPreview.hideAttachmentPreview();
- } else {
- mHost.onAttachmentsChanged(haveAttachments);
- mAttachmentPreview.onAttachmentsChanged(mBinding.getData());
- }
- }
-
- public void setInputManager(final ConversationInputManager inputManager) {
- mInputManager = inputManager;
- }
-
- public void setConversationDataModel(final ImmutableBindingRef<ConversationData> refDataModel) {
- mConversationDataModel = refDataModel;
- mConversationDataModel.getData().addConversationDataListener(mDataListener);
- }
-
- ImmutableBindingRef<DraftMessageData> getDraftDataModel() {
- return BindingBase.createBindingReference(mBinding);
- }
-
- // returns true if it actually shows the subject editor and false if already showing
- private boolean showSubjectEditor() {
- // show the subject editor
- if (mSubjectView.getVisibility() == View.GONE) {
- mSubjectView.setVisibility(View.VISIBLE);
- mSubjectView.requestFocus();
- return true;
- }
- return false;
- }
-
- private void hideSubjectEditor() {
- mSubjectView.setVisibility(View.GONE);
- mComposeEditText.requestFocus();
- }
-
- /**
- * {@inheritDoc} from TextView.OnEditorActionListener
- */
- @Override // TextView.OnEditorActionListener.onEditorAction
- public boolean onEditorAction(final TextView view, final int actionId, final KeyEvent event) {
- if (actionId == EditorInfo.IME_ACTION_SEND) {
- sendMessageInternal(true /* checkMessageSize */);
- return true;
- }
- return false;
- }
-
- private void sendMessageInternal(final boolean checkMessageSize) {
- LogUtil.i(LogUtil.BUGLE_TAG, "UI initiated message sending in conversation " +
- mBinding.getData().getConversationId());
- if (mBinding.getData().isCheckingDraft()) {
- // Don't send message if we are currently checking draft for sending.
- LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: still checking draft");
- return;
- }
- // Check the host for pre-conditions about any action.
- if (mHost.isReadyForAction()) {
- mInputManager.showHideSimSelector(false /* show */, true /* animate */);
- final String messageToSend = mComposeEditText.getText().toString();
- mBinding.getData().setMessageText(messageToSend);
- final String subject = mComposeSubjectText.getText().toString();
- mBinding.getData().setMessageSubject(subject);
- // Asynchronously check the draft against various requirements before sending.
- mBinding.getData().checkDraftForAction(checkMessageSize,
- mHost.getConversationSelfSubId(), new CheckDraftTaskCallback() {
- @Override
- public void onDraftChecked(DraftMessageData data, int result) {
- mBinding.ensureBound(data);
- switch (result) {
- case CheckDraftForSendTask.RESULT_PASSED:
- // Continue sending after check succeeded.
- final MessageData message = mBinding.getData()
- .prepareMessageForSending(mBinding);
- if (message != null && message.hasContent()) {
- playSentSound();
- mHost.sendMessage(message);
- hideSubjectEditor();
- if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
- AccessibilityUtil.announceForAccessibilityCompat(
- ComposeMessageView.this, null,
- R.string.sending_message);
- }
- }
- break;
-
- case CheckDraftForSendTask.RESULT_HAS_PENDING_ATTACHMENTS:
- // Cannot send while there's still attachment(s) being loaded.
- UiUtils.showToastAtBottom(
- R.string.cant_send_message_while_loading_attachments);
- break;
-
- case CheckDraftForSendTask.RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS:
- mHost.promptForSelfPhoneNumber();
- break;
-
- case CheckDraftForSendTask.RESULT_MESSAGE_OVER_LIMIT:
- Assert.isTrue(checkMessageSize);
- mHost.warnOfExceedingMessageLimit(
- true /*sending*/, false /* tooManyVideos */);
- break;
-
- case CheckDraftForSendTask.RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED:
- Assert.isTrue(checkMessageSize);
- mHost.warnOfExceedingMessageLimit(
- true /*sending*/, true /* tooManyVideos */);
- break;
-
- case CheckDraftForSendTask.RESULT_SIM_NOT_READY:
- // Cannot send if there is no active subscription
- UiUtils.showToastAtBottom(
- R.string.cant_send_message_without_active_subscription);
- break;
-
- default:
- break;
- }
- }
- }, mBinding);
- } else {
- mHost.warnOfMissingActionConditions(true /*sending*/,
- new Runnable() {
- @Override
- public void run() {
- sendMessageInternal(checkMessageSize);
- }
-
- });
- }
- }
-
- public static void playSentSound() {
- // Check if this setting is enabled before playing
- final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
- final Context context = Factory.get().getApplicationContext();
- final String prefKey = context.getString(R.string.send_sound_pref_key);
- final boolean defaultValue = context.getResources().getBoolean(
- R.bool.send_sound_pref_default);
- if (!prefs.getBoolean(prefKey, defaultValue)) {
- return;
- }
- MediaUtil.get().playSound(context, R.raw.message_sent, null /* completionListener */);
- }
-
- /**
- * {@inheritDoc} from DraftMessageDataListener
- */
- @Override // From DraftMessageDataListener
- public void onDraftChanged(final DraftMessageData data, final int changeFlags) {
- // As this is called asynchronously when message read check bound before updating text
- mBinding.ensureBound(data);
-
- // We have to cache the values of the DraftMessageData because when we set
- // mComposeEditText, its onTextChanged calls updateVisualsOnDraftChanged,
- // which immediately reloads the text from the subject and message fields and replaces
- // what's in the DraftMessageData.
-
- final String subject = data.getMessageSubject();
- final String message = data.getMessageText();
-
- if ((changeFlags & DraftMessageData.MESSAGE_SUBJECT_CHANGED) ==
- DraftMessageData.MESSAGE_SUBJECT_CHANGED) {
- mComposeSubjectText.setText(subject);
-
- // Set the cursor selection to the end since setText resets it to the start
- mComposeSubjectText.setSelection(mComposeSubjectText.getText().length());
- }
-
- if ((changeFlags & DraftMessageData.MESSAGE_TEXT_CHANGED) ==
- DraftMessageData.MESSAGE_TEXT_CHANGED) {
- mComposeEditText.setText(message);
-
- // Set the cursor selection to the end since setText resets it to the start
- mComposeEditText.setSelection(mComposeEditText.getText().length());
- }
-
- if ((changeFlags & DraftMessageData.ATTACHMENTS_CHANGED) ==
- DraftMessageData.ATTACHMENTS_CHANGED) {
- final boolean haveAttachments = mAttachmentPreview.onAttachmentsChanged(data);
- mHost.onAttachmentsChanged(haveAttachments);
- }
-
- if ((changeFlags & DraftMessageData.SELF_CHANGED) == DraftMessageData.SELF_CHANGED) {
- updateOnSelfSubscriptionChange();
- }
- updateVisualsOnDraftChanged();
- }
-
- @Override // From DraftMessageDataListener
- public void onDraftAttachmentLimitReached(final DraftMessageData data) {
- mBinding.ensureBound(data);
- mHost.warnOfExceedingMessageLimit(false /* sending */, false /* tooManyVideos */);
- }
-
- private void updateOnSelfSubscriptionChange() {
- // Refresh the length filters according to the selected self's MmsConfig.
- mComposeEditText.setFilters(new InputFilter[] {
- new LengthFilter(MmsConfig.get(mBinding.getData().getSelfSubId())
- .getMaxTextLimit()) });
- mComposeSubjectText.setFilters(new InputFilter[] {
- new LengthFilter(MmsConfig.get(mBinding.getData().getSelfSubId())
- .getMaxSubjectLength())});
- }
-
- @Override
- public void onMediaItemsSelected(final Collection<MessagePartData> items) {
- mBinding.getData().addAttachments(items);
- announceMediaItemState(true /*isSelected*/);
- }
-
- @Override
- public void onMediaItemsUnselected(final MessagePartData item) {
- mBinding.getData().removeAttachment(item);
- announceMediaItemState(false /*isSelected*/);
- }
-
- @Override
- public void onPendingAttachmentAdded(final PendingAttachmentData pendingItem) {
- mBinding.getData().addPendingAttachment(pendingItem, mBinding);
- resumeComposeMessage();
- }
-
- private void announceMediaItemState(final boolean isSelected) {
- final Resources res = getContext().getResources();
- final String announcement = isSelected ? res.getString(
- R.string.mediapicker_gallery_item_selected_content_description) :
- res.getString(R.string.mediapicker_gallery_item_unselected_content_description);
- AccessibilityUtil.announceForAccessibilityCompat(
- this, null, announcement);
- }
-
- private void announceAttachmentState() {
- if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
- int attachmentCount = mBinding.getData().getReadOnlyAttachments().size()
- + mBinding.getData().getReadOnlyPendingAttachments().size();
- final String announcement = getContext().getResources().getQuantityString(
- R.plurals.attachment_changed_accessibility_announcement,
- attachmentCount, attachmentCount);
- AccessibilityUtil.announceForAccessibilityCompat(
- this, null, announcement);
- }
- }
-
- @Override
- public void resumeComposeMessage() {
- mComposeEditText.requestFocus();
- mInputManager.showHideImeKeyboard(true, true);
- announceAttachmentState();
- }
-
- public void clearAttachments() {
- mBinding.getData().clearAttachments(mHost.getAttachmentsClearedFlags());
- mHost.onAttachmentsCleared();
- }
-
- public void requestDraftMessage(boolean clearLocalDraft) {
- mBinding.getData().loadFromStorage(mBinding, null, clearLocalDraft);
- }
-
- public void setDraftMessage(final MessageData message) {
- mBinding.getData().loadFromStorage(mBinding, message, false);
- }
-
- public void writeDraftMessage() {
- final String messageText = mComposeEditText.getText().toString();
- mBinding.getData().setMessageText(messageText);
-
- final String subject = mComposeSubjectText.getText().toString();
- mBinding.getData().setMessageSubject(subject);
-
- mBinding.getData().saveToStorage(mBinding);
- }
-
- private void updateConversationSelfId(final String selfId, final boolean notify) {
- mBinding.getData().setSelfId(selfId, notify);
- }
-
- private Uri getSelfSendButtonIconUri() {
- final Uri overridenSelfUri = mHost.getSelfSendButtonIconUri();
- if (overridenSelfUri != null) {
- return overridenSelfUri;
- }
- final SubscriptionListEntry subscriptionListEntry = getSelfSubscriptionListEntry();
-
- if (subscriptionListEntry != null) {
- return subscriptionListEntry.selectedIconUri;
- }
-
- // Fall back to default self-avatar in the base case.
- final ParticipantData self = mConversationDataModel.getData().getDefaultSelfParticipant();
- return self == null ? null : AvatarUriUtil.createAvatarUri(self);
- }
-
- private SubscriptionListEntry getSelfSubscriptionListEntry() {
- return mConversationDataModel.getData().getSubscriptionEntryForSelfParticipant(
- mBinding.getData().getSelfId(), false /* excludeDefault */);
- }
-
- private boolean isDataLoadedForMessageSend() {
- // Check data loading prerequisites for sending a message.
- return mConversationDataModel != null && mConversationDataModel.isBound() &&
- mConversationDataModel.getData().getParticipantsLoaded();
- }
-
- private void updateVisualsOnDraftChanged() {
- final String messageText = mComposeEditText.getText().toString();
- final DraftMessageData draftMessageData = mBinding.getData();
- draftMessageData.setMessageText(messageText);
-
- final String subject = mComposeSubjectText.getText().toString();
- draftMessageData.setMessageSubject(subject);
- if (!TextUtils.isEmpty(subject)) {
- mSubjectView.setVisibility(View.VISIBLE);
- }
-
- final boolean hasMessageText = (TextUtils.getTrimmedLength(messageText) > 0);
- final boolean hasSubject = (TextUtils.getTrimmedLength(subject) > 0);
- final boolean hasWorkingDraft = hasMessageText || hasSubject ||
- mBinding.getData().hasAttachments();
-
- // Update the SMS text counter.
- final int messageCount = draftMessageData.getNumMessagesToBeSent();
- final int codePointsRemaining = draftMessageData.getCodePointsRemainingInCurrentMessage();
- // Show the counter only if:
- // - We are not in MMS mode
- // - We are going to send more than one message OR we are getting close
- boolean showCounter = false;
- if (!draftMessageData.getIsMms() && (messageCount > 1 ||
- codePointsRemaining <= CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN)) {
- showCounter = true;
- }
-
- if (showCounter) {
- // Update the remaining characters and number of messages required.
- final String counterText = messageCount > 1 ? codePointsRemaining + " / " +
- messageCount : String.valueOf(codePointsRemaining);
- mCharCounter.setText(counterText);
- mCharCounter.setVisibility(View.VISIBLE);
- } else {
- mCharCounter.setVisibility(View.INVISIBLE);
- }
-
- // Update the send message button. Self icon uri might be null if self participant data
- // and/or conversation metadata hasn't been loaded by the host.
- final Uri selfSendButtonUri = getSelfSendButtonIconUri();
- int sendWidgetMode = SEND_WIDGET_MODE_SELF_AVATAR;
- if (selfSendButtonUri != null) {
- if (hasWorkingDraft && isDataLoadedForMessageSend()) {
- UiUtils.revealOrHideViewWithAnimation(mSendButton, VISIBLE, null);
- if (isOverriddenAvatarAGroup()) {
- // If the host has overriden the avatar to show a group avatar where the
- // send button sits, we have to hide the group avatar because it can be larger
- // than the send button and pieces of the avatar will stick out from behind
- // the send button.
- UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, GONE, null);
- }
- mMmsIndicator.setVisibility(draftMessageData.getIsMms() ? VISIBLE : INVISIBLE);
- sendWidgetMode = SEND_WIDGET_MODE_SEND_BUTTON;
- } else {
- mSelfSendIcon.setImageResourceUri(selfSendButtonUri);
- if (isOverriddenAvatarAGroup()) {
- UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, VISIBLE, null);
- }
- UiUtils.revealOrHideViewWithAnimation(mSendButton, GONE, null);
- mMmsIndicator.setVisibility(INVISIBLE);
- if (shouldShowSimSelector(mConversationDataModel.getData())) {
- sendWidgetMode = SEND_WIDGET_MODE_SIM_SELECTOR;
- }
- }
- } else {
- mSelfSendIcon.setImageResourceUri(null);
- }
-
- if (mSendWidgetMode != sendWidgetMode || sendWidgetMode == SEND_WIDGET_MODE_SIM_SELECTOR) {
- setSendButtonAccessibility(sendWidgetMode);
- mSendWidgetMode = sendWidgetMode;
- }
-
- // Update the text hint on the message box depending on the attachment type.
- final List<MessagePartData> attachments = draftMessageData.getReadOnlyAttachments();
- final int attachmentCount = attachments.size();
- if (attachmentCount == 0) {
- final SubscriptionListEntry subscriptionListEntry =
- mConversationDataModel.getData().getSubscriptionEntryForSelfParticipant(
- mBinding.getData().getSelfId(), false /* excludeDefault */);
- if (subscriptionListEntry == null) {
- mComposeEditText.setHint(R.string.compose_message_view_hint_text);
- } else {
- mComposeEditText.setHint(Html.fromHtml(getResources().getString(
- R.string.compose_message_view_hint_text_multi_sim,
- subscriptionListEntry.displayName)));
- }
- } else {
- int type = -1;
- for (final MessagePartData attachment : attachments) {
- int newType;
- if (attachment.isImage()) {
- newType = ContentType.TYPE_IMAGE;
- } else if (attachment.isAudio()) {
- newType = ContentType.TYPE_AUDIO;
- } else if (attachment.isVideo()) {
- newType = ContentType.TYPE_VIDEO;
- } else if (attachment.isVCard()) {
- newType = ContentType.TYPE_VCARD;
- } else {
- newType = ContentType.TYPE_OTHER;
- }
-
- if (type == -1) {
- type = newType;
- } else if (type != newType || type == ContentType.TYPE_OTHER) {
- type = ContentType.TYPE_OTHER;
- break;
- }
- }
-
- switch (type) {
- case ContentType.TYPE_IMAGE:
- mComposeEditText.setHint(getResources().getQuantityString(
- R.plurals.compose_message_view_hint_text_photo, attachmentCount));
- break;
-
- case ContentType.TYPE_AUDIO:
- mComposeEditText.setHint(getResources().getQuantityString(
- R.plurals.compose_message_view_hint_text_audio, attachmentCount));
- break;
-
- case ContentType.TYPE_VIDEO:
- mComposeEditText.setHint(getResources().getQuantityString(
- R.plurals.compose_message_view_hint_text_video, attachmentCount));
- break;
-
- case ContentType.TYPE_VCARD:
- mComposeEditText.setHint(getResources().getQuantityString(
- R.plurals.compose_message_view_hint_text_vcard, attachmentCount));
- break;
-
- case ContentType.TYPE_OTHER:
- mComposeEditText.setHint(getResources().getQuantityString(
- R.plurals.compose_message_view_hint_text_attachments, attachmentCount));
- break;
-
- default:
- Assert.fail("Unsupported attachment type!");
- break;
- }
- }
- }
-
- private void setSendButtonAccessibility(final int sendWidgetMode) {
- switch (sendWidgetMode) {
- case SEND_WIDGET_MODE_SELF_AVATAR:
- // No send button and no SIM selector; the self send button is no longer
- // important for accessibility.
- mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- mSelfSendIcon.setContentDescription(null);
- mSendButton.setVisibility(View.GONE);
- setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SELF_AVATAR);
- break;
-
- case SEND_WIDGET_MODE_SIM_SELECTOR:
- mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
- mSelfSendIcon.setContentDescription(getSimContentDescription());
- setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SIM_SELECTOR);
- break;
-
- case SEND_WIDGET_MODE_SEND_BUTTON:
- mMmsIndicator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- mMmsIndicator.setContentDescription(null);
- setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SEND_BUTTON);
- break;
- }
- }
-
- private String getSimContentDescription() {
- final SubscriptionListEntry sub = getSelfSubscriptionListEntry();
- if (sub != null) {
- return getResources().getString(
- R.string.sim_selector_button_content_description_with_selection,
- sub.displayName);
- } else {
- return getResources().getString(
- R.string.sim_selector_button_content_description);
- }
- }
-
- // Set accessibility traversal order of the components in the send widget.
- private void setSendWidgetAccessibilityTraversalOrder(final int mode) {
- if (OsUtil.isAtLeastL_MR1()) {
- mAttachMediaButton.setAccessibilityTraversalBefore(R.id.compose_message_text);
- switch (mode) {
- case SEND_WIDGET_MODE_SIM_SELECTOR:
- mComposeEditText.setAccessibilityTraversalBefore(R.id.self_send_icon);
- break;
- case SEND_WIDGET_MODE_SEND_BUTTON:
- mComposeEditText.setAccessibilityTraversalBefore(R.id.send_message_button);
- break;
- default:
- break;
- }
- }
- }
-
- @Override
- public void afterTextChanged(final Editable editable) {
- }
-
- @Override
- public void beforeTextChanged(final CharSequence s, final int start, final int count,
- final int after) {
- if (mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
- hideSimSelector();
- }
- }
-
- private void hideSimSelector() {
- if (mInputManager.showHideSimSelector(false /* show */, true /* animate */)) {
- // Now that the sim selector has been hidden, reshow the attachments if they
- // have been hidden.
- hideAttachmentsWhenShowingSims(false /*simPickerVisible*/);
- }
- }
-
- @Override
- public void onTextChanged(final CharSequence s, final int start, final int before,
- final int count) {
- final BugleActionBarActivity activity = (mOriginalContext instanceof BugleActionBarActivity)
- ? (BugleActionBarActivity) mOriginalContext : null;
- if (activity != null && activity.getIsDestroyed()) {
- LogUtil.v(LogUtil.BUGLE_TAG, "got onTextChanged after onDestroy");
-
- // if we get onTextChanged after the activity is destroyed then, ah, wtf
- // b/18176615
- // This appears to have occurred as the result of orientation change.
- return;
- }
-
- mBinding.ensureBound();
- updateVisualsOnDraftChanged();
- }
-
- @Override
- public PlainTextEditText getComposeEditText() {
- return mComposeEditText;
- }
-
- public void displayPhoto(final Uri photoUri, final Rect imageBounds) {
- mHost.displayPhoto(photoUri, imageBounds, true /* isDraft */);
- }
-
- public void updateConversationSelfIdOnExternalChange(final String selfId) {
- updateConversationSelfId(selfId, true /* notify */);
- }
-
- /**
- * The selfId of the conversation. As soon as the DraftMessageData successfully loads (i.e.
- * getSelfId() is non-null), the selfId in DraftMessageData is treated as the sole source
- * of truth for conversation self id since it reflects any pending self id change the user
- * makes in the UI.
- */
- public String getConversationSelfId() {
- return mBinding.getData().getSelfId();
- }
-
- public void selectSim(SubscriptionListEntry subscriptionData) {
- final String oldSelfId = getConversationSelfId();
- final String newSelfId = subscriptionData.selfParticipantId;
- Assert.notNull(newSelfId);
- // Don't attempt to change self if self hasn't been loaded, or if self hasn't changed.
- if (oldSelfId == null || TextUtils.equals(oldSelfId, newSelfId)) {
- return;
- }
- updateConversationSelfId(newSelfId, true /* notify */);
- }
-
- public void hideAllComposeInputs(final boolean animate) {
- mInputManager.hideAllInputs(animate);
- }
-
- public void saveInputState(final Bundle outState) {
- mInputManager.onSaveInputState(outState);
- }
-
- public void resetMediaPickerState() {
- mInputManager.resetMediaPickerState();
- }
-
- public boolean onBackPressed() {
- return mInputManager.onBackPressed();
- }
-
- public boolean onNavigationUpPressed() {
- return mInputManager.onNavigationUpPressed();
- }
-
- public boolean updateActionBar(final ActionBar actionBar) {
- return mInputManager != null ? mInputManager.updateActionBar(actionBar) : false;
- }
-
- public static boolean shouldShowSimSelector(final ConversationData convData) {
- return OsUtil.isAtLeastL_MR1() &&
- convData.getSelfParticipantsCountExcludingDefault(true /* activeOnly */) > 1;
- }
-
- public void sendMessageIgnoreMessageSizeLimit() {
- sendMessageInternal(false /* checkMessageSize */);
- }
-
- public void onAttachmentPreviewLongClicked() {
- mHost.showAttachmentChooser();
- }
-
- @Override
- public void onDraftAttachmentLoadFailed() {
- mHost.notifyOfAttachmentLoadFailed();
- }
-
- private boolean isOverriddenAvatarAGroup() {
- final Uri overridenSelfUri = mHost.getSelfSendButtonIconUri();
- if (overridenSelfUri == null) {
- return false;
- }
- return AvatarUriUtil.TYPE_GROUP_URI.equals(AvatarUriUtil.getAvatarType(overridenSelfUri));
- }
-
- @Override
- public void setAccessibility(boolean enabled) {
- if (enabled) {
- mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
- mComposeEditText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
- mSendButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
- setSendButtonAccessibility(mSendWidgetMode);
- } else {
- mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- mComposeEditText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- mSendButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
- }
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/ConversationActivity.java b/src/com/android/messaging/ui/conversation/ConversationActivity.java
deleted file mode 100644
index 66310ea..0000000
--- a/src/com/android/messaging/ui/conversation/ConversationActivity.java
+++ /dev/null
@@ -1,379 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.messaging.ui.conversation;
-
-import android.app.FragmentManager;
-import android.app.FragmentTransaction;
-import android.content.Intent;
-import android.graphics.Rect;
-import android.net.Uri;
-import android.os.Bundle;
-import android.support.v7.app.ActionBar;
-import android.text.TextUtils;
-import android.view.MenuItem;
-
-import com.android.messaging.R;
-import com.android.messaging.datamodel.MessagingContentProvider;
-import com.android.messaging.datamodel.data.MessageData;
-import com.android.messaging.ui.BugleActionBarActivity;
-import com.android.messaging.ui.UIIntents;
-import com.android.messaging.ui.contact.ContactPickerFragment;
-import com.android.messaging.ui.contact.ContactPickerFragment.ContactPickerFragmentHost;
-import com.android.messaging.ui.conversation.ConversationActivityUiState.ConversationActivityUiStateHost;
-import com.android.messaging.ui.conversation.ConversationFragment.ConversationFragmentHost;
-import com.android.messaging.ui.conversationlist.ConversationListActivity;
-import com.android.messaging.util.Assert;
-import com.android.messaging.util.ContentType;
-import com.android.messaging.util.LogUtil;
-import com.android.messaging.util.OsUtil;
-import com.android.messaging.util.UiUtils;
-
-public class ConversationActivity extends BugleActionBarActivity
- implements ContactPickerFragmentHost, ConversationFragmentHost,
- ConversationActivityUiStateHost {
- public static final int FINISH_RESULT_CODE = 1;
- private static final String SAVED_INSTANCE_STATE_UI_STATE_KEY = "uistate";
-
- private ConversationActivityUiState mUiState;
-
- // Fragment transactions cannot be performed after onSaveInstanceState() has been called since
- // it will cause state loss. We don't want to call commitAllowingStateLoss() since it's
- // dangerous. Therefore, we note when instance state is saved and avoid performing UI state
- // updates concerning fragments past that point.
- private boolean mInstanceStateSaved;
-
- // Tracks whether onPause is called.
- private boolean mIsPaused;
-
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.conversation_activity);
-
- final Intent intent = getIntent();
-
- // Do our best to restore UI state from saved instance state.
- if (savedInstanceState != null) {
- mUiState = savedInstanceState.getParcelable(SAVED_INSTANCE_STATE_UI_STATE_KEY);
- } else {
- if (intent.
- getBooleanExtra(UIIntents.UI_INTENT_EXTRA_GOTO_CONVERSATION_LIST, false)) {
- // See the comment in BugleWidgetService.getViewMoreConversationsView() why this
- // is unfortunately necessary. The Bugle desktop widget can display a list of
- // conversations. When there are more conversations that can be displayed in
- // the widget, the last item is a "More conversations" item. The way widgets
- // are built, the list items can only go to a single fill-in intent which points
- // to this ConversationActivity. When the user taps on "More conversations", we
- // really want to go to the ConversationList. This code makes that possible.
- finish();
- final Intent convListIntent = new Intent(this, ConversationListActivity.class);
- convListIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- startActivity(convListIntent);
- return;
- }
- }
-
- // If saved instance state doesn't offer a clue, get the info from the intent.
- if (mUiState == null) {
- final String conversationId = intent.getStringExtra(
- UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID);
- mUiState = new ConversationActivityUiState(conversationId);
- }
- mUiState.setHost(this);
- mInstanceStateSaved = false;
-
- // Don't animate UI state change for initial setup.
- updateUiState(false /* animate */);
-
- // See if we're getting called from a widget to directly display an image or video
- final String extraToDisplay =
- intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_ATTACHMENT_URI);
- if (!TextUtils.isEmpty(extraToDisplay)) {
- final String contentType =
- intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_ATTACHMENT_TYPE);
- final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(
- findViewById(R.id.conversation_and_compose_container));
- if (ContentType.isImageType(contentType)) {
- final Uri imagesUri = MessagingContentProvider.buildConversationImagesUri(
- mUiState.getConversationId());
- UIIntents.get().launchFullScreenPhotoViewer(
- this, Uri.parse(extraToDisplay), bounds, imagesUri);
- } else if (ContentType.isVideoType(contentType)) {
- UIIntents.get().launchFullScreenVideoViewer(this, Uri.parse(extraToDisplay));
- }
- }
- }
-
- @Override
- protected void onSaveInstanceState(final Bundle outState) {
- super.onSaveInstanceState(outState);
- // After onSaveInstanceState() is called, future changes to mUiState won't update the UI
- // anymore, because fragment transactions are not allowed past this point.
- // For an activity recreation due to orientation change, the saved instance state keeps
- // using the in-memory copy of the UI state instead of writing it to parcel as an
- // optimization, so the UI state values may still change in response to, for example,
- // focus change from the framework, making mUiState and actual UI inconsistent.
- // Therefore, save an exact "snapshot" (clone) of the UI state object to make sure the
- // restored UI state ALWAYS matches the actual restored UI components.
- outState.putParcelable(SAVED_INSTANCE_STATE_UI_STATE_KEY, mUiState.clone());
- mInstanceStateSaved = true;
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- // we need to reset the mInstanceStateSaved flag since we may have just been restored from
- // a previous onStop() instead of an onDestroy().
- mInstanceStateSaved = false;
- mIsPaused = false;
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- mIsPaused = true;
- }
-
- @Override
- public void onWindowFocusChanged(final boolean hasFocus) {
- super.onWindowFocusChanged(hasFocus);
- final ConversationFragment conversationFragment = getConversationFragment();
- // When the screen is turned on, the last used activity gets resumed, but it gets
- // window focus only after the lock screen is unlocked.
- if (hasFocus && conversationFragment != null) {
- conversationFragment.setConversationFocus();
- }
- }
-
- @Override
- public void onDisplayHeightChanged(final int heightSpecification) {
- super.onDisplayHeightChanged(heightSpecification);
- invalidateActionBar();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- if (mUiState != null) {
- mUiState.setHost(null);
- }
- }
-
- @Override
- public void updateActionBar(final ActionBar actionBar) {
- super.updateActionBar(actionBar);
- final ConversationFragment conversation = getConversationFragment();
- final ContactPickerFragment contactPicker = getContactPicker();
- if (contactPicker != null && mUiState.shouldShowContactPickerFragment()) {
- contactPicker.updateActionBar(actionBar);
- } else if (conversation != null && mUiState.shouldShowConversationFragment()) {
- conversation.updateActionBar(actionBar);
- }
- }
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem menuItem) {
- if (super.onOptionsItemSelected(menuItem)) {
- return true;
- }
- if (menuItem.getItemId() == android.R.id.home) {
- onNavigationUpPressed();
- return true;
- }
- return false;
- }
-
- public void onNavigationUpPressed() {
- // Let the conversation fragment handle the navigation up press.
- final ConversationFragment conversationFragment = getConversationFragment();
- if (conversationFragment != null && conversationFragment.onNavigationUpPressed()) {
- return;
- }
- onFinishCurrentConversation();
- }
-
- @Override
- public void onBackPressed() {
- // If action mode is active dismiss it
- if (getActionMode() != null) {
- dismissActionMode();
- return;
- }
-
- // Let the conversation fragment handle the back press.
- final ConversationFragment conversationFragment = getConversationFragment();
- if (conversationFragment != null && conversationFragment.onBackPressed()) {
- return;
- }
- super.onBackPressed();
- }
-
- private ContactPickerFragment getContactPicker() {
- return (ContactPickerFragment) getFragmentManager().findFragmentByTag(
- ContactPickerFragment.FRAGMENT_TAG);
- }
-
- private ConversationFragment getConversationFragment() {
- return (ConversationFragment) getFragmentManager().findFragmentByTag(
- ConversationFragment.FRAGMENT_TAG);
- }
-
- @Override // From ContactPickerFragmentHost
- public void onGetOrCreateNewConversation(final String conversationId) {
- Assert.isTrue(conversationId != null);
- mUiState.onGetOrCreateConversation(conversationId);
- }
-
- @Override // From ContactPickerFragmentHost
- public void onBackButtonPressed() {
- onBackPressed();
- }
-
- @Override // From ContactPickerFragmentHost
- public void onInitiateAddMoreParticipants() {
- mUiState.onAddMoreParticipants();
- }
-
-
- @Override
- public void onParticipantCountChanged(final boolean canAddMoreParticipants) {
- mUiState.onParticipantCountUpdated(canAddMoreParticipants);
- }
-
- @Override // From ConversationFragmentHost
- public void onStartComposeMessage() {
- mUiState.onStartMessageCompose();
- }
-
- @Override // From ConversationFragmentHost
- public void onConversationMetadataUpdated() {
- invalidateActionBar();
- }
-
- @Override // From ConversationFragmentHost
- public void onConversationMessagesUpdated(final int numberOfMessages) {
- }
-
- @Override // From ConversationFragmentHost
- public void onConversationParticipantDataLoaded(final int numberOfParticipants) {
- }
-
- @Override // From ConversationFragmentHost
- public boolean isActiveAndFocused() {
- return !mIsPaused && hasWindowFocus();
- }
-
- @Override // From ConversationActivityUiStateListener
- public void onConversationContactPickerUiStateChanged(final int oldState, final int newState,
- final boolean animate) {
- Assert.isTrue(oldState != newState);
- updateUiState(animate);
- }
-
- private void updateUiState(final boolean animate) {
- if (mInstanceStateSaved || mIsPaused) {
- return;
- }
- Assert.notNull(mUiState);
- final Intent intent = getIntent();
- final String conversationId = mUiState.getConversationId();
-
- final FragmentManager fragmentManager = getFragmentManager();
- final FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
-
- final boolean needConversationFragment = mUiState.shouldShowConversationFragment();
- final boolean needContactPickerFragment = mUiState.shouldShowContactPickerFragment();
- ConversationFragment conversationFragment = getConversationFragment();
-
- // Set up the conversation fragment.
- if (needConversationFragment) {
- Assert.notNull(conversationId);
- if (conversationFragment == null) {
- conversationFragment = new ConversationFragment();
- fragmentTransaction.add(R.id.conversation_fragment_container,
- conversationFragment, ConversationFragment.FRAGMENT_TAG);
- }
- final MessageData draftData = intent.getParcelableExtra(
- UIIntents.UI_INTENT_EXTRA_DRAFT_DATA);
- if (!needContactPickerFragment) {
- // Once the user has committed the audience,remove the draft data from the
- // intent to prevent reuse
- intent.removeExtra(UIIntents.UI_INTENT_EXTRA_DRAFT_DATA);
- }
- conversationFragment.setHost(this);
- conversationFragment.setConversationInfo(this, conversationId, draftData);
- } else if (conversationFragment != null) {
- // Don't save draft to DB when removing conversation fragment and switching to
- // contact picking mode. The draft is intended for the new group.
- conversationFragment.suppressWriteDraft();
- fragmentTransaction.remove(conversationFragment);
- }
-
- // Set up the contact picker fragment.
- ContactPickerFragment contactPickerFragment = getContactPicker();
- if (needContactPickerFragment) {
- if (contactPickerFragment == null) {
- contactPickerFragment = new ContactPickerFragment();
- fragmentTransaction.add(R.id.contact_picker_fragment_container,
- contactPickerFragment, ContactPickerFragment.FRAGMENT_TAG);
- }
- contactPickerFragment.setHost(this);
- contactPickerFragment.setContactPickingMode(mUiState.getDesiredContactPickingMode(),
- animate);
- } else if (contactPickerFragment != null) {
- fragmentTransaction.remove(contactPickerFragment);
- }
-
- fragmentTransaction.commit();
- invalidateActionBar();
- }
-
- @Override
- public void onFinishCurrentConversation() {
- // Simply finish the current activity. The current design is to leave any empty
- // conversations as is.
- if (OsUtil.isAtLeastL()) {
- finishAfterTransition();
- } else {
- finish();
- }
- }
-
- @Override
- public boolean shouldResumeComposeMessage() {
- return mUiState.shouldResumeComposeMessage();
- }
-
- @Override
- protected void onActivityResult(final int requestCode, final int resultCode,
- final Intent data) {
- if (requestCode == ConversationFragment.REQUEST_CHOOSE_ATTACHMENTS &&
- resultCode == RESULT_OK) {
- final ConversationFragment conversationFragment = getConversationFragment();
- if (conversationFragment != null) {
- conversationFragment.onAttachmentChoosen();
- } else {
- LogUtil.e(LogUtil.BUGLE_TAG, "ConversationFragment is missing after launching " +
- "AttachmentChooserActivity!");
- }
- } else if (resultCode == FINISH_RESULT_CODE) {
- finish();
- }
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/ConversationActivityUiState.java b/src/com/android/messaging/ui/conversation/ConversationActivityUiState.java
deleted file mode 100644
index 1469c93..0000000
--- a/src/com/android/messaging/ui/conversation/ConversationActivityUiState.java
+++ /dev/null
@@ -1,306 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.messaging.ui.conversation;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.messaging.ui.contact.ContactPickerFragment;
-import com.android.messaging.util.Assert;
-import com.google.common.annotations.VisibleForTesting;
-
-/**
- * Keeps track of the different UI states that the ConversationActivity may be in. This acts as
- * a state machine which, based on different actions (e.g. onAddMoreParticipants), notifies the
- * ConversationActivity about any state UI change so it can update the visuals. This class
- * implements Parcelable and it's persisted across activity tear down and relaunch.
- */
-public class ConversationActivityUiState implements Parcelable, Cloneable {
- interface ConversationActivityUiStateHost {
- void onConversationContactPickerUiStateChanged(int oldState, int newState, boolean animate);
- }
-
- /*------ Overall UI states (conversation & contact picker) ------*/
-
- /** Only a full screen conversation is showing. */
- public static final int STATE_CONVERSATION_ONLY = 1;
- /** Only a full screen contact picker is showing asking user to pick the initial contact. */
- public static final int STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT = 2;
- /**
- * Only a full screen contact picker is showing asking user to pick more participants. This
- * happens after the user picked the initial contact, and then decide to go back and add more.
- */
- public static final int STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS = 3;
- /**
- * Only a full screen contact picker is showing asking user to pick more participants. However
- * user has reached max number of conversation participants and can add no more.
- */
- public static final int STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS = 4;
- /**
- * A hybrid mode where the conversation view + contact chips view are showing. This happens
- * right after the user picked the initial contact for which a 1-1 conversation is fetched or
- * created.
- */
- public static final int STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW = 5;
-
- // The overall UI state of the ConversationActivity.
- private int mConversationContactUiState;
-
- // The currently displayed conversation (if any).
- private String mConversationId;
-
- // Indicates whether we should put focus in the compose message view when the
- // ConversationFragment is attached. This is a transient state that's not persisted as
- // part of the parcelable.
- private boolean mPendingResumeComposeMessage = false;
-
- // The owner ConversationActivity. This is not parceled since the instance always change upon
- // object reuse.
- private ConversationActivityUiStateHost mHost;
-
- // Indicates the owning ConverastionActivity is in the process of updating its UI presentation
- // to be in sync with the UI states. Outside of the UI updates, the UI states here should
- // ALWAYS be consistent with the actual states of the activity.
- private int mUiUpdateCount;
-
- /**
- * Create a new instance with an initial conversation id.
- */
- ConversationActivityUiState(final String conversationId) {
- // The conversation activity may be initialized with only one of two states:
- // Conversation-only (when there's a conversation id) or picking initial contact
- // (when no conversation id is given).
- mConversationId = conversationId;
- mConversationContactUiState = conversationId == null ?
- STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT : STATE_CONVERSATION_ONLY;
- }
-
- public void setHost(final ConversationActivityUiStateHost host) {
- mHost = host;
- }
-
- public boolean shouldShowConversationFragment() {
- return mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW ||
- mConversationContactUiState == STATE_CONVERSATION_ONLY;
- }
-
- public boolean shouldShowContactPickerFragment() {
- return mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS ||
- mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS ||
- mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT ||
- mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW;
- }
-
- /**
- * Returns whether there's a pending request to resume message compose (i.e. set focus to
- * the compose message view and show the soft keyboard). If so, this request will be served
- * when the conversation fragment get created and resumed. This happens when the user commits
- * participant selection for a group conversation and goes back to the conversation fragment.
- * Since conversation fragment creation happens asynchronously, we issue and track this
- * pending request for it to be eventually fulfilled.
- */
- public boolean shouldResumeComposeMessage() {
- if (mPendingResumeComposeMessage) {
- // This is a one-shot operation that just keeps track of the pending resume compose
- // state. This is also a non-critical operation so we don't care about failure case.
- mPendingResumeComposeMessage = false;
- return true;
- }
- return false;
- }
-
- public int getDesiredContactPickingMode() {
- switch (mConversationContactUiState) {
- case STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS:
- return ContactPickerFragment.MODE_PICK_MORE_CONTACTS;
- case STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS:
- return ContactPickerFragment.MODE_PICK_MAX_PARTICIPANTS;
- case STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT:
- return ContactPickerFragment.MODE_PICK_INITIAL_CONTACT;
- case STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW:
- return ContactPickerFragment.MODE_CHIPS_ONLY;
- default:
- Assert.fail("Invalid contact picking mode for ConversationActivity!");
- return ContactPickerFragment.MODE_UNDEFINED;
- }
- }
-
- public String getConversationId() {
- return mConversationId;
- }
-
- /**
- * Called whenever the contact picker fragment successfully fetched or created a conversation.
- */
- public void onGetOrCreateConversation(final String conversationId) {
- int newState = STATE_CONVERSATION_ONLY;
- if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT) {
- newState = STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW;
- } else if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS ||
- mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS) {
- newState = STATE_CONVERSATION_ONLY;
- } else {
- // New conversation should only be created when we are in one of the contact picking
- // modes.
- Assert.fail("Invalid conversation activity state: can't create conversation!");
- }
- mConversationId = conversationId;
- performUiStateUpdate(newState, true);
- }
-
- /**
- * Called when the user started composing message. If we are in the hybrid chips state, we
- * should commit to enter the conversation only state.
- */
- public void onStartMessageCompose() {
- // This cannot happen when we are in one of the full-screen contact picking states.
- Assert.isTrue(mConversationContactUiState != STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT &&
- mConversationContactUiState != STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS &&
- mConversationContactUiState != STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS);
- if (mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW) {
- performUiStateUpdate(STATE_CONVERSATION_ONLY, true);
- }
- }
-
- /**
- * Called when the user initiated an action to add more participants in the hybrid state,
- * namely clicking on the "add more participants" button or entered a new contact chip via
- * auto-complete.
- */
- public void onAddMoreParticipants() {
- if (mConversationContactUiState == STATE_HYBRID_WITH_CONVERSATION_AND_CHIPS_VIEW) {
- mPendingResumeComposeMessage = true;
- performUiStateUpdate(STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS, true);
- } else {
- // This is only possible in the hybrid state.
- Assert.fail("Invalid conversation activity state: can't add more participants!");
- }
- }
-
- /**
- * Called each time the number of participants is updated to check against the limit and
- * update the ui state accordingly.
- */
- public void onParticipantCountUpdated(final boolean canAddMoreParticipants) {
- if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS
- && !canAddMoreParticipants) {
- performUiStateUpdate(STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS, false);
- } else if (mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_MAX_PARTICIPANTS
- && canAddMoreParticipants) {
- performUiStateUpdate(STATE_CONTACT_PICKER_ONLY_ADD_MORE_CONTACTS, false);
- }
- }
-
- private void performUiStateUpdate(final int conversationContactState, final boolean animate) {
- // This starts one UI update cycle, during which we allow the conversation activity's
- // UI presentation to be temporarily out of sync with the states here.
- beginUiUpdate();
-
- if (conversationContactState != mConversationContactUiState) {
- final int oldState = mConversationContactUiState;
- mConversationContactUiState = conversationContactState;
- notifyOnOverallUiStateChanged(oldState, mConversationContactUiState, animate);
- }
- endUiUpdate();
- }
-
- private void notifyOnOverallUiStateChanged(
- final int oldState, final int newState, final boolean animate) {
- // Always verify state validity whenever we have a state change.
- assertValidState();
- Assert.isTrue(isUiUpdateInProgress());
-
- // Only do this if we are still attached to the host. mHost can be null if the host
- // activity is already destroyed, but due to timing the contained UI components may still
- // receive events such as focus change and trigger a callback to the Ui state. We'd like
- // to guard against those cases.
- if (mHost != null) {
- mHost.onConversationContactPickerUiStateChanged(oldState, newState, animate);
- }
- }
-
- private void assertValidState() {
- // Conversation id may be null IF AND ONLY IF the user is picking the initial contact to
- // start a conversation.
- Assert.isTrue((mConversationContactUiState == STATE_CONTACT_PICKER_ONLY_INITIAL_CONTACT) ==
- (mConversationId == null));
- }
-
- private void beginUiUpdate() {
- mUiUpdateCount++;
- }
-
- private void endUiUpdate() {
- if (--mUiUpdateCount < 0) {
- Assert.fail("Unbalanced Ui updates!");
- }
- }
-
- private boolean isUiUpdateInProgress() {
- return mUiUpdateCount > 0;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(final Parcel dest, final int flags) {
- dest.writeInt(mConversationContactUiState);
- dest.writeString(mConversationId);
- }
-
- private ConversationActivityUiState(final Parcel in) {
- mConversationContactUiState = in.readInt();
- mConversationId = in.readString();
-
- // Always verify state validity whenever we initialize states.
- assertValidState();
- }
-
- public static final Parcelable.Creator<ConversationActivityUiState> CREATOR
- = new Parcelable.Creator<ConversationActivityUiState>() {
- @Override
- public ConversationActivityUiState createFromParcel(final Parcel in) {
- return new ConversationActivityUiState(in);
- }
-
- @Override
- public ConversationActivityUiState[] newArray(final int size) {
- return new ConversationActivityUiState[size];
- }
- };
-
- @Override
- protected ConversationActivityUiState clone() {
- try {
- return (ConversationActivityUiState) super.clone();
- } catch (CloneNotSupportedException e) {
- Assert.fail("ConversationActivityUiState: failed to clone(). Is there a mutable " +
- "reference?");
- }
- return null;
- }
-
- /**
- * allows for overridding the internal UI state. Should never be called except by test code.
- */
- @VisibleForTesting
- void testSetUiState(final int uiState) {
- mConversationContactUiState = uiState;
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/ConversationFastScroller.java b/src/com/android/messaging/ui/conversation/ConversationFastScroller.java
deleted file mode 100644
index b15f05a..0000000
--- a/src/com/android/messaging/ui/conversation/ConversationFastScroller.java
+++ /dev/null
@@ -1,489 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.messaging.ui.conversation;
-
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.content.res.Resources;
-import android.graphics.Rect;
-import android.graphics.drawable.StateListDrawable;
-import android.os.Handler;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.AdapterDataObserver;
-import android.support.v7.widget.RecyclerView.ViewHolder;
-import android.util.StateSet;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.View.MeasureSpec;
-import android.view.View.OnLayoutChangeListener;
-import android.view.ViewGroupOverlay;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.messaging.R;
-import com.android.messaging.datamodel.data.ConversationMessageData;
-import com.android.messaging.ui.ConversationDrawables;
-import com.android.messaging.util.Dates;
-import com.android.messaging.util.OsUtil;
-
-/**
- * Adds a "fast-scroll" bar to the conversation RecyclerView that shows the current position within
- * the conversation and allows quickly moving to another position by dragging the scrollbar thumb
- * up or down. As the thumb is dragged, we show a floating bubble alongside it that shows the
- * date/time of the first visible message at the current position.
- */
-public class ConversationFastScroller extends RecyclerView.OnScrollListener implements
- OnLayoutChangeListener, RecyclerView.OnItemTouchListener {
-
- /**
- * Creates a {@link ConversationFastScroller} instance, attached to the provided
- * {@link RecyclerView}.
- *
- * @param rv the conversation RecyclerView
- * @param position where the scrollbar should appear (either {@code POSITION_RIGHT_SIDE} or
- * {@code POSITION_LEFT_SIDE})
- * @return a new ConversationFastScroller, or {@code null} if fast-scrolling is not supported
- * (the feature requires Jellybean MR2 or newer)
- */
- public static ConversationFastScroller addTo(RecyclerView rv, int position) {
- if (OsUtil.isAtLeastJB_MR2()) {
- return new ConversationFastScroller(rv, position);
- }
- return null;
- }
-
- public static final int POSITION_RIGHT_SIDE = 0;
- public static final int POSITION_LEFT_SIDE = 1;
-
- private static final int MIN_PAGES_TO_ENABLE = 7;
- private static final int SHOW_ANIMATION_DURATION_MS = 150;
- private static final int HIDE_ANIMATION_DURATION_MS = 300;
- private static final int HIDE_DELAY_MS = 1500;
-
- private final Context mContext;
- private final RecyclerView mRv;
- private final ViewGroupOverlay mOverlay;
- private final ImageView mTrackImageView;
- private final ImageView mThumbImageView;
- private final TextView mPreviewTextView;
-
- private final int mTrackWidth;
- private final int mThumbHeight;
- private final int mPreviewHeight;
- private final int mPreviewMinWidth;
- private final int mPreviewMarginTop;
- private final int mPreviewMarginLeftRight;
- private final int mTouchSlop;
-
- private final Rect mContainer = new Rect();
- private final Handler mHandler = new Handler();
-
- // Whether to render the scrollbar on the right side (otherwise it'll be on the left).
- private final boolean mPosRight;
-
- // Whether the scrollbar is currently visible (it may still be animating).
- private boolean mVisible = false;
-
- // Whether we are waiting to hide the scrollbar (i.e. scrolling has stopped).
- private boolean mPendingHide = false;
-
- // Whether the user is currently dragging the thumb up or down.
- private boolean mDragging = false;
-
- // Animations responsible for hiding the scrollbar & preview. May be null.
- private AnimatorSet mHideAnimation;
- private ObjectAnimator mHidePreviewAnimation;
-
- private final Runnable mHideTrackRunnable = new Runnable() {
- @Override
- public void run() {
- hide(true /* animate */);
- mPendingHide = false;
- }
- };
-
- private ConversationFastScroller(RecyclerView rv, int position) {
- mContext = rv.getContext();
- mRv = rv;
- mRv.addOnLayoutChangeListener(this);
- mRv.addOnScrollListener(this);
- mRv.addOnItemTouchListener(this);
- mRv.getAdapter().registerAdapterDataObserver(new AdapterDataObserver() {
- @Override
- public void onChanged() {
- updateScrollPos();
- }
- });
- mPosRight = (position == POSITION_RIGHT_SIDE);
-
- // Cache the dimensions we'll need during layout
- final Resources res = mContext.getResources();
- mTrackWidth = res.getDimensionPixelSize(R.dimen.fastscroll_track_width);
- mThumbHeight = res.getDimensionPixelSize(R.dimen.fastscroll_thumb_height);
- mPreviewHeight = res.getDimensionPixelSize(R.dimen.fastscroll_preview_height);
- mPreviewMinWidth = res.getDimensionPixelSize(R.dimen.fastscroll_preview_min_width);
- mPreviewMarginTop = res.getDimensionPixelOffset(R.dimen.fastscroll_preview_margin_top);
- mPreviewMarginLeftRight = res.getDimensionPixelOffset(
- R.dimen.fastscroll_preview_margin_left_right);
- mTouchSlop = res.getDimensionPixelOffset(R.dimen.fastscroll_touch_slop);
-
- final LayoutInflater inflator = LayoutInflater.from(mContext);
- mTrackImageView = (ImageView) inflator.inflate(R.layout.fastscroll_track, null);
- mThumbImageView = (ImageView) inflator.inflate(R.layout.fastscroll_thumb, null);
- mPreviewTextView = (TextView) inflator.inflate(R.layout.fastscroll_preview, null);
-
- refreshConversationThemeColor();
-
- // Add the fast scroll views to the overlay, so they are rendered above the list
- mOverlay = rv.getOverlay();
- mOverlay.add(mTrackImageView);
- mOverlay.add(mThumbImageView);
- mOverlay.add(mPreviewTextView);
-
- hide(false /* animate */);
- mPreviewTextView.setAlpha(0f);
- }
-
- public void refreshConversationThemeColor() {
- mPreviewTextView.setBackground(
- ConversationDrawables.get().getFastScrollPreviewDrawable(mPosRight));
- if (OsUtil.isAtLeastL()) {
- final StateListDrawable drawable = new StateListDrawable();
- drawable.addState(new int[]{ android.R.attr.state_pressed },
- ConversationDrawables.get().getFastScrollThumbDrawable(true /* pressed */));
- drawable.addState(StateSet.WILD_CARD,
- ConversationDrawables.get().getFastScrollThumbDrawable(false /* pressed */));
- mThumbImageView.setImageDrawable(drawable);
- } else {
- // Android pre-L doesn't seem to handle a StateListDrawable containing a tinted
- // drawable (it's rendered in the filter base color, which is red), so fall back to
- // just the regular (non-pressed) drawable.
- mThumbImageView.setImageDrawable(
- ConversationDrawables.get().getFastScrollThumbDrawable(false /* pressed */));
- }
- }
-
- @Override
- public void onScrollStateChanged(final RecyclerView view, final int newState) {
- if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
- // Only show the scrollbar once the user starts scrolling
- if (!mVisible && isEnabled()) {
- show();
- }
- cancelAnyPendingHide();
- } else if (newState == RecyclerView.SCROLL_STATE_IDLE && !mDragging) {
- // Hide the scrollbar again after scrolling stops
- hideAfterDelay();
- }
- }
-
- private boolean isEnabled() {
- final int range = mRv.computeVerticalScrollRange();
- final int extent = mRv.computeVerticalScrollExtent();
-
- if (range == 0 || extent == 0) {
- return false; // Conversation isn't long enough to scroll
- }
- // Only enable scrollbars for conversations long enough that they would require several
- // flings to scroll through.
- final float pages = (float) range / extent;
- return (pages > MIN_PAGES_TO_ENABLE);
- }
-
- private void show() {
- if (mHideAnimation != null && mHideAnimation.isRunning()) {
- mHideAnimation.cancel();
- }
- // Slide the scrollbar in from the side
- ObjectAnimator trackSlide = ObjectAnimator.ofFloat(mTrackImageView, View.TRANSLATION_X, 0);
- ObjectAnimator thumbSlide = ObjectAnimator.ofFloat(mThumbImageView, View.TRANSLATION_X, 0);
- AnimatorSet animation = new AnimatorSet();
- animation.playTogether(trackSlide, thumbSlide);
- animation.setDuration(SHOW_ANIMATION_DURATION_MS);
- animation.start();
-
- mVisible = true;
- updateScrollPos();
- }
-
- private void hideAfterDelay() {
- cancelAnyPendingHide();
- mHandler.postDelayed(mHideTrackRunnable, HIDE_DELAY_MS);
- mPendingHide = true;
- }
-
- private void cancelAnyPendingHide() {
- if (mPendingHide) {
- mHandler.removeCallbacks(mHideTrackRunnable);
- }
- }
-
- private void hide(boolean animate) {
- final int hiddenTranslationX = mPosRight ? mTrackWidth : -mTrackWidth;
- if (animate) {
- // Slide the scrollbar off to the side
- ObjectAnimator trackSlide = ObjectAnimator.ofFloat(mTrackImageView, View.TRANSLATION_X,
- hiddenTranslationX);
- ObjectAnimator thumbSlide = ObjectAnimator.ofFloat(mThumbImageView, View.TRANSLATION_X,
- hiddenTranslationX);
- mHideAnimation = new AnimatorSet();
- mHideAnimation.playTogether(trackSlide, thumbSlide);
- mHideAnimation.setDuration(HIDE_ANIMATION_DURATION_MS);
- mHideAnimation.start();
- } else {
- mTrackImageView.setTranslationX(hiddenTranslationX);
- mThumbImageView.setTranslationX(hiddenTranslationX);
- }
-
- mVisible = false;
- }
-
- private void showPreview() {
- if (mHidePreviewAnimation != null && mHidePreviewAnimation.isRunning()) {
- mHidePreviewAnimation.cancel();
- }
- mPreviewTextView.setAlpha(1f);
- }
-
- private void hidePreview() {
- mHidePreviewAnimation = ObjectAnimator.ofFloat(mPreviewTextView, View.ALPHA, 0f);
- mHidePreviewAnimation.setDuration(HIDE_ANIMATION_DURATION_MS);
- mHidePreviewAnimation.start();
- }
-
- @Override
- public void onScrolled(final RecyclerView view, final int dx, final int dy) {
- updateScrollPos();
- }
-
- private void updateScrollPos() {
- if (!mVisible) {
- return;
- }
- final int verticalScrollLength = mContainer.height() - mThumbHeight;
- final int verticalScrollStart = mContainer.top + mThumbHeight / 2;
-
- final float scrollRatio = computeScrollRatio();
- final int thumbCenterY = verticalScrollStart + (int)(verticalScrollLength * scrollRatio);
- layoutThumb(thumbCenterY);
-
- if (mDragging) {
- updatePreviewText();
- layoutPreview(thumbCenterY);
- }
- }
-
- /**
- * Returns the current position in the conversation, as a value between 0 and 1, inclusive.
- * The top of the conversation is 0, the bottom is 1, the exact middle is 0.5, and so on.
- */
- private float computeScrollRatio() {
- final int range = mRv.computeVerticalScrollRange();
- final int extent = mRv.computeVerticalScrollExtent();
- int offset = mRv.computeVerticalScrollOffset();
-
- if (range == 0 || extent == 0) {
- // If the conversation doesn't scroll, we're at the bottom.
- return 1.0f;
- }
- final int scrollRange = range - extent;
- offset = Math.min(offset, scrollRange);
- return offset / (float) scrollRange;
- }
-
- private void updatePreviewText() {
- final LinearLayoutManager lm = (LinearLayoutManager) mRv.getLayoutManager();
- final int pos = lm.findFirstVisibleItemPosition();
- if (pos == RecyclerView.NO_POSITION) {
- return;
- }
- final ViewHolder vh = mRv.findViewHolderForAdapterPosition(pos);
- if (vh == null) {
- // This can happen if the messages update while we're dragging the thumb.
- return;
- }
- final ConversationMessageView messageView = (ConversationMessageView) vh.itemView;
- final ConversationMessageData messageData = messageView.getData();
- final long timestamp = messageData.getReceivedTimeStamp();
- final CharSequence timestampText = Dates.getFastScrollPreviewTimeString(timestamp);
- mPreviewTextView.setText(timestampText);
- }
-
- @Override
- public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
- if (!mVisible) {
- return false;
- }
- // If the user presses down on the scroll thumb, we'll start intercepting events from the
- // RecyclerView so we can handle the move events while they're dragging it up/down.
- final int action = e.getActionMasked();
- switch (action) {
- case MotionEvent.ACTION_DOWN:
- if (isInsideThumb(e.getX(), e.getY())) {
- startDrag();
- return true;
- }
- break;
- case MotionEvent.ACTION_MOVE:
- if (mDragging) {
- return true;
- }
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- if (mDragging) {
- cancelDrag();
- }
- return false;
- }
- return false;
- }
-
- private boolean isInsideThumb(float x, float y) {
- final int hitTargetLeft = mThumbImageView.getLeft() - mTouchSlop;
- final int hitTargetRight = mThumbImageView.getRight() + mTouchSlop;
-
- if (x < hitTargetLeft || x > hitTargetRight) {
- return false;
- }
- if (y < mThumbImageView.getTop() || y > mThumbImageView.getBottom()) {
- return false;
- }
- return true;
- }
-
- @Override
- public void onTouchEvent(RecyclerView rv, MotionEvent e) {
- if (!mDragging) {
- return;
- }
- final int action = e.getActionMasked();
- switch (action) {
- case MotionEvent.ACTION_MOVE:
- handleDragMove(e.getY());
- break;
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- cancelDrag();
- break;
- }
- }
-
- @Override
- public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
- }
-
- private void startDrag() {
- mDragging = true;
- mThumbImageView.setPressed(true);
- updateScrollPos();
- showPreview();
- cancelAnyPendingHide();
- }
-
- private void handleDragMove(float y) {
- final int verticalScrollLength = mContainer.height() - mThumbHeight;
- final int verticalScrollStart = mContainer.top + (mThumbHeight / 2);
-
- // Convert the desired position from px to a scroll position in the conversation.
- float dragScrollRatio = (y - verticalScrollStart) / verticalScrollLength;
- dragScrollRatio = Math.max(dragScrollRatio, 0.0f);
- dragScrollRatio = Math.min(dragScrollRatio, 1.0f);
-
- // Scroll the RecyclerView to a new position.
- final int itemCount = mRv.getAdapter().getItemCount();
- final int itemPos = (int)((itemCount - 1) * dragScrollRatio);
- mRv.scrollToPosition(itemPos);
- }
-
- private void cancelDrag() {
- mDragging = false;
- mThumbImageView.setPressed(false);
- hidePreview();
- hideAfterDelay();
- }
-
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom,
- int oldLeft, int oldTop, int oldRight, int oldBottom) {
- if (!mVisible) {
- hide(false /* animate */);
- }
- // The container is the size of the RecyclerView that's visible on screen. We have to
- // exclude the top padding, because it's usually hidden behind the conversation action bar.
- mContainer.set(left, top + mRv.getPaddingTop(), right, bottom);
- layoutTrack();
- updateScrollPos();
- }
-
- private void layoutTrack() {
- int trackHeight = Math.max(0, mContainer.height());
- int widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTrackWidth, MeasureSpec.EXACTLY);
- int heightMeasureSpec = MeasureSpec.makeMeasureSpec(trackHeight, MeasureSpec.EXACTLY);
- mTrackImageView.measure(widthMeasureSpec, heightMeasureSpec);
-
- int left = mPosRight ? (mContainer.right - mTrackWidth) : mContainer.left;
- int top = mContainer.top;
- int right = mPosRight ? mContainer.right : (mContainer.left + mTrackWidth);
- int bottom = mContainer.bottom;
- mTrackImageView.layout(left, top, right, bottom);
- }
-
- private void layoutThumb(int centerY) {
- int widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTrackWidth, MeasureSpec.EXACTLY);
- int heightMeasureSpec = MeasureSpec.makeMeasureSpec(mThumbHeight, MeasureSpec.EXACTLY);
- mThumbImageView.measure(widthMeasureSpec, heightMeasureSpec);
-
- int left = mPosRight ? (mContainer.right - mTrackWidth) : mContainer.left;
- int top = centerY - (mThumbImageView.getHeight() / 2);
- int right = mPosRight ? mContainer.right : (mContainer.left + mTrackWidth);
- int bottom = top + mThumbHeight;
- mThumbImageView.layout(left, top, right, bottom);
- }
-
- private void layoutPreview(int centerY) {
- int widthMeasureSpec = MeasureSpec.makeMeasureSpec(mContainer.width(), MeasureSpec.AT_MOST);
- int heightMeasureSpec = MeasureSpec.makeMeasureSpec(mPreviewHeight, MeasureSpec.EXACTLY);
- mPreviewTextView.measure(widthMeasureSpec, heightMeasureSpec);
-
- // Ensure that the preview bubble is at least as wide as it is tall
- if (mPreviewTextView.getMeasuredWidth() < mPreviewMinWidth) {
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(mPreviewMinWidth, MeasureSpec.EXACTLY);
- mPreviewTextView.measure(widthMeasureSpec, heightMeasureSpec);
- }
- final int previewMinY = mContainer.top + mPreviewMarginTop;
-
- final int left, right;
- if (mPosRight) {
- right = mContainer.right - mTrackWidth - mPreviewMarginLeftRight;
- left = right - mPreviewTextView.getMeasuredWidth();
- } else {
- left = mContainer.left + mTrackWidth + mPreviewMarginLeftRight;
- right = left + mPreviewTextView.getMeasuredWidth();
- }
-
- int bottom = centerY;
- int top = bottom - mPreviewTextView.getMeasuredHeight();
- if (top < previewMinY) {
- top = previewMinY;
- bottom = top + mPreviewTextView.getMeasuredHeight();
- }
- mPreviewTextView.layout(left, top, right, bottom);
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/ConversationFragment.java b/src/com/android/messaging/ui/conversation/ConversationFragment.java
deleted file mode 100644
index a6a191a..0000000
--- a/src/com/android/messaging/ui/conversation/ConversationFragment.java
+++ /dev/null
@@ -1,1662 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.messaging.ui.conversation;
-
-import android.Manifest;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.DownloadManager;
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.FragmentTransaction;
-import android.content.BroadcastReceiver;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.DialogInterface.OnCancelListener;
-import android.content.DialogInterface.OnClickListener;
-import android.content.DialogInterface.OnDismissListener;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.res.Configuration;
-import android.database.Cursor;
-import android.graphics.Point;
-import android.graphics.Rect;
-import android.graphics.drawable.ColorDrawable;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.Handler;
-import android.os.Parcelable;
-import android.support.v4.content.LocalBroadcastManager;
-import android.support.v4.text.BidiFormatter;
-import android.support.v4.text.TextDirectionHeuristicsCompat;
-import android.support.v7.app.ActionBar;
-import android.support.v7.widget.DefaultItemAnimator;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.ViewHolder;
-import android.text.TextUtils;
-import android.view.ActionMode;
-import android.view.Display;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import com.android.messaging.R;
-import com.android.messaging.datamodel.DataModel;
-import com.android.messaging.datamodel.MessagingContentProvider;
-import com.android.messaging.datamodel.action.InsertNewMessageAction;
-import com.android.messaging.datamodel.binding.Binding;
-import com.android.messaging.datamodel.binding.BindingBase;
-import com.android.messaging.datamodel.binding.ImmutableBindingRef;
-import com.android.messaging.datamodel.data.ConversationData;
-import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
-import com.android.messaging.datamodel.data.ConversationMessageData;
-import com.android.messaging.datamodel.data.ConversationParticipantsData;
-import com.android.messaging.datamodel.data.DraftMessageData;
-import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener;
-import com.android.messaging.datamodel.data.MessageData;
-import com.android.messaging.datamodel.data.MessagePartData;
-import com.android.messaging.datamodel.data.ParticipantData;
-import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
-import com.android.messaging.ui.AttachmentPreview;
-import com.android.messaging.ui.BugleActionBarActivity;
-import com.android.messaging.ui.ConversationDrawables;
-import com.android.messaging.ui.SnackBar;
-import com.android.messaging.ui.UIIntents;
-import com.android.messaging.ui.animation.PopupTransitionAnimation;
-import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
-import com.android.messaging.ui.conversation.ComposeMessageView.IComposeMessageViewHost;
-import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputHost;
-import com.android.messaging.ui.conversation.ConversationMessageView.ConversationMessageViewHost;
-import com.android.messaging.ui.mediapicker.MediaPicker;
-import com.android.messaging.util.AccessibilityUtil;
-import com.android.messaging.util.Assert;
-import com.android.messaging.util.AvatarUriUtil;
-import com.android.messaging.util.ChangeDefaultSmsAppHelper;
-import com.android.messaging.util.ContentType;
-import com.android.messaging.util.ImeUtil;
-import com.android.messaging.util.LogUtil;
-import com.android.messaging.util.OsUtil;
-import com.android.messaging.util.PhoneUtils;
-import com.android.messaging.util.SafeAsyncTask;
-import com.android.messaging.util.TextUtil;
-import com.android.messaging.util.UiUtils;
-import com.android.messaging.util.UriUtil;
-import com.google.common.annotations.VisibleForTesting;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Shows a list of messages/parts comprising a conversation.
- */
-public class ConversationFragment extends Fragment implements ConversationDataListener,
- IComposeMessageViewHost, ConversationMessageViewHost, ConversationInputHost,
- DraftMessageDataListener {
-
- public interface ConversationFragmentHost extends ImeUtil.ImeStateHost {
- void onStartComposeMessage();
- void onConversationMetadataUpdated();
- boolean shouldResumeComposeMessage();
- void onFinishCurrentConversation();
- void invalidateActionBar();
- ActionMode startActionMode(ActionMode.Callback callback);
- void dismissActionMode();
- ActionMode getActionMode();
- void onConversationMessagesUpdated(int numberOfMessages);
- void onConversationParticipantDataLoaded(int numberOfParticipants);
- boolean isActiveAndFocused();
- }
-
- public static final String FRAGMENT_TAG = "conversation";
-
- static final int REQUEST_CHOOSE_ATTACHMENTS = 2;
- private static final int JUMP_SCROLL_THRESHOLD = 15;
- // We animate the message from draft to message list, if we the message doesn't show up in the
- // list within this time limit, then we just do a fade in animation instead
- public static final int MESSAGE_ANIMATION_MAX_WAIT = 500;
-
- private ComposeMessageView mComposeMessageView;
- private RecyclerView mRecyclerView;
- private ConversationMessageAdapter mAdapter;
- private ConversationFastScroller mFastScroller;
-
- private View mConversationComposeDivider;
- private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
-
- private String mConversationId;
- // If the fragment receives a draft as part of the invocation this is set
- private MessageData mIncomingDraft;
-
- // This binding keeps track of our associated ConversationData instance
- // A binding should have the lifetime of the owning component,
- // don't recreate, unbind and bind if you need new data
- @VisibleForTesting
- final Binding<ConversationData> mBinding = BindingBase.createBinding(this);
-
- // Saved Instance State Data - only for temporal data which is nice to maintain but not
- // critical for correctness.
- private static final String SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY = "conversationViewState";
- private Parcelable mListState;
-
- private ConversationFragmentHost mHost;
-
- protected List<Integer> mFilterResults;
-
- // The minimum scrolling distance between RecyclerView's scroll change event beyong which
- // a fling motion is considered fast, in which case we'll delay load image attachments for
- // perf optimization.
- private int mFastFlingThreshold;
-
- // ConversationMessageView that is currently selected
- private ConversationMessageView mSelectedMessage;
-
- // Attachment data for the attachment within the selected message that was long pressed
- private MessagePartData mSelectedAttachment;
-
- // Normally, as soon as draft message is loaded, we trust the UI state held in
- // ComposeMessageView to be the only source of truth (incl. the conversation self id). However,
- // there can be external events that forces the UI state to change, such as SIM state changes
- // or SIM auto-switching on receiving a message. This receiver is used to receive such
- // local broadcast messages and reflect the change in the UI.
- private final BroadcastReceiver mConversationSelfIdChangeReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(final Context context, final Intent intent) {
- final String conversationId =
- intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID);
- final String selfId =
- intent.getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_SELF_ID);
- Assert.notNull(conversationId);
- Assert.notNull(selfId);
- if (TextUtils.equals(mBinding.getData().getConversationId(), conversationId)) {
- mComposeMessageView.updateConversationSelfIdOnExternalChange(selfId);
- }
- }
- };
-
- // Flag to prevent writing draft to DB on pause
- private boolean mSuppressWriteDraft;
-
- // Indicates whether local draft should be cleared due to external draft changes that must
- // be reloaded from db
- private boolean mClearLocalDraft;
- private ImmutableBindingRef<DraftMessageData> mDraftMessageDataModel;
-
- private boolean isScrolledToBottom() {
- if (mRecyclerView.getChildCount() == 0) {
- return true;
- }
- final View lastView = mRecyclerView.getChildAt(mRecyclerView.getChildCount() - 1);
- int lastVisibleItem = ((LinearLayoutManager) mRecyclerView
- .getLayoutManager()).findLastVisibleItemPosition();
- if (lastVisibleItem < 0) {
- // If the recyclerView height is 0, then the last visible item position is -1
- // Try to compute the position of the last item, even though it's not visible
- final long id = mRecyclerView.getChildItemId(lastView);
- final RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForItemId(id);
- if (holder != null) {
- lastVisibleItem = holder.getAdapterPosition();
- }
- }
- final int totalItemCount = mRecyclerView.getAdapter().getItemCount();
- final boolean isAtBottom = (lastVisibleItem + 1 == totalItemCount);
- return isAtBottom && lastView.getBottom() <= mRecyclerView.getHeight();
- }
-
- private void scrollToBottom(final boolean smoothScroll) {
- if (mAdapter.getItemCount() > 0) {
- scrollToPosition(mAdapter.getItemCount() - 1, smoothScroll);
- }
- }
-
- private int mScrollToDismissThreshold;
- private final RecyclerView.OnScrollListener mListScrollListener =
- new RecyclerView.OnScrollListener() {
- // Keeps track of cumulative scroll delta during a scroll event, which we may use to
- // hide the media picker & co.
- private int mCumulativeScrollDelta;
- private boolean mScrollToDismissHandled;
- private boolean mWasScrolledToBottom = true;
- private int mScrollState = RecyclerView.SCROLL_STATE_IDLE;
-
- @Override
- public void onScrollStateChanged(final RecyclerView view, final int newState) {
- if (newState == RecyclerView.SCROLL_STATE_IDLE) {
- // Reset scroll states.
- mCumulativeScrollDelta = 0;
- mScrollToDismissHandled = false;
- } else if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
- mRecyclerView.getItemAnimator().endAnimations();
- }
- mScrollState = newState;
- }
-
- @Override
- public void onScrolled(final RecyclerView view, final int dx, final int dy) {
- if (mScrollState == RecyclerView.SCROLL_STATE_DRAGGING &&
- !mScrollToDismissHandled) {
- mCumulativeScrollDelta += dy;
- // Dismiss the keyboard only when the user scroll up (into the past).
- if (mCumulativeScrollDelta < -mScrollToDismissThreshold) {
- mComposeMessageView.hideAllComposeInputs(false /* animate */);
- mScrollToDismissHandled = true;
- }
- }
- if (mWasScrolledToBottom != isScrolledToBottom()) {
- mConversationComposeDivider.animate().alpha(isScrolledToBottom() ? 0 : 1);
- mWasScrolledToBottom = isScrolledToBottom();
- }
- }
- };
-
- private final ActionMode.Callback mMessageActionModeCallback = new ActionMode.Callback() {
- @Override
- public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) {
- if (mSelectedMessage == null) {
- return false;
- }
- final ConversationMessageData data = mSelectedMessage.getData();
- final MenuInflater menuInflater = getActivity().getMenuInflater();
- menuInflater.inflate(R.menu.conversation_fragment_select_menu, menu);
- menu.findItem(R.id.action_download).setVisible(data.getShowDownloadMessage());
- menu.findItem(R.id.action_send).setVisible(data.getShowResendMessage());
-
- // ShareActionProvider does not work with ActionMode. So we use a normal menu item.
- menu.findItem(R.id.share_message_menu).setVisible(data.getCanForwardMessage());
- menu.findItem(R.id.save_attachment).setVisible(mSelectedAttachment != null);
- menu.findItem(R.id.forward_message_menu).setVisible(data.getCanForwardMessage());
-
- // TODO: We may want to support copying attachments in the future, but it's
- // unclear which attachment to pick when we make this context menu at the message level
- // instead of the part level
- menu.findItem(R.id.copy_text).setVisible(data.getCanCopyMessageToClipboard());
-
- return true;
- }
-
- @Override
- public boolean onPrepareActionMode(final ActionMode actionMode, final Menu menu) {
- return true;
- }
-
- @Override
- public boolean onActionItemClicked(final ActionMode actionMode, final MenuItem menuItem) {
- final ConversationMessageData data = mSelectedMessage.getData();
- final String messageId = data.getMessageId();
- switch (menuItem.getItemId()) {
- case R.id.save_attachment:
- if (OsUtil.hasStoragePermission()) {
- final SaveAttachmentTask saveAttachmentTask = new SaveAttachmentTask(
- getActivity());
- for (final MessagePartData part : data.getAttachments()) {
- saveAttachmentTask.addAttachmentToSave(part.getContentUri(),
- part.getContentType());
- }
- if (saveAttachmentTask.getAttachmentCount() > 0) {
- saveAttachmentTask.executeOnThreadPool();
- mHost.dismissActionMode();
- }
- } else {
- getActivity().requestPermissions(
- new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, 0);
- }
- return true;
- case R.id.action_delete_message:
- if (mSelectedMessage != null) {
- deleteMessage(messageId);
- }
- return true;
- case R.id.action_download:
- if (mSelectedMessage != null) {
- retryDownload(messageId);
- mHost.dismissActionMode();
- }
- return true;
- case R.id.action_send:
- if (mSelectedMessage != null) {
- retrySend(messageId);
- mHost.dismissActionMode();
- }
- return true;
- case R.id.copy_text:
- Assert.isTrue(data.hasText());
- final ClipboardManager clipboard = (ClipboardManager) getActivity()
- .getSystemService(Context.CLIPBOARD_SERVICE);
- clipboard.setPrimaryClip(
- ClipData.newPlainText(null /* label */, data.getText()));
- mHost.dismissActionMode();
- return true;
- case R.id.details_menu:
- MessageDetailsDialog.show(
- getActivity(), data, mBinding.getData().getParticipants(),
- mBinding.getData().getSelfParticipantById(data.getSelfParticipantId()));
- mHost.dismissActionMode();
- return true;
- case R.id.share_message_menu:
- shareMessage(data);
- mHost.dismissActionMode();
- return true;
- case R.id.forward_message_menu:
- // TODO: Currently we are forwarding one part at a time, instead of
- // the entire message. Change this to forwarding the entire message when we
- // use message-based cursor in conversation.
- final MessageData message = mBinding.getData().createForwardedMessage(data);
- UIIntents.get().launchForwardMessageActivity(getActivity(), message);
- mHost.dismissActionMode();
- return true;
- }
- return false;
- }
-
- private void shareMessage(final ConversationMessageData data) {
- // Figure out what to share.
- MessagePartData attachmentToShare = mSelectedAttachment;
- // If the user long-pressed on the background, we will share the text (if any)
- // or the first attachment.
- if (mSelectedAttachment == null
- && TextUtil.isAllWhitespace(data.getText())) {
- final List<MessagePartData> attachments = data.getAttachments();
- if (attachments.size() > 0) {
- attachmentToShare = attachments.get(0);
- }
- }
-
- final Intent shareIntent = new Intent();
- shareIntent.setAction(Intent.ACTION_SEND);
- if (attachmentToShare == null) {
- shareIntent.putExtra(Intent.EXTRA_TEXT, data.getText());
- shareIntent.setType("text/plain");
- } else {
- shareIntent.putExtra(
- Intent.EXTRA_STREAM, attachmentToShare.getContentUri());
- shareIntent.setType(attachmentToShare.getContentType());
- }
- final CharSequence title = getResources().getText(R.string.action_share);
- startActivity(Intent.createChooser(shareIntent, title));
- }
-
- @Override
- public void onDestroyActionMode(final ActionMode actionMode) {
- selectMessage(null);
- }
- };
-
- /**
- * {@inheritDoc} from Fragment
- */
- @Override
- public void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- mFastFlingThreshold = getResources().getDimensionPixelOffset(
- R.dimen.conversation_fast_fling_threshold);
- mAdapter = new ConversationMessageAdapter(getActivity(), null, this,
- null,
- // Sets the item click listener on the Recycler item views.
- new View.OnClickListener() {
- @Override
- public void onClick(final View v) {
- final ConversationMessageView messageView = (ConversationMessageView) v;
- handleMessageClick(messageView);
- }
- },
- new View.OnLongClickListener() {
- @Override
- public boolean onLongClick(final View view) {
- selectMessage((ConversationMessageView) view);
- return true;
- }
- }
- );
- }
-
- /**
- * setConversationInfo() may be called before or after onCreate(). When a user initiate a
- * conversation from compose, the ConversationActivity creates this fragment and calls
- * setConversationInfo(), so it happens before onCreate(). However, when the activity is
- * restored from saved instance state, the ConversationFragment is created automatically by
- * the fragment, before ConversationActivity has a chance to call setConversationInfo(). Since
- * the ability to start loading data depends on both methods being called, we need to start
- * loading when onActivityCreated() is called, which is guaranteed to happen after both.
- */
- @Override
- public void onActivityCreated(final Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- // Delay showing the message list until the participant list is loaded.
- mRecyclerView.setVisibility(View.INVISIBLE);
- mBinding.ensureBound();
- mBinding.getData().init(getLoaderManager(), mBinding);
-
- // Build the input manager with all its required dependencies and pass it along to the
- // compose message view.
- final ConversationInputManager inputManager = new ConversationInputManager(
- getActivity(), this, mComposeMessageView, mHost, getFragmentManagerToUse(),
- mBinding, mComposeMessageView.getDraftDataModel(), savedInstanceState);
- mComposeMessageView.setInputManager(inputManager);
- mComposeMessageView.setConversationDataModel(BindingBase.createBindingReference(mBinding));
- mHost.invalidateActionBar();
-
- mDraftMessageDataModel =
- BindingBase.createBindingReference(mComposeMessageView.getDraftDataModel());
- mDraftMessageDataModel.getData().addListener(this);
- }
-
- public void onAttachmentChoosen() {
- // Attachment has been choosen in the AttachmentChooserActivity, so clear local draft
- // and reload draft on resume.
- mClearLocalDraft = true;
- }
-
- private int getScrollToMessagePosition() {
- final Activity activity = getActivity();
- if (activity == null) {
- return -1;
- }
-
- final Intent intent = activity.getIntent();
- if (intent == null) {
- return -1;
- }
-
- return intent.getIntExtra(UIIntents.UI_INTENT_EXTRA_MESSAGE_POSITION, -1);
- }
-
- private void clearScrollToMessagePosition() {
- final Activity activity = getActivity();
- if (activity == null) {
- return;
- }
-
- final Intent intent = activity.getIntent();
- if (intent == null) {
- return;
- }
- intent.putExtra(UIIntents.UI_INTENT_EXTRA_MESSAGE_POSITION, -1);
- }
-
- private final Handler mHandler = new Handler();
-
- /**
- * {@inheritDoc} from Fragment
- */
- @Override
- public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
- final Bundle savedInstanceState) {
- final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
- mRecyclerView = (RecyclerView) view.findViewById(android.R.id.list);
- final LinearLayoutManager manager = new LinearLayoutManager(getActivity());
- manager.setStackFromEnd(true);
- manager.setReverseLayout(false);
- mRecyclerView.setHasFixedSize(true);
- mRecyclerView.setLayoutManager(manager);
- mRecyclerView.setItemAnimator(new DefaultItemAnimator() {
- private final List<ViewHolder> mAddAnimations = new ArrayList<ViewHolder>();
- private PopupTransitionAnimation mPopupTransitionAnimation;
-
- @Override
- public boolean animateAdd(final ViewHolder holder) {
- final ConversationMessageView view =
- (ConversationMessageView) holder.itemView;
- final ConversationMessageData data = view.getData();
- endAnimation(holder);
- final long timeSinceSend = System.currentTimeMillis() - data.getReceivedTimeStamp();
- if (data.getReceivedTimeStamp() ==
- InsertNewMessageAction.getLastSentMessageTimestamp() &&
- !data.getIsIncoming() &&
- timeSinceSend < MESSAGE_ANIMATION_MAX_WAIT) {
- final ConversationMessageBubbleView messageBubble =
- (ConversationMessageBubbleView) view
- .findViewById(R.id.message_content);
- final Rect startRect = UiUtils.getMeasuredBoundsOnScreen(mComposeMessageView);
- final View composeBubbleView = mComposeMessageView.findViewById(
- R.id.compose_message_text);
- final Rect composeBubbleRect =
- UiUtils.getMeasuredBoundsOnScreen(composeBubbleView);
- final AttachmentPreview attachmentView =
- (AttachmentPreview) mComposeMessageView.findViewById(
- R.id.attachment_draft_view);
- final Rect attachmentRect = UiUtils.getMeasuredBoundsOnScreen(attachmentView);
- if (attachmentView.getVisibility() == View.VISIBLE) {
- startRect.top = attachmentRect.top;
- } else {
- startRect.top = composeBubbleRect.top;
- }
- startRect.top -= view.getPaddingTop();
- startRect.bottom =
- composeBubbleRect.bottom;
- startRect.left += view.getPaddingRight();
-
- view.setAlpha(0);
- mPopupTransitionAnimation = new PopupTransitionAnimation(startRect, view);
- mPopupTransitionAnimation.setOnStartCallback(new Runnable() {
- @Override
- public void run() {
- final int startWidth = composeBubbleRect.width();
- attachmentView.onMessageAnimationStart();
- messageBubble.kickOffMorphAnimation(startWidth,
- messageBubble.findViewById(R.id.message_text_and_info)
- .getMeasuredWidth());
- }
- });
- mPopupTransitionAnimation.setOnStopCallback(new Runnable() {
- @Override
- public void run() {
- view.setAlpha(1);
- }
- });
- mPopupTransitionAnimation.startAfterLayoutComplete();
- mAddAnimations.add(holder);
- return true;
- } else {
- return super.animateAdd(holder);
- }
- }
-
- @Override
- public void endAnimation(final ViewHolder holder) {
- if (mAddAnimations.remove(holder)) {
- holder.itemView.clearAnimation();
- }
- super.endAnimation(holder);
- }
-
- @Override
- public void endAnimations() {
- for (final ViewHolder holder : mAddAnimations) {
- holder.itemView.clearAnimation();
- }
- mAddAnimations.clear();
- if (mPopupTransitionAnimation != null) {
- mPopupTransitionAnimation.cancel();
- }
- super.endAnimations();
- }
- });
- mRecyclerView.setAdapter(mAdapter);
-
- if (savedInstanceState != null) {
- mListState = savedInstanceState.getParcelable(SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY);
- }
-
- mConversationComposeDivider = view.findViewById(R.id.conversation_compose_divider);
- mScrollToDismissThreshold = ViewConfiguration.get(getActivity()).getScaledTouchSlop();
- mRecyclerView.addOnScrollListener(mListScrollListener);
- mFastScroller = ConversationFastScroller.addTo(mRecyclerView,
- UiUtils.isRtlMode() ? ConversationFastScroller.POSITION_LEFT_SIDE :
- ConversationFastScroller.POSITION_RIGHT_SIDE);
-
- mComposeMessageView = (ComposeMessageView)
- view.findViewById(R.id.message_compose_view_container);
- // Bind the compose message view to the DraftMessageData
- mComposeMessageView.bind(DataModel.get().createDraftMessageData(
- mBinding.getData().getConversationId()), this);
-
- return view;
- }
-
- private void scrollToPosition(final int targetPosition, final boolean smoothScroll) {
- if (smoothScroll) {
- final int maxScrollDelta = JUMP_SCROLL_THRESHOLD;
-
- final LinearLayoutManager layoutManager =
- (LinearLayoutManager) mRecyclerView.getLayoutManager();
- final int firstVisibleItemPosition =
- layoutManager.findFirstVisibleItemPosition();
- final int delta = targetPosition - firstVisibleItemPosition;
- final int intermediatePosition;
-
- if (delta > maxScrollDelta) {
- intermediatePosition = Math.max(0, targetPosition - maxScrollDelta);
- } else if (delta < -maxScrollDelta) {
- final int count = layoutManager.getItemCount();
- intermediatePosition = Math.min(count - 1, targetPosition + maxScrollDelta);
- } else {
- intermediatePosition = -1;
- }
- if (intermediatePosition != -1) {
- mRecyclerView.scrollToPosition(intermediatePosition);
- }
- mRecyclerView.smoothScrollToPosition(targetPosition);
- } else {
- mRecyclerView.scrollToPosition(targetPosition);
- }
- }
-
- private int getScrollPositionFromBottom() {
- final LinearLayoutManager layoutManager =
- (LinearLayoutManager) mRecyclerView.getLayoutManager();
- final int lastVisibleItem =
- layoutManager.findLastVisibleItemPosition();
- return Math.max(mAdapter.getItemCount() - 1 - lastVisibleItem, 0);
- }
-
- /**
- * Display a photo using the Photoviewer component.
- */
- @Override
- public void displayPhoto(final Uri photoUri, final Rect imageBounds, final boolean isDraft) {
- displayPhoto(photoUri, imageBounds, isDraft, mConversationId, getActivity());
- }
-
- public static void displayPhoto(final Uri photoUri, final Rect imageBounds,
- final boolean isDraft, final String conversationId, final Activity activity) {
- final Uri imagesUri =
- isDraft ? MessagingContentProvider.buildDraftImagesUri(conversationId)
- : MessagingContentProvider.buildConversationImagesUri(conversationId);
- UIIntents.get().launchFullScreenPhotoViewer(
- activity, photoUri, imageBounds, imagesUri);
- }
-
- private void selectMessage(final ConversationMessageView messageView) {
- selectMessage(messageView, null /* attachment */);
- }
-
- private void selectMessage(final ConversationMessageView messageView,
- final MessagePartData attachment) {
- mSelectedMessage = messageView;
- if (mSelectedMessage == null) {
- mAdapter.setSelectedMessage(null);
- mHost.dismissActionMode();
- mSelectedAttachment = null;
- return;
- }
- mSelectedAttachment = attachment;
- mAdapter.setSelectedMessage(messageView.getData().getMessageId());
- mHost.startActionMode(mMessageActionModeCallback);
- }
-
- @Override
- public void onSaveInstanceState(final Bundle outState) {
- super.onSaveInstanceState(outState);
- if (mListState != null) {
- outState.putParcelable(SAVED_INSTANCE_STATE_LIST_VIEW_STATE_KEY, mListState);
- }
- mComposeMessageView.saveInputState(outState);
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- if (mIncomingDraft == null) {
- mComposeMessageView.requestDraftMessage(mClearLocalDraft);
- } else {
- mComposeMessageView.setDraftMessage(mIncomingDraft);
- mIncomingDraft = null;
- }
- mClearLocalDraft = false;
-
- // On resume, check if there's a pending request for resuming message compose. This
- // may happen when the user commits the contact selection for a group conversation and
- // goes from compose back to the conversation fragment.
- if (mHost.shouldResumeComposeMessage()) {
- mComposeMessageView.resumeComposeMessage();
- }
-
- setConversationFocus();
-
- // On resume, invalidate all message views to show the updated timestamp.
- mAdapter.notifyDataSetChanged();
-
- LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
- mConversationSelfIdChangeReceiver,
- new IntentFilter(UIIntents.CONVERSATION_SELF_ID_CHANGE_BROADCAST_ACTION));
- }
-
- void setConversationFocus() {
- if (mHost.isActiveAndFocused()) {
- mBinding.getData().setFocus();
- }
- }
-
- @Override
- public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
- if (mHost.getActionMode() != null) {
- return;
- }
-
- inflater.inflate(R.menu.conversation_menu, menu);
-
- final ConversationData data = mBinding.getData();
-
- // Disable the "people & options" item if we haven't loaded participants yet.
- menu.findItem(R.id.action_people_and_options).setEnabled(data.getParticipantsLoaded());
-
- // See if we can show add contact action.
- final ParticipantData participant = data.getOtherParticipant();
- final boolean addContactActionVisible = (participant != null
- && TextUtils.isEmpty(participant.getLookupKey()));
- menu.findItem(R.id.action_add_contact).setVisible(addContactActionVisible);
-
- // See if we should show archive or unarchive.
- final boolean isArchived = data.getIsArchived();
- menu.findItem(R.id.action_archive).setVisible(!isArchived);
- menu.findItem(R.id.action_unarchive).setVisible(isArchived);
-
- // Conditionally enable the phone call button.
- final boolean supportCallAction = (PhoneUtils.getDefault().isVoiceCapable() &&
- data.getParticipantPhoneNumber() != null);
- menu.findItem(R.id.action_call).setVisible(supportCallAction);
- }
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item) {
- switch (item.getItemId()) {
- case R.id.action_people_and_options:
- Assert.isTrue(mBinding.getData().getParticipantsLoaded());
- UIIntents.get().launchPeopleAndOptionsActivity(getActivity(), mConversationId);
- return true;
-
- case R.id.action_call:
- final String phoneNumber = mBinding.getData().getParticipantPhoneNumber();
- Assert.notNull(phoneNumber);
- final View targetView = getActivity().findViewById(R.id.action_call);
- Point centerPoint;
- if (targetView != null) {
- final int screenLocation[] = new int[2];
- targetView.getLocationOnScreen(screenLocation);
- final int centerX = screenLocation[0] + targetView.getWidth() / 2;
- final int centerY = screenLocation[1] + targetView.getHeight() / 2;
- centerPoint = new Point(centerX, centerY);
- } else {
- // In the overflow menu, just use the center of the screen.
- final Display display = getActivity().getWindowManager().getDefaultDisplay();
- centerPoint = new Point(display.getWidth() / 2, display.getHeight() / 2);
- }
- UIIntents.get().launchPhoneCallActivity(getActivity(), phoneNumber, centerPoint);
- return true;
-
- case R.id.action_archive:
- mBinding.getData().archiveConversation(mBinding);
- closeConversation(mConversationId);
- return true;
-
- case R.id.action_unarchive:
- mBinding.getData().unarchiveConversation(mBinding);
- return true;
-
- case R.id.action_settings:
- return true;
-
- case R.id.action_add_contact:
- final ParticipantData participant = mBinding.getData().getOtherParticipant();
- Assert.notNull(participant);
- final String destination = participant.getNormalizedDestination();
- final Uri avatarUri = AvatarUriUtil.createAvatarUri(participant);
- (new AddContactsConfirmationDialog(getActivity(), avatarUri, destination)).show();
- return true;
-
- case R.id.action_delete:
- if (isReadyForAction()) {
- new AlertDialog.Builder(getActivity())
- .setTitle(getResources().getQuantityString(
- R.plurals.delete_conversations_confirmation_dialog_title, 1))
- .setPositiveButton(R.string.delete_conversation_confirmation_button,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(final DialogInterface dialog,
- final int button) {
- deleteConversation();
- }
- })
- .setNegativeButton(R.string.delete_conversation_decline_button, null)
- .show();
- } else {
- warnOfMissingActionConditions(false /*sending*/,
- null /*commandToRunAfterActionConditionResolved*/);
- }
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- /**
- * {@inheritDoc} from ConversationDataListener
- */
- @Override
- public void onConversationMessagesCursorUpdated(final ConversationData data,
- final Cursor cursor, final ConversationMessageData newestMessage,
- final boolean isSync) {
- mBinding.ensureBound(data);
-
- // This needs to be determined before swapping cursor, which may change the scroll state.
- final boolean scrolledToBottom = isScrolledToBottom();
- final int positionFromBottom = getScrollPositionFromBottom();
-
- // If participants not loaded, assume 1:1 since that's the 99% case
- final boolean oneOnOne =
- !data.getParticipantsLoaded() || data.getOtherParticipant() != null;
- mAdapter.setOneOnOne(oneOnOne, false /* invalidate */);
-
- // Ensure that the action bar is updated with the current data.
- invalidateOptionsMenu();
- final Cursor oldCursor = mAdapter.swapCursor(cursor);
-
- if (cursor != null && oldCursor == null) {
- if (mListState != null) {
- mRecyclerView.getLayoutManager().onRestoreInstanceState(mListState);
- // RecyclerView restores scroll states without triggering scroll change events, so
- // we need to manually ensure that they are correctly handled.
- mListScrollListener.onScrolled(mRecyclerView, 0, 0);
- }
- }
-
- if (isSync) {
- // This is a message sync. Syncing messages changes cursor item count, which would
- // implicitly change RV's scroll position. We'd like the RV to keep scrolled to the same
- // relative position from the bottom (because RV is stacked from bottom), so that it
- // stays relatively put as we sync.
- final int position = Math.max(mAdapter.getItemCount() - 1 - positionFromBottom, 0);
- scrollToPosition(position, false /* smoothScroll */);
- } else if (newestMessage != null) {
- // Show a snack bar notification if we are not scrolled to the bottom and the new
- // message is an incoming message.
- if (!scrolledToBottom && newestMessage.getIsIncoming()) {
- // If the conversation activity is started but not resumed (if another dialog
- // activity was in the foregrond), we will show a system notification instead of
- // the snack bar.
- if (mBinding.getData().isFocused()) {
- UiUtils.showSnackBarWithCustomAction(getActivity(),
- getView().getRootView(),
- getString(R.string.in_conversation_notify_new_message_text),
- SnackBar.Action.createCustomAction(new Runnable() {
- @Override
- public void run() {
- scrollToBottom(true /* smoothScroll */);
- mComposeMessageView.hideAllComposeInputs(false /* animate */);
- }
- },
- getString(R.string.in_conversation_notify_new_message_action)),
- null /* interactions */,
- SnackBar.Placement.above(mComposeMessageView));
- }
- } else {
- // We are either already scrolled to the bottom or this is an outgoing message,
- // scroll to the bottom to reveal it.
- // Don't smooth scroll if we were already at the bottom; instead, we scroll
- // immediately so RecyclerView's view animation will take place.
- scrollToBottom(!scrolledToBottom);
- }
- }
-
- if (cursor != null) {
- mHost.onConversationMessagesUpdated(cursor.getCount());
-
- // Are we coming from a widget click where we're told to scroll to a particular item?
- final int scrollToPos = getScrollToMessagePosition();
- if (scrollToPos >= 0) {
- if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) {
- LogUtil.v(LogUtil.BUGLE_TAG, "onConversationMessagesCursorUpdated " +
- " scrollToPos: " + scrollToPos +
- " cursorCount: " + cursor.getCount());
- }
- scrollToPosition(scrollToPos, true /*smoothScroll*/);
- clearScrollToMessagePosition();
- }
- }
-
- mHost.invalidateActionBar();
- }
-
- /**
- * {@inheritDoc} from ConversationDataListener
- */
- @Override
- public void onConversationMetadataUpdated(final ConversationData conversationData) {
- mBinding.ensureBound(conversationData);
-
- if (mSelectedMessage != null && mSelectedAttachment != null) {
- // We may have just sent a message and the temp attachment we selected is now gone.
- // and it was replaced with some new attachment. Since we don't know which one it
- // is we shouldn't reselect it (unless there is just one) In the multi-attachment
- // case we would just deselect the message and allow the user to reselect, otherwise we
- // may act on old temp data and may crash.
- final List<MessagePartData> currentAttachments = mSelectedMessage.getData().getAttachments();
- if (currentAttachments.size() == 1) {
- mSelectedAttachment = currentAttachments.get(0);
- } else if (!currentAttachments.contains(mSelectedAttachment)) {
- selectMessage(null);
- }
- }
- // Ensure that the action bar is updated with the current data.
- invalidateOptionsMenu();
- mHost.onConversationMetadataUpdated();
- mAdapter.notifyDataSetChanged();
- }
-
- public void setConversationInfo(final Context context, final String conversationId,
- final MessageData draftData) {
- // TODO: Eventually I would like the Factory to implement
- // Factory.get().bindConversationData(mBinding, getActivity(), this, conversationId));
- if (!mBinding.isBound()) {
- mConversationId = conversationId;
- mIncomingDraft = draftData;
- mBinding.bind(DataModel.get().createConversationData(context, this, conversationId));
- } else {
- Assert.isTrue(TextUtils.equals(mBinding.getData().getConversationId(), conversationId));
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- // Unbind all the views that we bound to data
- if (mComposeMessageView != null) {
- mComposeMessageView.unbind();
- }
-
- // And unbind this fragment from its data
- mBinding.unbind();
- mConversationId = null;
- }
-
- void suppressWriteDraft() {
- mSuppressWriteDraft = true;
- }
-
- @Override
- public void onPause() {
- super.onPause();
- if (mComposeMessageView != null && !mSuppressWriteDraft) {
- mComposeMessageView.writeDraftMessage();
- }
- mSuppressWriteDraft = false;
- mBinding.getData().unsetFocus();
- mListState = mRecyclerView.getLayoutManager().onSaveInstanceState();
-
- LocalBroadcastManager.getInstance(getActivity())
- .unregisterReceiver(mConversationSelfIdChangeReceiver);
- }
-
- @Override
- public void onConfigurationChanged(final Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- mRecyclerView.getItemAnimator().endAnimations();
- }
-
- // TODO: Remove isBound and replace it with ensureBound after b/15704674.
- public boolean isBound() {
- return mBinding.isBound();
- }
-
- private FragmentManager getFragmentManagerToUse() {
- return OsUtil.isAtLeastJB_MR1() ? getChildFragmentManager() : getFragmentManager();
- }
-
- public MediaPicker getMediaPicker() {
- return (MediaPicker) getFragmentManagerToUse().findFragmentByTag(
- MediaPicker.FRAGMENT_TAG);
- }
-
- @Override
- public void sendMessage(final MessageData message) {
- if (isReadyForAction()) {
- if (ensureKnownRecipients()) {
- // Merge the caption text from attachments into the text body of the messages
- message.consolidateText();
-
- mBinding.getData().sendMessage(mBinding, message);
- mComposeMessageView.resetMediaPickerState();
- } else {
- LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: conv participants not loaded");
- }
- } else {
- warnOfMissingActionConditions(true /*sending*/,
- new Runnable() {
- @Override
- public void run() {
- sendMessage(message);
- }
- });
- }
- }
-
- public void setHost(final ConversationFragmentHost host) {
- mHost = host;
- }
-
- public String getConversationName() {
- return mBinding.getData().getConversationName();
- }
-
- @Override
- public void onComposeEditTextFocused() {
- mHost.onStartComposeMessage();
- }
-
- @Override
- public void onAttachmentsCleared() {
- // When attachments are removed, reset transient media picker state such as image selection.
- mComposeMessageView.resetMediaPickerState();
- }
-
- /**
- * Called to check if all conditions are nominal and a "go" for some action, such as deleting
- * a message, that requires this app to be the default app. This is also a precondition
- * required for sending a draft.
- * @return true if all conditions are nominal and we're ready to send a message
- */
- @Override
- public boolean isReadyForAction() {
- return UiUtils.isReadyForAction();
- }
-
- /**
- * When there's some condition that prevents an operation, such as sending a message,
- * call warnOfMissingActionConditions to put up a snackbar and allow the user to repair
- * that condition.
- * @param sending - true if we're called during a sending operation
- * @param commandToRunAfterActionConditionResolved - a runnable to run after the user responds
- * positively to the condition prompt and resolves the condition. If null,
- * the user will be shown a toast to tap the send button again.
- */
- @Override
- public void warnOfMissingActionConditions(final boolean sending,
- final Runnable commandToRunAfterActionConditionResolved) {
- if (mChangeDefaultSmsAppHelper == null) {
- mChangeDefaultSmsAppHelper = new ChangeDefaultSmsAppHelper();
- }
- mChangeDefaultSmsAppHelper.warnOfMissingActionConditions(sending,
- commandToRunAfterActionConditionResolved, mComposeMessageView,
- getView().getRootView(),
- getActivity(), this);
- }
-
- private boolean ensureKnownRecipients() {
- final ConversationData conversationData = mBinding.getData();
-
- if (!conversationData.getParticipantsLoaded()) {
- // We can't tell yet whether or not we have an unknown recipient
- return false;
- }
-
- final ConversationParticipantsData participants = conversationData.getParticipants();
- for (final ParticipantData participant : participants) {
-
-
- if (participant.isUnknownSender()) {
- UiUtils.showToast(R.string.unknown_sender);
- return false;
- }
- }
-
- return true;
- }
-
- public void retryDownload(final String messageId) {
- if (isReadyForAction()) {
- mBinding.getData().downloadMessage(mBinding, messageId);
- } else {
- warnOfMissingActionConditions(false /*sending*/,
- null /*commandToRunAfterActionConditionResolved*/);
- }
- }
-
- public void retrySend(final String messageId) {
- if (isReadyForAction()) {
- if (ensureKnownRecipients()) {
- mBinding.getData().resendMessage(mBinding, messageId);
- }
- } else {
- warnOfMissingActionConditions(true /*sending*/,
- new Runnable() {
- @Override
- public void run() {
- retrySend(messageId);
- }
-
- });
- }
- }
-
- void deleteMessage(final String messageId) {
- if (isReadyForAction()) {
- final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
- .setTitle(R.string.delete_message_confirmation_dialog_title)
- .setMessage(R.string.delete_message_confirmation_dialog_text)
- .setPositiveButton(R.string.delete_message_confirmation_button,
- new OnClickListener() {
- @Override
- public void onClick(final DialogInterface dialog, final int which) {
- mBinding.getData().deleteMessage(mBinding, messageId);
- }
- })
- .setNegativeButton(android.R.string.cancel, null);
- if (OsUtil.isAtLeastJB_MR1()) {
- builder.setOnDismissListener(new OnDismissListener() {
- @Override
- public void onDismiss(final DialogInterface dialog) {
- mHost.dismissActionMode();
- }
- });
- } else {
- builder.setOnCancelListener(new OnCancelListener() {
- @Override
- public void onCancel(final DialogInterface dialog) {
- mHost.dismissActionMode();
- }
- });
- }
- builder.create().show();
- } else {
- warnOfMissingActionConditions(false /*sending*/,
- null /*commandToRunAfterActionConditionResolved*/);
- mHost.dismissActionMode();
- }
- }
-
- public void deleteConversation() {
- if (isReadyForAction()) {
- final Context context = getActivity();
- mBinding.getData().deleteConversation(mBinding);
- closeConversation(mConversationId);
- } else {
- warnOfMissingActionConditions(false /*sending*/,
- null /*commandToRunAfterActionConditionResolved*/);
- }
- }
-
- @Override
- public void closeConversation(final String conversationId) {
- if (TextUtils.equals(conversationId, mConversationId)) {
- mHost.onFinishCurrentConversation();
- // TODO: Explicitly transition to ConversationList (or just go back)?
- }
- }
-
- @Override
- public void onConversationParticipantDataLoaded(final ConversationData data) {
- mBinding.ensureBound(data);
- if (mBinding.getData().getParticipantsLoaded()) {
- final boolean oneOnOne = mBinding.getData().getOtherParticipant() != null;
- mAdapter.setOneOnOne(oneOnOne, true /* invalidate */);
-
- // refresh the options menu which will enable the "people & options" item.
- invalidateOptionsMenu();
-
- mHost.invalidateActionBar();
-
- mRecyclerView.setVisibility(View.VISIBLE);
- mHost.onConversationParticipantDataLoaded
- (mBinding.getData().getNumberOfParticipantsExcludingSelf());
- }
- }
-
- @Override
- public void onSubscriptionListDataLoaded(final ConversationData data) {
- mBinding.ensureBound(data);
- mAdapter.notifyDataSetChanged();
- }
-
- @Override
- public void promptForSelfPhoneNumber() {
- if (mComposeMessageView != null) {
- // Avoid bug in system which puts soft keyboard over dialog after orientation change
- ImeUtil.hideSoftInput(getActivity(), mComposeMessageView);
- }
-
- final FragmentTransaction ft = getActivity().getFragmentManager().beginTransaction();
- final EnterSelfPhoneNumberDialog dialog = EnterSelfPhoneNumberDialog
- .newInstance(getConversationSelfSubId());
- dialog.setTargetFragment(this, 0/*requestCode*/);
- dialog.show(ft, null/*tag*/);
- }
-
- @Override
- public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
- if (mChangeDefaultSmsAppHelper == null) {
- mChangeDefaultSmsAppHelper = new ChangeDefaultSmsAppHelper();
- }
- mChangeDefaultSmsAppHelper.handleChangeDefaultSmsResult(requestCode, resultCode, null);
- }
-
- public boolean hasMessages() {
- return mAdapter != null && mAdapter.getItemCount() > 0;
- }
-
- public boolean onBackPressed() {
- if (mComposeMessageView.onBackPressed()) {
- return true;
- }
- return false;
- }
-
- public boolean onNavigationUpPressed() {
- return mComposeMessageView.onNavigationUpPressed();
- }
-
- @Override
- public boolean onAttachmentClick(final ConversationMessageView messageView,
- final MessagePartData attachment, final Rect imageBounds, final boolean longPress) {
- if (longPress) {
- selectMessage(messageView, attachment);
- return true;
- } else if (messageView.getData().getOneClickResendMessage()) {
- handleMessageClick(messageView);
- return true;
- }
-
- if (attachment.isImage()) {
- displayPhoto(attachment.getContentUri(), imageBounds, false /* isDraft */);
- }
-
- if (attachment.isVCard()) {
- UIIntents.get().launchVCardDetailActivity(getActivity(), attachment.getContentUri());
- }
-
- return false;
- }
-
- private void handleMessageClick(final ConversationMessageView messageView) {
- if (messageView != mSelectedMessage) {
- final ConversationMessageData data = messageView.getData();
- final boolean isReadyToSend = isReadyForAction();
- if (data.getOneClickResendMessage()) {
- // Directly resend the message on tap if it's failed
- retrySend(data.getMessageId());
- selectMessage(null);
- } else if (data.getShowResendMessage() && isReadyToSend) {
- // Select the message to show the resend/download/delete options
- selectMessage(messageView);
- } else if (data.getShowDownloadMessage() && isReadyToSend) {
- // Directly download the message on tap
- retryDownload(data.getMessageId());
- } else {
- // Let the toast from warnOfMissingActionConditions show and skip
- // selecting
- warnOfMissingActionConditions(false /*sending*/,
- null /*commandToRunAfterActionConditionResolved*/);
- selectMessage(null);
- }
- } else {
- selectMessage(null);
- }
- }
-
- private static class AttachmentToSave {
- public final Uri uri;
- public final String contentType;
- public Uri persistedUri;
-
- AttachmentToSave(final Uri uri, final String contentType) {
- this.uri = uri;
- this.contentType = contentType;
- }
- }
-
- public static class SaveAttachmentTask extends SafeAsyncTask<Void, Void, Void> {
- private final Context mContext;
- private final List<AttachmentToSave> mAttachmentsToSave = new ArrayList<>();
-
- public SaveAttachmentTask(final Context context, final Uri contentUri,
- final String contentType) {
- mContext = context;
- addAttachmentToSave(contentUri, contentType);
- }
-
- public SaveAttachmentTask(final Context context) {
- mContext = context;
- }
-
- public void addAttachmentToSave(final Uri contentUri, final String contentType) {
- mAttachmentsToSave.add(new AttachmentToSave(contentUri, contentType));
- }
-
- public int getAttachmentCount() {
- return mAttachmentsToSave.size();
- }
-
- @Override
- protected Void doInBackgroundTimed(final Void... arg) {
- final File appDir = new File(Environment.getExternalStoragePublicDirectory(
- Environment.DIRECTORY_PICTURES),
- mContext.getResources().getString(R.string.app_name));
- final File downloadDir = Environment.getExternalStoragePublicDirectory(
- Environment.DIRECTORY_DOWNLOADS);
- for (final AttachmentToSave attachment : mAttachmentsToSave) {
- final boolean isImageOrVideo = ContentType.isImageType(attachment.contentType)
- || ContentType.isVideoType(attachment.contentType);
- attachment.persistedUri = UriUtil.persistContent(attachment.uri,
- isImageOrVideo ? appDir : downloadDir, attachment.contentType);
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(final Void result) {
- int failCount = 0;
- int imageCount = 0;
- int videoCount = 0;
- int otherCount = 0;
- for (final AttachmentToSave attachment : mAttachmentsToSave) {
- if (attachment.persistedUri == null) {
- failCount++;
- continue;
- }
-
- // Inform MediaScanner about the new file
- final Intent scanFileIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
- scanFileIntent.setData(attachment.persistedUri);
- mContext.sendBroadcast(scanFileIntent);
-
- if (ContentType.isImageType(attachment.contentType)) {
- imageCount++;
- } else if (ContentType.isVideoType(attachment.contentType)) {
- videoCount++;
- } else {
- otherCount++;
- // Inform DownloadManager of the file so it will show in the "downloads" app
- final DownloadManager downloadManager =
- (DownloadManager) mContext.getSystemService(
- Context.DOWNLOAD_SERVICE);
- final String filePath = attachment.persistedUri.getPath();
- final File file = new File(filePath);
-
- if (file.exists()) {
- downloadManager.addCompletedDownload(
- file.getName() /* title */,
- mContext.getString(
- R.string.attachment_file_description) /* description */,
- true /* isMediaScannerScannable */,
- attachment.contentType,
- file.getAbsolutePath(),
- file.length(),
- false /* showNotification */);
- }
- }
- }
-
- String message;
- if (failCount > 0) {
- message = mContext.getResources().getQuantityString(
- R.plurals.attachment_save_error, failCount, failCount);
- } else {
- int messageId = R.plurals.attachments_saved;
- if (otherCount > 0) {
- if (imageCount + videoCount == 0) {
- messageId = R.plurals.attachments_saved_to_downloads;
- }
- } else {
- if (videoCount == 0) {
- messageId = R.plurals.photos_saved_to_album;
- } else if (imageCount == 0) {
- messageId = R.plurals.videos_saved_to_album;
- } else {
- messageId = R.plurals.attachments_saved_to_album;
- }
- }
- final String appName = mContext.getResources().getString(R.string.app_name);
- final int count = imageCount + videoCount + otherCount;
- message = mContext.getResources().getQuantityString(
- messageId, count, count, appName);
- }
- UiUtils.showToastAtBottom(message);
- }
- }
-
- private void invalidateOptionsMenu() {
- final Activity activity = getActivity();
- // TODO: Add the supportInvalidateOptionsMenu call to the host activity.
- if (activity == null || !(activity instanceof BugleActionBarActivity)) {
- return;
- }
- ((BugleActionBarActivity) activity).supportInvalidateOptionsMenu();
- }
-
- @Override
- public void setOptionsMenuVisibility(final boolean visible) {
- setHasOptionsMenu(visible);
- }
-
- @Override
- public int getConversationSelfSubId() {
- final String selfParticipantId = mComposeMessageView.getConversationSelfId();
- final ParticipantData self = mBinding.getData().getSelfParticipantById(selfParticipantId);
- // If the self id or the self participant data hasn't been loaded yet, fallback to
- // the default setting.
- return self == null ? ParticipantData.DEFAULT_SELF_SUB_ID : self.getSubId();
- }
-
- @Override
- public void invalidateActionBar() {
- mHost.invalidateActionBar();
- }
-
- @Override
- public void dismissActionMode() {
- mHost.dismissActionMode();
- }
-
- @Override
- public void selectSim(final SubscriptionListEntry subscriptionData) {
- mComposeMessageView.selectSim(subscriptionData);
- mHost.onStartComposeMessage();
- }
-
- @Override
- public void onStartComposeMessage() {
- mHost.onStartComposeMessage();
- }
-
- @Override
- public SubscriptionListEntry getSubscriptionEntryForSelfParticipant(
- final String selfParticipantId, final boolean excludeDefault) {
- // TODO: ConversationMessageView is the only one using this. We should probably
- // inject this into the view during binding in the ConversationMessageAdapter.
- return mBinding.getData().getSubscriptionEntryForSelfParticipant(selfParticipantId,
- excludeDefault);
- }
-
- @Override
- public SimSelectorView getSimSelectorView() {
- return (SimSelectorView) getView().findViewById(R.id.sim_selector);
- }
-
- @Override
- public MediaPicker createMediaPicker() {
- return new MediaPicker(getActivity());
- }
-
- @Override
- public void notifyOfAttachmentLoadFailed() {
- UiUtils.showToastAtBottom(R.string.attachment_load_failed_dialog_message);
- }
-
- @Override
- public void warnOfExceedingMessageLimit(final boolean sending, final boolean tooManyVideos) {
- warnOfExceedingMessageLimit(sending, mComposeMessageView, mConversationId,
- getActivity(), tooManyVideos);
- }
-
- public static void warnOfExceedingMessageLimit(final boolean sending,
- final ComposeMessageView composeMessageView, final String conversationId,
- final Activity activity, final boolean tooManyVideos) {
- final AlertDialog.Builder builder =
- new AlertDialog.Builder(activity)
- .setTitle(R.string.mms_attachment_limit_reached);
-
- if (sending) {
- if (tooManyVideos) {
- builder.setMessage(R.string.video_attachment_limit_exceeded_when_sending);
- } else {
- builder.setMessage(R.string.attachment_limit_reached_dialog_message_when_sending)
- .setNegativeButton(R.string.attachment_limit_reached_send_anyway,
- new OnClickListener() {
- @Override
- public void onClick(final DialogInterface dialog,
- final int which) {
- composeMessageView.sendMessageIgnoreMessageSizeLimit();
- }
- });
- }
- builder.setPositiveButton(android.R.string.ok, new OnClickListener() {
- @Override
- public void onClick(final DialogInterface dialog, final int which) {
- showAttachmentChooser(conversationId, activity);
- }});
- } else {
- builder.setMessage(R.string.attachment_limit_reached_dialog_message_when_composing)
- .setPositiveButton(android.R.string.ok, null);
- }
- builder.show();
- }
-
- @Override
- public void showAttachmentChooser() {
- showAttachmentChooser(mConversationId, getActivity());
- }
-
- public static void showAttachmentChooser(final String conversationId,
- final Activity activity) {
- UIIntents.get().launchAttachmentChooserActivity(activity,
- conversationId, REQUEST_CHOOSE_ATTACHMENTS);
- }
-
- private void updateActionAndStatusBarColor(final ActionBar actionBar) {
- final int themeColor = ConversationDrawables.get().getConversationThemeColor();
- actionBar.setBackgroundDrawable(new ColorDrawable(themeColor));
- UiUtils.setStatusBarColor(getActivity(), themeColor);
- }
-
- public void updateActionBar(final ActionBar actionBar) {
- if (mComposeMessageView == null || !mComposeMessageView.updateActionBar(actionBar)) {
- updateActionAndStatusBarColor(actionBar);
- // We update this regardless of whether or not the action bar is showing so that we
- // don't get a race when it reappears.
- actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
- actionBar.setDisplayHomeAsUpEnabled(true);
- // Reset the back arrow to its default
- actionBar.setHomeAsUpIndicator(0);
- View customView = actionBar.getCustomView();
- if (customView == null || customView.getId() != R.id.conversation_title_container) {
- final LayoutInflater inflator = (LayoutInflater)
- getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- customView = inflator.inflate(R.layout.action_bar_conversation_name, null);
- customView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(final View v) {
- onBackPressed();
- }
- });
- actionBar.setCustomView(customView);
- }
-
- final TextView conversationNameView =
- (TextView) customView.findViewById(R.id.conversation_title);
- final String conversationName = getConversationName();
- if (!TextUtils.isEmpty(conversationName)) {
- // RTL : To format conversation title if it happens to be phone numbers.
- final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
- final String formattedName = bidiFormatter.unicodeWrap(
- UiUtils.commaEllipsize(
- conversationName,
- conversationNameView.getPaint(),
- conversationNameView.getWidth(),
- getString(R.string.plus_one),
- getString(R.string.plus_n)).toString(),
- TextDirectionHeuristicsCompat.LTR);
- conversationNameView.setText(formattedName);
- // In case phone numbers are mixed in the conversation name, we need to vocalize it.
- final String vocalizedConversationName =
- AccessibilityUtil.getVocalizedPhoneNumber(getResources(), conversationName);
- conversationNameView.setContentDescription(vocalizedConversationName);
- getActivity().setTitle(conversationName);
- } else {
- final String appName = getString(R.string.app_name);
- conversationNameView.setText(appName);
- getActivity().setTitle(appName);
- }
-
- // When conversation is showing and media picker is not showing, then hide the action
- // bar only when we are in landscape mode, with IME open.
- if (mHost.isImeOpen() && UiUtils.isLandscapeMode()) {
- actionBar.hide();
- } else {
- actionBar.show();
- }
- }
- }
-
- @Override
- public boolean shouldShowSubjectEditor() {
- return true;
- }
-
- @Override
- public boolean shouldHideAttachmentsWhenSimSelectorShown() {
- return false;
- }
-
- @Override
- public void showHideSimSelector(final boolean show) {
- // no-op for now
- }
-
- @Override
- public int getSimSelectorItemLayoutId() {
- return R.layout.sim_selector_item_view;
- }
-
- @Override
- public Uri getSelfSendButtonIconUri() {
- return null; // use default button icon uri
- }
-
- @Override
- public int overrideCounterColor() {
- return -1; // don't override the color
- }
-
- @Override
- public void onAttachmentsChanged(final boolean haveAttachments) {
- // no-op for now
- }
-
- @Override
- public void onDraftChanged(final DraftMessageData data, final int changeFlags) {
- mDraftMessageDataModel.ensureBound(data);
- // We're specifically only interested in ATTACHMENTS_CHANGED from the widget. Ignore
- // other changes. When the widget changes an attachment, we need to reload the draft.
- if (changeFlags ==
- (DraftMessageData.WIDGET_CHANGED | DraftMessageData.ATTACHMENTS_CHANGED)) {
- mClearLocalDraft = true; // force a reload of the draft in onResume
- }
- }
-
- @Override
- public void onDraftAttachmentLimitReached(final DraftMessageData data) {
- // no-op for now
- }
-
- @Override
- public void onDraftAttachmentLoadFailed() {
- // no-op for now
- }
-
- @Override
- public int getAttachmentsClearedFlags() {
- return DraftMessageData.ATTACHMENTS_CHANGED;
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/ConversationInput.java b/src/com/android/messaging/ui/conversation/ConversationInput.java
deleted file mode 100644
index bf60aa8..0000000
--- a/src/com/android/messaging/ui/conversation/ConversationInput.java
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.messaging.ui.conversation;
-
-import android.os.Bundle;
-import android.support.v7.app.ActionBar;
-
-/**
- * The base class for a method of user input, e.g. media picker.
- */
-public abstract class ConversationInput {
- /**
- * The host component where all input components are contained. This is typically the
- * conversation fragment but may be mocked in test code.
- */
- public interface ConversationInputBase {
- boolean showHideInternal(final ConversationInput target, final boolean show,
- final boolean animate);
- String getInputStateKey(final ConversationInput input);
- void beginUpdate();
- void handleOnShow(final ConversationInput target);
- void endUpdate();
- }
-
- protected boolean mShowing;
- protected ConversationInputBase mConversationInputBase;
-
- public abstract boolean show(boolean animate);
- public abstract boolean hide(boolean animate);
-
- public ConversationInput(ConversationInputBase baseHost, final boolean isShowing) {
- mConversationInputBase = baseHost;
- mShowing = isShowing;
- }
-
- public boolean onBackPressed() {
- if (mShowing) {
- mConversationInputBase.showHideInternal(this, false /* show */, true /* animate */);
- return true;
- }
- return false;
- }
-
- public boolean onNavigationUpPressed() {
- return false;
- }
-
- /**
- * Toggle the visibility of this view.
- * @param animate
- * @return true if the view is now shown, false if it now hidden
- */
- public boolean toggle(final boolean animate) {
- mConversationInputBase.showHideInternal(this, !mShowing /* show */, true /* animate */);
- return mShowing;
- }
-
- public void saveState(final Bundle savedState) {
- savedState.putBoolean(mConversationInputBase.getInputStateKey(this), mShowing);
- }
-
- public void restoreState(final Bundle savedState) {
- // Things are hidden by default, so only handle show.
- if (savedState.getBoolean(mConversationInputBase.getInputStateKey(this))) {
- mConversationInputBase.showHideInternal(this, true /* show */, false /* animate */);
- }
- }
-
- public boolean updateActionBar(final ActionBar actionBar) {
- return false;
- }
-
- /**
- * Update our visibility flag in response to visibility change, both for actions
- * initiated by this class (through the show/hide methods), and for external changes
- * tracked by event listeners (e.g. ImeStateObserver, MediaPickerListener). As part of
- * handling an input showing, we will hide all other inputs to ensure they are mutually
- * exclusive.
- */
- protected void onVisibilityChanged(final boolean visible) {
- if (mShowing != visible) {
- mConversationInputBase.beginUpdate();
- mShowing = visible;
- if (visible) {
- mConversationInputBase.handleOnShow(this);
- }
- mConversationInputBase.endUpdate();
- }
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/ConversationInputManager.java b/src/com/android/messaging/ui/conversation/ConversationInputManager.java
deleted file mode 100644
index e10abe7..0000000
--- a/src/com/android/messaging/ui/conversation/ConversationInputManager.java
+++ /dev/null
@@ -1,550 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.messaging.ui.conversation;
-
-import android.app.FragmentManager;
-import android.content.Context;
-import android.os.Bundle;
-import android.support.v7.app.ActionBar;
-import android.widget.EditText;
-
-import com.android.messaging.R;
-import com.android.messaging.datamodel.binding.BindingBase;
-import com.android.messaging.datamodel.binding.ImmutableBindingRef;
-import com.android.messaging.datamodel.data.ConversationData;
-import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener;
-import com.android.messaging.datamodel.data.ConversationData.SimpleConversationDataListener;
-import com.android.messaging.datamodel.data.DraftMessageData;
-import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageSubscriptionDataProvider;
-import com.android.messaging.datamodel.data.MessagePartData;
-import com.android.messaging.datamodel.data.PendingAttachmentData;
-import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
-import com.android.messaging.ui.ConversationDrawables;
-import com.android.messaging.ui.mediapicker.MediaPicker;
-import com.android.messaging.ui.mediapicker.MediaPicker.MediaPickerListener;
-import com.android.messaging.util.Assert;
-import com.android.messaging.util.ImeUtil;
-import com.android.messaging.util.ImeUtil.ImeStateHost;
-import com.google.common.annotations.VisibleForTesting;
-
-import java.util.Collection;
-
-/**
- * Manages showing/hiding/persisting different mutually exclusive UI components nested in
- * ConversationFragment that take user inputs, i.e. media picker, SIM selector and
- * IME keyboard (the IME keyboard is not owned by Bugle, but we try to model it the same way
- * as the other components).
- */
-public class ConversationInputManager implements ConversationInput.ConversationInputBase {
- /**
- * The host component where all input components are contained. This is typically the
- * conversation fragment but may be mocked in test code.
- */
- public interface ConversationInputHost extends DraftMessageSubscriptionDataProvider {
- void invalidateActionBar();
- void setOptionsMenuVisibility(boolean visible);
- void dismissActionMode();
- void selectSim(SubscriptionListEntry subscriptionData);
- void onStartComposeMessage();
- SimSelectorView getSimSelectorView();
- MediaPicker createMediaPicker();
- void showHideSimSelector(boolean show);
- int getSimSelectorItemLayoutId();
- }
-
- /**
- * The "sink" component where all inputs components will direct the user inputs to. This is
- * typically the ComposeMessageView but may be mocked in test code.
- */
- public interface ConversationInputSink {
- void onMediaItemsSelected(Collection<MessagePartData> items);
- void onMediaItemsUnselected(MessagePartData item);
- void onPendingAttachmentAdded(PendingAttachmentData pendingItem);
- void resumeComposeMessage();
- EditText getComposeEditText();
- void setAccessibility(boolean enabled);
- }
-
- private final ConversationInputHost mHost;
- private final ConversationInputSink mSink;
-
- /** Dependencies injected from the host during construction */
- private final FragmentManager mFragmentManager;
- private final Context mContext;
- private final ImeStateHost mImeStateHost;
- private final ImmutableBindingRef<ConversationData> mConversationDataModel;
- private final ImmutableBindingRef<DraftMessageData> mDraftDataModel;
-
- private final ConversationInput[] mInputs;
- private final ConversationMediaPicker mMediaInput;
- private final ConversationSimSelector mSimInput;
- private final ConversationImeKeyboard mImeInput;
- private int mUpdateCount;
-
- private final ImeUtil.ImeStateObserver mImeStateObserver = new ImeUtil.ImeStateObserver() {
- @Override
- public void onImeStateChanged(final boolean imeOpen) {
- mImeInput.onVisibilityChanged(imeOpen);
- }
- };
-
- private final ConversationDataListener mDataListener = new SimpleConversationDataListener() {
- @Override
- public void onConversationParticipantDataLoaded(ConversationData data) {
- mConversationDataModel.ensureBound(data);
- }
-
- @Override
- public void onSubscriptionListDataLoaded(ConversationData data) {
- mConversationDataModel.ensureBound(data);
- mSimInput.onSubscriptionListDataLoaded(data.getSubscriptionListData());
- }
- };
-
- public ConversationInputManager(
- final Context context,
- final ConversationInputHost host,
- final ConversationInputSink sink,
- final ImeStateHost imeStateHost,
- final FragmentManager fm,
- final BindingBase<ConversationData> conversationDataModel,
- final BindingBase<DraftMessageData> draftDataModel,
- final Bundle savedState) {
- mHost = host;
- mSink = sink;
- mFragmentManager = fm;
- mContext = context;
- mImeStateHost = imeStateHost;
- mConversationDataModel = BindingBase.createBindingReference(conversationDataModel);
- mDraftDataModel = BindingBase.createBindingReference(draftDataModel);
-
- // Register listeners on dependencies.
- mImeStateHost.registerImeStateObserver(mImeStateObserver);
- mConversationDataModel.getData().addConversationDataListener(mDataListener);
-
- // Initialize the inputs
- mMediaInput = new ConversationMediaPicker(this);
- mSimInput = new SimSelector(this);
- mImeInput = new ConversationImeKeyboard(this, mImeStateHost.isImeOpen());
- mInputs = new ConversationInput[] { mMediaInput, mSimInput, mImeInput };
-
- if (savedState != null) {
- for (int i = 0; i < mInputs.length; i++) {
- mInputs[i].restoreState(savedState);
- }
- }
- updateHostOptionsMenu();
- }
-
- public void onDetach() {
- mImeStateHost.unregisterImeStateObserver(mImeStateObserver);
- // Don't need to explicitly unregister for data model events. It will unregister all
- // listeners automagically on unbind.
- }
-
- public void onSaveInputState(final Bundle savedState) {
- for (int i = 0; i < mInputs.length; i++) {
- mInputs[i].saveState(savedState);
- }
- }
-
- @Override
- public String getInputStateKey(final ConversationInput input) {
- return input.getClass().getCanonicalName() + "_savedstate_";
- }
-
- public boolean onBackPressed() {
- for (int i = 0; i < mInputs.length; i++) {
- if (mInputs[i].onBackPressed()) {
- return true;
- }
- }
- return false;
- }
-
- public boolean onNavigationUpPressed() {
- for (int i = 0; i < mInputs.length; i++) {
- if (mInputs[i].onNavigationUpPressed()) {
- return true;
- }
- }
- return false;
- }
-
- public void resetMediaPickerState() {
- mMediaInput.resetViewHolderState();
- }
-
- public void showHideMediaPicker(final boolean show, final boolean animate) {
- showHideInternal(mMediaInput, show, animate);
- }
-
- /**
- * Show or hide the sim selector
- * @param show visibility
- * @param animate whether to animate the change in visibility
- * @return true if the state of the visibility was changed
- */
- public boolean showHideSimSelector(final boolean show, final boolean animate) {
- return showHideInternal(mSimInput, show, animate);
- }
-
- public void showHideImeKeyboard(final boolean show, final boolean animate) {
- showHideInternal(mImeInput, show, animate);
- }
-
- public void hideAllInputs(final boolean animate) {
- beginUpdate();
- for (int i = 0; i < mInputs.length; i++) {
- showHideInternal(mInputs[i], false, animate);
- }
- endUpdate();
- }
-
- /**
- * Toggle the visibility of the sim selector.
- * @param animate
- * @param subEntry
- * @return true if the view is now shown, false if it now hidden
- */
- public boolean toggleSimSelector(final boolean animate, final SubscriptionListEntry subEntry) {
- mSimInput.setSelected(subEntry);
- return mSimInput.toggle(animate);
- }
-
- public boolean updateActionBar(final ActionBar actionBar) {
- for (int i = 0; i < mInputs.length; i++) {
- if (mInputs[i].mShowing) {
- return mInputs[i].updateActionBar(actionBar);
- }
- }
- return false;
- }
-
- @VisibleForTesting
- boolean isMediaPickerVisible() {
- return mMediaInput.mShowing;
- }
-
- @VisibleForTesting
- boolean isSimSelectorVisible() {
- return mSimInput.mShowing;
- }
-
- @VisibleForTesting
- boolean isImeKeyboardVisible() {
- return mImeInput.mShowing;
- }
-
- @VisibleForTesting
- void testNotifyImeStateChanged(final boolean imeOpen) {
- mImeStateObserver.onImeStateChanged(imeOpen);
- }
-
- /**
- * returns true if the state of the visibility was actually changed
- */
- @Override
- public boolean showHideInternal(final ConversationInput target, final boolean show,
- final boolean animate) {
- if (!mConversationDataModel.isBound()) {
- return false;
- }
-
- if (target.mShowing == show) {
- return false;
- }
- beginUpdate();
- boolean success;
- if (!show) {
- success = target.hide(animate);
- } else {
- success = target.show(animate);
- }
-
- if (success) {
- target.onVisibilityChanged(show);
- }
- endUpdate();
- return true;
- }
-
- @Override
- public void handleOnShow(final ConversationInput target) {
- if (!mConversationDataModel.isBound()) {
- return;
- }
- beginUpdate();
-
- // All inputs are mutually exclusive. Showing one will hide everything else.
- // The one exception, is that the keyboard and location media chooser can be open at the
- // time to enable searching within that chooser
- for (int i = 0; i < mInputs.length; i++) {
- final ConversationInput currInput = mInputs[i];
- if (currInput != target) {
- // TODO : If there's more exceptions we will want to make this more
- // generic
- if (currInput instanceof ConversationMediaPicker &&
- target instanceof ConversationImeKeyboard &&
- mMediaInput.getExistingOrCreateMediaPicker() != null &&
- mMediaInput.getExistingOrCreateMediaPicker().canShowIme()) {
- // Allow the keyboard and location mediaPicker to be open at the same time,
- // but ensure the media picker is full screen to allow enough room
- mMediaInput.getExistingOrCreateMediaPicker().setFullScreen(true);
- continue;
- }
- showHideInternal(currInput, false /* show */, false /* animate */);
- }
- }
- // Always dismiss action mode on show.
- mHost.dismissActionMode();
- // Invoking any non-keyboard input UI is treated as starting message compose.
- if (target != mImeInput) {
- mHost.onStartComposeMessage();
- }
- endUpdate();
- }
-
- @Override
- public void beginUpdate() {
- mUpdateCount++;
- }
-
- @Override
- public void endUpdate() {
- Assert.isTrue(mUpdateCount > 0);
- if (--mUpdateCount == 0) {
- // Always try to update the host action bar after every update cycle.
- mHost.invalidateActionBar();
- }
- }
-
- private void updateHostOptionsMenu() {
- mHost.setOptionsMenuVisibility(!mMediaInput.isOpen());
- }
-
- /**
- * Manages showing/hiding the media picker in conversation.
- */
- private class ConversationMediaPicker extends ConversationInput {
- public ConversationMediaPicker(ConversationInputBase baseHost) {
- super(baseHost, false);
- }
-
- private MediaPicker mMediaPicker;
-
- @Override
- public boolean show(boolean animate) {
- if (mMediaPicker == null) {
- mMediaPicker = getExistingOrCreateMediaPicker();
- setConversationThemeColor(ConversationDrawables.get().getConversationThemeColor());
- mMediaPicker.setSubscriptionDataProvider(mHost);
- mMediaPicker.setDraftMessageDataModel(mDraftDataModel);
- mMediaPicker.setListener(new MediaPickerListener() {
- @Override
- public void onOpened() {
- handleStateChange();
- }
-
- @Override
- public void onFullScreenChanged(boolean fullScreen) {
- // When we're full screen, we want to disable accessibility on the
- // ComposeMessageView controls (attach button, message input, sim chooser)
- // that are hiding underneath the action bar.
- mSink.setAccessibility(!fullScreen /*enabled*/);
- handleStateChange();
- }
-
- @Override
- public void onDismissed() {
- // Re-enable accessibility on all controls now that the media picker is
- // going away.
- mSink.setAccessibility(true /*enabled*/);
- handleStateChange();
- }
-
- private void handleStateChange() {
- onVisibilityChanged(isOpen());
- mHost.invalidateActionBar();
- updateHostOptionsMenu();
- }
-
- @Override
- public void onItemsSelected(final Collection<MessagePartData> items,
- final boolean resumeCompose) {
- mSink.onMediaItemsSelected(items);
- mHost.invalidateActionBar();
- if (resumeCompose) {
- mSink.resumeComposeMessage();
- }
- }
-
- @Override
- public void onItemUnselected(final MessagePartData item) {
- mSink.onMediaItemsUnselected(item);
- mHost.invalidateActionBar();
- }
-
- @Override
- public void onConfirmItemSelection() {
- mSink.resumeComposeMessage();
- }
-
- @Override
- public void onPendingItemAdded(final PendingAttachmentData pendingItem) {
- mSink.onPendingAttachmentAdded(pendingItem);
- }
-
- @Override
- public void onChooserSelected(final int chooserIndex) {
- mHost.invalidateActionBar();
- mHost.dismissActionMode();
- }
- });
- }
-
- mMediaPicker.open(MediaPicker.MEDIA_TYPE_DEFAULT, animate);
-
- return isOpen();
- }
-
- @Override
- public boolean hide(boolean animate) {
- if (mMediaPicker != null) {
- mMediaPicker.dismiss(animate);
- }
- return !isOpen();
- }
-
- public void resetViewHolderState() {
- if (mMediaPicker != null) {
- mMediaPicker.resetViewHolderState();
- }
- }
-
- public void setConversationThemeColor(final int themeColor) {
- if (mMediaPicker != null) {
- mMediaPicker.setConversationThemeColor(themeColor);
- }
- }
-
- private boolean isOpen() {
- return (mMediaPicker != null && mMediaPicker.isOpen());
- }
-
- private MediaPicker getExistingOrCreateMediaPicker() {
- if (mMediaPicker != null) {
- return mMediaPicker;
- }
- MediaPicker mediaPicker = (MediaPicker)
- mFragmentManager.findFragmentByTag(MediaPicker.FRAGMENT_TAG);
- if (mediaPicker == null) {
- mediaPicker = mHost.createMediaPicker();
- if (mediaPicker == null) {
- return null; // this use of ComposeMessageView doesn't support media picking
- }
- mFragmentManager.beginTransaction().replace(
- R.id.mediapicker_container,
- mediaPicker,
- MediaPicker.FRAGMENT_TAG).commit();
- }
- return mediaPicker;
- }
-
- @Override
- public boolean updateActionBar(ActionBar actionBar) {
- if (isOpen()) {
- mMediaPicker.updateActionBar(actionBar);
- return true;
- }
- return false;
- }
-
- @Override
- public boolean onNavigationUpPressed() {
- if (isOpen() && mMediaPicker.isFullScreen()) {
- return onBackPressed();
- }
- return super.onNavigationUpPressed();
- }
-
- public boolean onBackPressed() {
- if (mMediaPicker != null && mMediaPicker.onBackPressed()) {
- return true;
- }
- return super.onBackPressed();
- }
- }
-
- /**
- * Manages showing/hiding the SIM selector in conversation.
- */
- private class SimSelector extends ConversationSimSelector {
- public SimSelector(ConversationInputBase baseHost) {
- super(baseHost);
- }
-
- @Override
- protected SimSelectorView getSimSelectorView() {
- return mHost.getSimSelectorView();
- }
-
- @Override
- public int getSimSelectorItemLayoutId() {
- return mHost.getSimSelectorItemLayoutId();
- }
-
- @Override
- protected void selectSim(SubscriptionListEntry item) {
- mHost.selectSim(item);
- }
-
- @Override
- public boolean show(boolean animate) {
- final boolean result = super.show(animate);
- mHost.showHideSimSelector(true /*show*/);
- return result;
- }
-
- @Override
- public boolean hide(boolean animate) {
- final boolean result = super.hide(animate);
- mHost.showHideSimSelector(false /*show*/);
- return result;
- }
- }
-
- /**
- * Manages showing/hiding the IME keyboard in conversation.
- */
- private class ConversationImeKeyboard extends ConversationInput {
- public ConversationImeKeyboard(ConversationInputBase baseHost, final boolean isShowing) {
- super(baseHost, isShowing);
- }
-
- @Override
- public boolean show(boolean animate) {
- ImeUtil.get().showImeKeyboard(mContext, mSink.getComposeEditText());
- return true;
- }
-
- @Override
- public boolean hide(boolean animate) {
- ImeUtil.get().hideImeKeyboard(mContext, mSink.getComposeEditText());
- return true;
- }
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/ConversationMessageAdapter.java b/src/com/android/messaging/ui/conversation/ConversationMessageAdapter.java
deleted file mode 100644
index 2748fff..0000000
--- a/src/com/android/messaging/ui/conversation/ConversationMessageAdapter.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.messaging.ui.conversation;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.messaging.R;
-import com.android.messaging.ui.AsyncImageView;
-import com.android.messaging.ui.CursorRecyclerAdapter;
-import com.android.messaging.ui.AsyncImageView.AsyncImageViewDelayLoader;
-import com.android.messaging.ui.conversation.ConversationMessageView.ConversationMessageViewHost;
-import com.android.messaging.util.Assert;
-
-import java.util.HashSet;
-import java.util.List;
-
-/**
- * Provides an interface to expose Conversation Message Cursor data to a UI widget like a
- * RecyclerView.
- */
-public class ConversationMessageAdapter extends
- CursorRecyclerAdapter<ConversationMessageAdapter.ConversationMessageViewHolder> {
-
- private final ConversationMessageViewHost mHost;
- private final AsyncImageViewDelayLoader mImageViewDelayLoader;
- private final View.OnClickListener mViewClickListener;
- private final View.OnLongClickListener mViewLongClickListener;
- private boolean mOneOnOne;
- private String mSelectedMessageId;
-
- public ConversationMessageAdapter(final Context context, final Cursor cursor,
- final ConversationMessageViewHost host,
- final AsyncImageViewDelayLoader imageViewDelayLoader,
- final View.OnClickListener viewClickListener,
- final View.OnLongClickListener longClickListener) {
- super(context, cursor, 0);
- mHost = host;
- mViewClickListener = viewClickListener;
- mViewLongClickListener = longClickListener;
- mImageViewDelayLoader = imageViewDelayLoader;
- setHasStableIds(true);
- }
-
- @Override
- public void bindViewHolder(final ConversationMessageViewHolder holder,
- final Context context, final Cursor cursor) {
- Assert.isTrue(holder.mView instanceof ConversationMessageView);
- final ConversationMessageView conversationMessageView =
- (ConversationMessageView) holder.mView;
- conversationMessageView.bind(cursor, mOneOnOne, mSelectedMessageId);
- }
-
- @Override
- public ConversationMessageViewHolder createViewHolder(final Context context,
- final ViewGroup parent, final int viewType) {
- final LayoutInflater layoutInflater = LayoutInflater.from(context);
- final ConversationMessageView conversationMessageView = (ConversationMessageView)
- layoutInflater.inflate(R.layout.conversation_message_view, null);
- conversationMessageView.setHost(mHost);
- conversationMessageView.setImageViewDelayLoader(mImageViewDelayLoader);
- return new ConversationMessageViewHolder(conversationMessageView,
- mViewClickListener, mViewLongClickListener);
- }
-
- public void setSelectedMessage(final String messageId) {
- mSelectedMessageId = messageId;
- notifyDataSetChanged();
- }
-
- public void setOneOnOne(final boolean oneOnOne, final boolean invalidate) {
- if (mOneOnOne != oneOnOne) {
- mOneOnOne = oneOnOne;
- if (invalidate) {
- notifyDataSetChanged();
- }
- }
- }
-
- /**
- * ViewHolder that holds a ConversationMessageView.
- */
- public static class ConversationMessageViewHolder extends RecyclerView.ViewHolder {
- final View mView;
-
- /**
- * @param viewClickListener a View.OnClickListener that should define the interaction when
- * an item in the RecyclerView is clicked.
- */
- public ConversationMessageViewHolder(final View itemView,
- final View.OnClickListener viewClickListener,
- final View.OnLongClickListener viewLongClickListener) {
- super(itemView);
- mView = itemView;
-
- mView.setOnClickListener(viewClickListener);
- mView.setOnLongClickListener(viewLongClickListener);
- }
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/ConversationMessageBubbleView.java b/src/com/android/messaging/ui/conversation/ConversationMessageBubbleView.java
deleted file mode 100644
index ef6aeb4..0000000
--- a/src/com/android/messaging/ui/conversation/ConversationMessageBubbleView.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.messaging.ui.conversation;
-
-import android.animation.Animator;
-import android.animation.Animator.AnimatorListener;
-import android.animation.ObjectAnimator;
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-
-import com.android.messaging.R;
-import com.android.messaging.annotation.VisibleForAnimation;
-import com.android.messaging.datamodel.data.ConversationMessageBubbleData;
-import com.android.messaging.datamodel.data.ConversationMessageData;
-import com.android.messaging.util.UiUtils;
-
-/**
- * Shows the message bubble for one conversation message. It is able to animate size changes
- * by morphing when the message content changes size.
- */
-// TODO: Move functionality from ConversationMessageView into this class as appropriate
-public class ConversationMessageBubbleView extends LinearLayout {
- private int mIntrinsicWidth;
- private int mMorphedWidth;
- private ObjectAnimator mAnimator;
- private boolean mShouldAnimateWidthChange;
- private final ConversationMessageBubbleData mData;
- private int mRunningStartWidth;
- private ViewGroup mBubbleBackground;
-
- public ConversationMessageBubbleView(final Context context, final AttributeSet attrs) {
- super(context, attrs);
- mData = new ConversationMessageBubbleData();
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mBubbleBackground = (ViewGroup) findViewById(R.id.message_text_and_info);
- }
-
- @Override
- protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- final int newIntrinsicWidth = getMeasuredWidth();
- if (mIntrinsicWidth == 0 && newIntrinsicWidth != mIntrinsicWidth) {
- if (mShouldAnimateWidthChange) {
- kickOffMorphAnimation(mIntrinsicWidth, newIntrinsicWidth);
- }
- mIntrinsicWidth = newIntrinsicWidth;
- }
-
- if (mMorphedWidth > 0) {
- mBubbleBackground.getLayoutParams().width = mMorphedWidth;
- } else {
- mBubbleBackground.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
- }
- mBubbleBackground.requestLayout();
- }
-
- @VisibleForAnimation
- public void setMorphWidth(final int width) {
- mMorphedWidth = width;
- requestLayout();
- }
-
- public void bind(final ConversationMessageData data) {
- final boolean changed = mData.bind(data);
- // Animate width change only when we are binding to the same message, so that we may
- // animate view size changes on the same message bubble due to things like status text
- // change.
- // Don't animate width change when the bubble contains attachments. Width animation is
- // only suitable for text-only messages (where the bubble size change due to status or
- // time stamp changes).
- mShouldAnimateWidthChange = !changed && !data.hasAttachments();
- if (mAnimator == null) {
- mMorphedWidth = 0;
- }
- }
-
- public void kickOffMorphAnimation(final int oldWidth, final int newWidth) {
- if (mAnimator != null) {
- mAnimator.setIntValues(mRunningStartWidth, newWidth);
- return;
- }
- mRunningStartWidth = oldWidth;
- mAnimator = ObjectAnimator.ofInt(this, "morphWidth", oldWidth, newWidth);
- mAnimator.setDuration(UiUtils.MEDIAPICKER_TRANSITION_DURATION);
- mAnimator.addListener(new AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animator) {
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- mAnimator = null;
- mMorphedWidth = 0;
- // Allow the bubble to resize if, for example, the status text changed during
- // the animation. This will snap to the bigger size if needed. This is intentional
- // as animating immediately after looks really bad and switching layout params
- // during the original animation does not achieve the desired effect.
- mBubbleBackground.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
- mBubbleBackground.requestLayout();
- }
-
- @Override
- public void onAnimationCancel(Animator animator) {
- }
-
- @Override
- public void onAnimationRepeat(Animator animator) {
- }
- });
- mAnimator.start();
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/ConversationMessageView.java b/src/com/android/messaging/ui/conversation/ConversationMessageView.java
deleted file mode 100644
index e22e2c7..0000000
--- a/src/com/android/messaging/ui/conversation/ConversationMessageView.java
+++ /dev/null
@@ -1,1206 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.messaging.ui.conversation;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.support.annotation.Nullable;
-import android.text.Spanned;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.text.format.Formatter;
-import android.text.style.URLSpan;
-import android.text.util.Linkify;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.FrameLayout;
-import android.widget.ImageView.ScaleType;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.messaging.R;
-import com.android.messaging.datamodel.DataModel;
-import com.android.messaging.datamodel.data.ConversationMessageData;
-import com.android.messaging.datamodel.data.MessageData;
-import com.android.messaging.datamodel.data.MessagePartData;
-import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
-import com.android.messaging.datamodel.media.ImageRequestDescriptor;
-import com.android.messaging.datamodel.media.MessagePartImageRequestDescriptor;
-import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
-import com.android.messaging.sms.MmsUtils;
-import com.android.messaging.ui.AsyncImageView;
-import com.android.messaging.ui.AsyncImageView.AsyncImageViewDelayLoader;
-import com.android.messaging.ui.AudioAttachmentView;
-import com.android.messaging.ui.ContactIconView;
-import com.android.messaging.ui.ConversationDrawables;
-import com.android.messaging.ui.MultiAttachmentLayout;
-import com.android.messaging.ui.MultiAttachmentLayout.OnAttachmentClickListener;
-import com.android.messaging.ui.PersonItemView;
-import com.android.messaging.ui.UIIntents;
-import com.android.messaging.ui.VideoThumbnailView;
-import com.android.messaging.util.AccessibilityUtil;
-import com.android.messaging.util.Assert;
-import com.android.messaging.util.AvatarUriUtil;
-import com.android.messaging.util.ContentType;
-import com.android.messaging.util.ImageUtils;
-import com.android.messaging.util.OsUtil;
-import com.android.messaging.util.PhoneUtils;
-import com.android.messaging.util.UiUtils;
-import com.android.messaging.util.YouTubeUtil;
-import com.google.common.base.Predicate;
-
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * The view for a single entry in a conversation.
- */
-public class ConversationMessageView extends FrameLayout implements View.OnClickListener,
- View.OnLongClickListener, OnAttachmentClickListener {
- public interface ConversationMessageViewHost {
- boolean onAttachmentClick(ConversationMessageView view, MessagePartData attachment,
- Rect imageBounds, boolean longPress);
- SubscriptionListEntry getSubscriptionEntryForSelfParticipant(String selfParticipantId,
- boolean excludeDefault);
- }
-
- private final ConversationMessageData mData;
-
- private LinearLayout mMessageAttachmentsView;
- private MultiAttachmentLayout mMultiAttachmentView;
- private AsyncImageView mMessageImageView;
- private TextView mMessageTextView;
- private boolean mMessageTextHasLinks;
- private boolean mMessageHasYouTubeLink;
- private TextView mStatusTextView;
- private TextView mTitleTextView;
- private TextView mMmsInfoTextView;
- private LinearLayout mMessageTitleLayout;
- private TextView mSenderNameTextView;
- private ContactIconView mContactIconView;
- private ConversationMessageBubbleView mMessageBubble;
- private View mSubjectView;
- private TextView mSubjectLabel;
- private TextView mSubjectText;
- private View mDeliveredBadge;
- private ViewGroup mMessageMetadataView;
- private ViewGroup mMessageTextAndInfoView;
- private TextView mSimNameView;
-
- private boolean mOneOnOne;
- private ConversationMessageViewHost mHost;
-
- public ConversationMessageView(final Context context, final AttributeSet attrs) {
- super(context, attrs);
- // TODO: we should switch to using Binding and DataModel factory methods.
- mData = new ConversationMessageData();
- }
-
- @Override
- protected void onFinishInflate() {
- mContactIconView = (ContactIconView) findViewById(R.id.conversation_icon);
- mContactIconView.setOnLongClickListener(new OnLongClickListener() {
- @Override
- public boolean onLongClick(final View view) {
- ConversationMessageView.this.performLongClick();
- return true;
- }
- });
-
- mMessageAttachmentsView = (LinearLayout) findViewById(R.id.message_attachments);
- mMultiAttachmentView = (MultiAttachmentLayout) findViewById(R.id.multiple_attachments);
- mMultiAttachmentView.setOnAttachmentClickListener(this);
-
- mMessageImageView = (AsyncImageView) findViewById(R.id.message_image);
- mMessageImageView.setOnClickListener(this);
- mMessageImageView.setOnLongClickListener(this);
-
- mMessageTextView = (TextView) findViewById(R.id.message_text);
- mMessageTextView.setOnClickListener(this);
- IgnoreLinkLongClickHelper.ignoreLinkLongClick(mMessageTextView, this);
-
- mStatusTextView = (TextView) findViewById(R.id.message_status);
- mTitleTextView = (TextView) findViewById(R.id.message_title);
- mMmsInfoTextView = (TextView) findViewById(R.id.mms_info);
- mMessageTitleLayout = (LinearLayout) findViewById(R.id.message_title_layout);
- mSenderNameTextView = (TextView) findViewById(R.id.message_sender_name);
- mMessageBubble = (ConversationMessageBubbleView) findViewById(R.id.message_content);
- mSubjectView = findViewById(R.id.subject_container);
- mSubjectLabel = (TextView) mSubjectView.findViewById(R.id.subject_label);
- mSubjectText = (TextView) mSubjectView.findViewById(R.id.subject_text);
- mDeliveredBadge = findViewById(R.id.smsDeliveredBadge);
- mMessageMetadataView = (ViewGroup) findViewById(R.id.message_metadata);
- mMessageTextAndInfoView = (ViewGroup) findViewById(R.id.message_text_and_info);
- mSimNameView = (TextView) findViewById(R.id.sim_name);
- }
-
- @Override
- protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
- final int horizontalSpace = MeasureSpec.getSize(widthMeasureSpec);
- final int iconSize = getResources()
- .getDimensionPixelSize(R.dimen.conversation_message_contact_icon_size);
-
- final int unspecifiedMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- final int iconMeasureSpec = MeasureSpec.makeMeasureSpec(iconSize, MeasureSpec.EXACTLY);
-
- mContactIconView.measure(iconMeasureSpec, iconMeasureSpec);
-
- final int arrowWidth =
- getResources().getDimensionPixelSize(R.dimen.message_bubble_arrow_width);
-
- // We need to subtract contact icon width twice from the horizontal space to get
- // the max leftover space because we want the message bubble to extend no further than the
- // starting position of the message bubble in the opposite direction.
- final int maxLeftoverSpace = horizontalSpace - mContactIconView.getMeasuredWidth() * 2
- - arrowWidth - getPaddingLeft() - getPaddingRight();
- final int messageContentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(maxLeftoverSpace,
- MeasureSpec.AT_MOST);
-
- mMessageBubble.measure(messageContentWidthMeasureSpec, unspecifiedMeasureSpec);
-
- final int maxHeight = Math.max(mContactIconView.getMeasuredHeight(),
- mMessageBubble.getMeasuredHeight());
- setMeasuredDimension(horizontalSpace, maxHeight + getPaddingBottom() + getPaddingTop());
- }
-
- @Override
- protected void onLayout(final boolean changed, final int left, final int top, final int right,
- final int bottom) {
- final boolean isRtl = AccessibilityUtil.isLayoutRtl(this);
-
- final int iconWidth = mContactIconView.getMeasuredWidth();
- final int iconHeight = mContactIconView.getMeasuredHeight();
- final int iconTop = getPaddingTop();
- final int contentWidth = (right -left) - iconWidth - getPaddingLeft() - getPaddingRight();
- final int contentHeight = mMessageBubble.getMeasuredHeight();
- final int contentTop = iconTop;
-
- final int iconLeft;
- final int contentLeft;
- if (mData.getIsIncoming()) {
- if (isRtl) {
- iconLeft = (right - left) - getPaddingRight() - iconWidth;
- contentLeft = iconLeft - contentWidth;
- } else {
- iconLeft = getPaddingLeft();
- contentLeft = iconLeft + iconWidth;
- }
- } else {
- if (isRtl) {
- iconLeft = getPaddingLeft();
- contentLeft = iconLeft + iconWidth;
- } else {
- iconLeft = (right - left) - getPaddingRight() - iconWidth;
- contentLeft = iconLeft - contentWidth;
- }
- }
-
- mContactIconView.layout(iconLeft, iconTop, iconLeft + iconWidth, iconTop + iconHeight);
-
- mMessageBubble.layout(contentLeft, contentTop, contentLeft + contentWidth,
- contentTop + contentHeight);
- }
-
- /**
- * Fills in the data associated with this view.
- *
- * @param cursor The cursor from a MessageList that this view is in, pointing to its entry.
- */
- public void bind(final Cursor cursor) {
- bind(cursor, true, null);
- }
-
- /**
- * Fills in the data associated with this view.
- *
- * @param cursor The cursor from a MessageList that this view is in, pointing to its entry.
- * @param oneOnOne Whether this is a 1:1 conversation
- */
- public void bind(final Cursor cursor,
- final boolean oneOnOne, final String selectedMessageId) {
- mOneOnOne = oneOnOne;
-
- // Update our UI model
- mData.bind(cursor);
- setSelected(TextUtils.equals(mData.getMessageId(), selectedMessageId));
-
- // Update text and image content for the view.
- updateViewContent();
-
- // Update colors and layout parameters for the view.
- updateViewAppearance();
-
- updateContentDescription();
- }
-
- public void setHost(final ConversationMessageViewHost host) {
- mHost = host;
- }
-
- /**
- * Sets a delay loader instance to manage loading / resuming of image attachments.
- */
- public void setImageViewDelayLoader(final AsyncImageViewDelayLoader delayLoader) {
- Assert.notNull(mMessageImageView);
- mMessageImageView.setDelayLoader(delayLoader);
- mMultiAttachmentView.setImageViewDelayLoader(delayLoader);
- }
-
- public ConversationMessageData getData() {
- return mData;
- }
-
- /**
- * Returns whether we should show simplified visual style for the message view (i.e. hide the
- * avatar and bubble arrow, reduce padding).
- */
- private boolean shouldShowSimplifiedVisualStyle() {
- return mData.getCanClusterWithPreviousMessage();
- }
-
- /**
- * Returns whether we need to show message bubble arrow. We don't show arrow if the message
- * contains media attachments or if shouldShowSimplifiedVisualStyle() is true.
- */
- private boolean shouldShowMessageBubbleArrow() {
- return !shouldShowSimplifiedVisualStyle()
- && !(mData.hasAttachments() || mMessageHasYouTubeLink);
- }
-
- /**
- * Returns whether we need to show a message bubble for text content.
- */
- private boolean shouldShowMessageTextBubble() {
- if (mData.hasText()) {
- return true;
- }
- final String subjectText = MmsUtils.cleanseMmsSubject(getResources(),
- mData.getMmsSubject());
- if (!TextUtils.isEmpty(subjectText)) {
- return true;
- }
- return false;
- }
-
- private void updateViewContent() {
- updateMessageContent();
- int titleResId = -1;
- int statusResId = -1;
- String statusText = null;
- switch(mData.getStatus()) {
- case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING:
- case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING:
- case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
- case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
- titleResId = R.string.message_title_downloading;
- statusResId = R.string.message_status_downloading;
- break;
-
- case MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD:
- if (!OsUtil.isSecondaryUser()) {
- titleResId = R.string.message_title_manual_download;
- if (isSelected()) {
- statusResId = R.string.message_status_download_action;
- } else {
- statusResId = R.string.message_status_download;
- }
- }
- break;
-
- case MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE:
- if (!OsUtil.isSecondaryUser()) {
- titleResId = R.string.message_title_download_failed;
- statusResId = R.string.message_status_download_error;
- }
- break;
-
- case MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED:
- if (!OsUtil.isSecondaryUser()) {
- titleResId = R.string.message_title_download_failed;
- if (isSelected()) {
- statusResId = R.string.message_status_download_action;
- } else {
- statusResId = R.string.message_status_download;
- }
- }
- break;
-
- case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND:
- case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
- statusResId = R.string.message_status_sending;
- break;
-
- case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
- case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
- statusResId = R.string.message_status_send_retrying;
- break;
-
- case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
- statusResId = R.string.message_status_send_failed_emergency_number;
- break;
-
- case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
- // don't show the error state unless we're the default sms app
- if (PhoneUtils.getDefault().isDefaultSmsApp()) {
- if (isSelected()) {
- statusResId = R.string.message_status_resend;
- } else {
- statusResId = MmsUtils.mapRawStatusToErrorResourceId(
- mData.getStatus(), mData.getRawTelephonyStatus());
- }
- break;
- }
- // FALL THROUGH HERE
-
- case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE:
- case MessageData.BUGLE_STATUS_INCOMING_COMPLETE:
- default:
- if (!mData.getCanClusterWithNextMessage()) {
- statusText = mData.getFormattedReceivedTimeStamp();
- }
- break;
- }
-
- final boolean titleVisible = (titleResId >= 0);
- if (titleVisible) {
- final String titleText = getResources().getString(titleResId);
- mTitleTextView.setText(titleText);
-
- final String mmsInfoText = getResources().getString(
- R.string.mms_info,
- Formatter.formatFileSize(getContext(), mData.getSmsMessageSize()),
- DateUtils.formatDateTime(
- getContext(),
- mData.getMmsExpiry(),
- DateUtils.FORMAT_SHOW_DATE |
- DateUtils.FORMAT_SHOW_TIME |
- DateUtils.FORMAT_NUMERIC_DATE |
- DateUtils.FORMAT_NO_YEAR));
- mMmsInfoTextView.setText(mmsInfoText);
- mMessageTitleLayout.setVisibility(View.VISIBLE);
- } else {
- mMessageTitleLayout.setVisibility(View.GONE);
- }
-
- final String subjectText = MmsUtils.cleanseMmsSubject(getResources(),
- mData.getMmsSubject());
- final boolean subjectVisible = !TextUtils.isEmpty(subjectText);
-
- final boolean senderNameVisible = !mOneOnOne && !mData.getCanClusterWithNextMessage()
- && mData.getIsIncoming();
- if (senderNameVisible) {
- mSenderNameTextView.setText(mData.getSenderDisplayName());
- mSenderNameTextView.setVisibility(View.VISIBLE);
- } else {
- mSenderNameTextView.setVisibility(View.GONE);
- }
-
- if (statusResId >= 0) {
- statusText = getResources().getString(statusResId);
- }
-
- // We set the text even if the view will be GONE for accessibility
- mStatusTextView.setText(statusText);
- final boolean statusVisible = !TextUtils.isEmpty(statusText);
- if (statusVisible) {
- mStatusTextView.setVisibility(View.VISIBLE);
- } else {
- mStatusTextView.setVisibility(View.GONE);
- }
-
- final boolean deliveredBadgeVisible =
- mData.getStatus() == MessageData.BUGLE_STATUS_OUTGOING_DELIVERED;
- mDeliveredBadge.setVisibility(deliveredBadgeVisible ? View.VISIBLE : View.GONE);
-
- // Update the sim indicator.
- final boolean showSimIconAsIncoming = mData.getIsIncoming() &&
- (!mData.hasAttachments() || shouldShowMessageTextBubble());
- final SubscriptionListEntry subscriptionEntry =
- mHost.getSubscriptionEntryForSelfParticipant(mData.getSelfParticipantId(),
- true /* excludeDefault */);
- final boolean simNameVisible = subscriptionEntry != null &&
- !TextUtils.isEmpty(subscriptionEntry.displayName) &&
- !mData.getCanClusterWithNextMessage();
- if (simNameVisible) {
- final String simNameText = mData.getIsIncoming() ? getResources().getString(
- R.string.incoming_sim_name_text, subscriptionEntry.displayName) :
- subscriptionEntry.displayName;
- mSimNameView.setText(simNameText);
- mSimNameView.setTextColor(showSimIconAsIncoming ? getResources().getColor(
- R.color.timestamp_text_incoming) : subscriptionEntry.displayColor);
- mSimNameView.setVisibility(VISIBLE);
- } else {
- mSimNameView.setText(null);
- mSimNameView.setVisibility(GONE);
- }
-
- final boolean metadataVisible = senderNameVisible || statusVisible
- || deliveredBadgeVisible || simNameVisible;
- mMessageMetadataView.setVisibility(metadataVisible ? View.VISIBLE : View.GONE);
-
- final boolean messageTextAndOrInfoVisible = titleVisible || subjectVisible
- || mData.hasText() || metadataVisible;
- mMessageTextAndInfoView.setVisibility(
- messageTextAndOrInfoVisible ? View.VISIBLE : View.GONE);
-
- if (shouldShowSimplifiedVisualStyle()) {
- mContactIconView.setVisibility(View.GONE);
- mContactIconView.setImageResourceUri(null);
- } else {
- mContactIconView.setVisibility(View.VISIBLE);
- final Uri avatarUri = AvatarUriUtil.createAvatarUri(
- mData.getSenderProfilePhotoUri(),
- mData.getSenderFullName(),
- mData.getSenderNormalizedDestination(),
- mData.getSenderContactLookupKey());
- mContactIconView.setImageResourceUri(avatarUri, mData.getSenderContactId(),
- mData.getSenderContactLookupKey(), mData.getSenderNormalizedDestination());
- }
- }
-
- private void updateMessageContent() {
- // We must update the text before the attachments since we search the text to see if we
- // should make a preview youtube image in the attachments
- updateMessageText();
- updateMessageAttachments();
- updateMessageSubject();
- mMessageBubble.bind(mData);
- }
-
- private void updateMessageAttachments() {
- // Bind video, audio, and VCard attachments. If there are multiple, they stack vertically.
- bindAttachmentsOfSameType(sVideoFilter,
- R.layout.message_video_attachment, mVideoViewBinder, VideoThumbnailView.class);
- bindAttachmentsOfSameType(sAudioFilter,
- R.layout.message_audio_attachment, mAudioViewBinder, AudioAttachmentView.class);
- bindAttachmentsOfSameType(sVCardFilter,
- R.layout.message_vcard_attachment, mVCardViewBinder, PersonItemView.class);
-
- // Bind image attachments. If there are multiple, they are shown in a collage view.
- final List<MessagePartData> imageParts = mData.getAttachments(sImageFilter);
- if (imageParts.size() > 1) {
- Collections.sort(imageParts, sImageComparator);
- mMultiAttachmentView.bindAttachments(imageParts, null, imageParts.size());
- mMultiAttachmentView.setVisibility(View.VISIBLE);
- } else {
- mMultiAttachmentView.setVisibility(View.GONE);
- }
-
- // In the case that we have no image attachments and exactly one youtube link in a message
- // then we will show a preview.
- String youtubeThumbnailUrl = null;
- String originalYoutubeLink = null;
- if (mMessageTextHasLinks && imageParts.size() == 0) {
- CharSequence messageTextWithSpans = mMessageTextView.getText();
- final URLSpan[] spans = ((Spanned) messageTextWithSpans).getSpans(0,
- messageTextWithSpans.length(), URLSpan.class);
- for (URLSpan span : spans) {
- String url = span.getURL();
- String youtubeLinkForUrl = YouTubeUtil.getYoutubePreviewImageLink(url);
- if (!TextUtils.isEmpty(youtubeLinkForUrl)) {
- if (TextUtils.isEmpty(youtubeThumbnailUrl)) {
- // Save the youtube link if we don't already have one
- youtubeThumbnailUrl = youtubeLinkForUrl;
- originalYoutubeLink = url;
- } else {
- // We already have a youtube link. This means we have two youtube links so
- // we shall show none.
- youtubeThumbnailUrl = null;
- originalYoutubeLink = null;
- break;
- }
- }
- }
- }
- // We need to keep track if we have a youtube link in the message so that we will not show
- // the arrow
- mMessageHasYouTubeLink = !TextUtils.isEmpty(youtubeThumbnailUrl);
-
- // We will show the message image view if there is one attachment or one youtube link
- if (imageParts.size() == 1 || mMessageHasYouTubeLink) {
- // Get the display metrics for a hint for how large to pull the image data into
- final WindowManager windowManager = (WindowManager) getContext().
- getSystemService(Context.WINDOW_SERVICE);
- final DisplayMetrics displayMetrics = new DisplayMetrics();
- windowManager.getDefaultDisplay().getMetrics(displayMetrics);
-
- final int iconSize = getResources()
- .getDimensionPixelSize(R.dimen.conversation_message_contact_icon_size);
- final int desiredWidth = displayMetrics.widthPixels - iconSize - iconSize;
-
- if (imageParts.size() == 1) {
- final MessagePartData imagePart = imageParts.get(0);
- // If the image is big, we want to scale it down to save memory since we're going to
- // scale it down to fit into the bubble width. We don't constrain the height.
- final ImageRequestDescriptor imageRequest =
- new MessagePartImageRequestDescriptor(imagePart,
- desiredWidth,
- MessagePartData.UNSPECIFIED_SIZE,
- false);
- adjustImageViewBounds(imagePart);
- mMessageImageView.setImageResourceId(imageRequest);
- mMessageImageView.setTag(imagePart);
- } else {
- // Youtube Thumbnail image
- final ImageRequestDescriptor imageRequest =
- new UriImageRequestDescriptor(Uri.parse(youtubeThumbnailUrl), desiredWidth,
- MessagePartData.UNSPECIFIED_SIZE, true /* allowCompression */,
- true /* isStatic */, false /* cropToCircle */,
- ImageUtils.DEFAULT_CIRCLE_BACKGROUND_COLOR /* circleBackgroundColor */,
- ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */);
- mMessageImageView.setImageResourceId(imageRequest);
- mMessageImageView.setTag(originalYoutubeLink);
- }
- mMessageImageView.setVisibility(View.VISIBLE);
- } else {
- mMessageImageView.setImageResourceId(null);
- mMessageImageView.setVisibility(View.GONE);
- }
-
- // Show the message attachments container if any of its children are visible
- boolean attachmentsVisible = false;
- for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) {
- final View attachmentView = mMessageAttachmentsView.getChildAt(i);
- if (attachmentView.getVisibility() == View.VISIBLE) {
- attachmentsVisible = true;
- break;
- }
- }
- mMessageAttachmentsView.setVisibility(attachmentsVisible ? View.VISIBLE : View.GONE);
- }
-
- private void bindAttachmentsOfSameType(final Predicate<MessagePartData> attachmentTypeFilter,
- final int attachmentViewLayoutRes, final AttachmentViewBinder viewBinder,
- final Class<?> attachmentViewClass) {
- final LayoutInflater layoutInflater = LayoutInflater.from(getContext());
-
- // Iterate through all attachments of a particular type (video, audio, etc).
- // Find the first attachment index that matches the given type if possible.
- int attachmentViewIndex = -1;
- View existingAttachmentView;
- do {
- existingAttachmentView = mMessageAttachmentsView.getChildAt(++attachmentViewIndex);
- } while (existingAttachmentView != null &&
- !(attachmentViewClass.isInstance(existingAttachmentView)));
-
- for (final MessagePartData attachment : mData.getAttachments(attachmentTypeFilter)) {
- View attachmentView = mMessageAttachmentsView.getChildAt(attachmentViewIndex);
- if (!attachmentViewClass.isInstance(attachmentView)) {
- attachmentView = layoutInflater.inflate(attachmentViewLayoutRes,
- mMessageAttachmentsView, false /* attachToRoot */);
- attachmentView.setOnClickListener(this);
- attachmentView.setOnLongClickListener(this);
- mMessageAttachmentsView.addView(attachmentView, attachmentViewIndex);
- }
- viewBinder.bindView(attachmentView, attachment);
- attachmentView.setTag(attachment);
- attachmentView.setVisibility(View.VISIBLE);
- attachmentViewIndex++;
- }
- // If there are unused views left over, unbind or remove them.
- while (attachmentViewIndex < mMessageAttachmentsView.getChildCount()) {
- final View attachmentView = mMessageAttachmentsView.getChildAt(attachmentViewIndex);
- if (attachmentViewClass.isInstance(attachmentView)) {
- mMessageAttachmentsView.removeViewAt(attachmentViewIndex);
- } else {
- // No more views of this type; we're done.
- break;
- }
- }
- }
-
- private void updateMessageSubject() {
- final String subjectText = MmsUtils.cleanseMmsSubject(getResources(),
- mData.getMmsSubject());
- final boolean subjectVisible = !TextUtils.isEmpty(subjectText);
-
- if (subjectVisible) {
- mSubjectText.setText(subjectText);
- mSubjectView.setVisibility(View.VISIBLE);
- } else {
- mSubjectView.setVisibility(View.GONE);
- }
- }
-
- private void updateMessageText() {
- final String text = mData.getText();
- if (!TextUtils.isEmpty(text)) {
- mMessageTextView.setText(text);
- // Linkify phone numbers, web urls, emails, and map addresses to allow users to
- // click on them and take the default intent.
- mMessageTextHasLinks = Linkify.addLinks(mMessageTextView, Linkify.ALL);
- mMessageTextView.setVisibility(View.VISIBLE);
- } else {
- mMessageTextView.setVisibility(View.GONE);
- mMessageTextHasLinks = false;
- }
- }
-
- private void updateViewAppearance() {
- final Resources res = getResources();
- final ConversationDrawables drawableProvider = ConversationDrawables.get();
- final boolean incoming = mData.getIsIncoming();
- final boolean outgoing = !incoming;
- final boolean showArrow = shouldShowMessageBubbleArrow();
-
- final int messageTopPaddingClustered =
- res.getDimensionPixelSize(R.dimen.message_padding_same_author);
- final int messageTopPaddingDefault =
- res.getDimensionPixelSize(R.dimen.message_padding_default);
- final int arrowWidth = res.getDimensionPixelOffset(R.dimen.message_bubble_arrow_width);
- final int messageTextMinHeightDefault = res.getDimensionPixelSize(
- R.dimen.conversation_message_contact_icon_size);
- final int messageTextLeftRightPadding = res.getDimensionPixelOffset(
- R.dimen.message_text_left_right_padding);
- final int textTopPaddingDefault = res.getDimensionPixelOffset(
- R.dimen.message_text_top_padding);
- final int textBottomPaddingDefault = res.getDimensionPixelOffset(
- R.dimen.message_text_bottom_padding);
-
- // These values depend on whether the message has text, attachments, or both.
- // We intentionally don't set defaults, so the compiler will tell us if we forget
- // to set one of them, or if we set one more than once.
- final int contentLeftPadding, contentRightPadding;
- final Drawable textBackground;
- final int textMinHeight;
- final int textTopMargin;
- final int textTopPadding, textBottomPadding;
- final int textLeftPadding, textRightPadding;
-
- if (mData.hasAttachments()) {
- if (shouldShowMessageTextBubble()) {
- // Text and attachment(s)
- contentLeftPadding = incoming ? arrowWidth : 0;
- contentRightPadding = outgoing ? arrowWidth : 0;
- textBackground = drawableProvider.getBubbleDrawable(
- isSelected(),
- incoming,
- false /* needArrow */,
- mData.hasIncomingErrorStatus());
- textMinHeight = messageTextMinHeightDefault;
- textTopMargin = messageTopPaddingClustered;
- textTopPadding = textTopPaddingDefault;
- textBottomPadding = textBottomPaddingDefault;
- textLeftPadding = messageTextLeftRightPadding;
- textRightPadding = messageTextLeftRightPadding;
- } else {
- // Attachment(s) only
- contentLeftPadding = incoming ? arrowWidth : 0;
- contentRightPadding = outgoing ? arrowWidth : 0;
- textBackground = null;
- textMinHeight = 0;
- textTopMargin = 0;
- textTopPadding = 0;
- textBottomPadding = 0;
- textLeftPadding = 0;
- textRightPadding = 0;
- }
- } else {
- // Text only
- contentLeftPadding = (!showArrow && incoming) ? arrowWidth : 0;
- contentRightPadding = (!showArrow && outgoing) ? arrowWidth : 0;
- textBackground = drawableProvider.getBubbleDrawable(
- isSelected(),
- incoming,
- shouldShowMessageBubbleArrow(),
- mData.hasIncomingErrorStatus());
- textMinHeight = messageTextMinHeightDefault;
- textTopMargin = 0;
- textTopPadding = textTopPaddingDefault;
- textBottomPadding = textBottomPaddingDefault;
- if (showArrow && incoming) {
- textLeftPadding = messageTextLeftRightPadding + arrowWidth;
- } else {
- textLeftPadding = messageTextLeftRightPadding;
- }
- if (showArrow && outgoing) {
- textRightPadding = messageTextLeftRightPadding + arrowWidth;
- } else {
- textRightPadding = messageTextLeftRightPadding;
- }
- }
-
- // These values do not depend on whether the message includes attachments
- final int gravity = incoming ? (Gravity.START | Gravity.CENTER_VERTICAL) :
- (Gravity.END | Gravity.CENTER_VERTICAL);
- final int messageTopPadding = shouldShowSimplifiedVisualStyle() ?
- messageTopPaddingClustered : messageTopPaddingDefault;
- final int metadataTopPadding = res.getDimensionPixelOffset(
- R.dimen.message_metadata_top_padding);
-
- // Update the message text/info views
- ImageUtils.setBackgroundDrawableOnView(mMessageTextAndInfoView, textBackground);
- mMessageTextAndInfoView.setMinimumHeight(textMinHeight);
- final LinearLayout.LayoutParams textAndInfoLayoutParams =
- (LinearLayout.LayoutParams) mMessageTextAndInfoView.getLayoutParams();
- textAndInfoLayoutParams.topMargin = textTopMargin;
-
- if (UiUtils.isRtlMode()) {
- // Need to switch right and left padding in RtL mode
- mMessageTextAndInfoView.setPadding(textRightPadding, textTopPadding, textLeftPadding,
- textBottomPadding);
- mMessageBubble.setPadding(contentRightPadding, 0, contentLeftPadding, 0);
- } else {
- mMessageTextAndInfoView.setPadding(textLeftPadding, textTopPadding, textRightPadding,
- textBottomPadding);
- mMessageBubble.setPadding(contentLeftPadding, 0, contentRightPadding, 0);
- }
-
- // Update the message row and message bubble views
- setPadding(getPaddingLeft(), messageTopPadding, getPaddingRight(), 0);
- mMessageBubble.setGravity(gravity);
- updateMessageAttachmentsAppearance(gravity);
-
- mMessageMetadataView.setPadding(0, metadataTopPadding, 0, 0);
-
- updateTextAppearance();
-
- requestLayout();
- }
-
- private void updateContentDescription() {
- StringBuilder description = new StringBuilder();
-
- Resources res = getResources();
- String separator = res.getString(R.string.enumeration_comma);
-
- // Sender information
- boolean hasPlainTextMessage = !(TextUtils.isEmpty(mData.getText()) ||
- mMessageTextHasLinks);
- if (mData.getIsIncoming()) {
- int senderResId = hasPlainTextMessage
- ? R.string.incoming_text_sender_content_description
- : R.string.incoming_sender_content_description;
- description.append(res.getString(senderResId, mData.getSenderDisplayName()));
- } else {
- int senderResId = hasPlainTextMessage
- ? R.string.outgoing_text_sender_content_description
- : R.string.outgoing_sender_content_description;
- description.append(res.getString(senderResId));
- }
-
- if (mSubjectView.getVisibility() == View.VISIBLE) {
- description.append(separator);
- description.append(mSubjectText.getText());
- }
-
- if (mMessageTextView.getVisibility() == View.VISIBLE) {
- // If the message has hyperlinks, we will let the user navigate to the text message so
- // that the hyperlink can be clicked. Otherwise, the text message does not need to
- // be reachable.
- if (mMessageTextHasLinks) {
- mMessageTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
- } else {
- mMessageTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
- description.append(separator);
- description.append(mMessageTextView.getText());
- }
- }
-
- if (mMessageTitleLayout.getVisibility() == View.VISIBLE) {
- description.append(separator);
- description.append(mTitleTextView.getText());
-
- description.append(separator);
- description.append(mMmsInfoTextView.getText());
- }
-
- if (mStatusTextView.getVisibility() == View.VISIBLE) {
- description.append(separator);
- description.append(mStatusTextView.getText());
- }
-
- if (mSimNameView.getVisibility() == View.VISIBLE) {
- description.append(separator);
- description.append(mSimNameView.getText());
- }
-
- if (mDeliveredBadge.getVisibility() == View.VISIBLE) {
- description.append(separator);
- description.append(res.getString(R.string.delivered_status_content_description));
- }
-
- setContentDescription(description);
- }
-
- private void updateMessageAttachmentsAppearance(final int gravity) {
- mMessageAttachmentsView.setGravity(gravity);
-
- // Tint image/video attachments when selected
- final int selectedImageTint = getResources().getColor(R.color.message_image_selected_tint);
- if (mMessageImageView.getVisibility() == View.VISIBLE) {
- if (isSelected()) {
- mMessageImageView.setColorFilter(selectedImageTint);
- } else {
- mMessageImageView.clearColorFilter();
- }
- }
- if (mMultiAttachmentView.getVisibility() == View.VISIBLE) {
- if (isSelected()) {
- mMultiAttachmentView.setColorFilter(selectedImageTint);
- } else {
- mMultiAttachmentView.clearColorFilter();
- }
- }
- for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) {
- final View attachmentView = mMessageAttachmentsView.getChildAt(i);
- if (attachmentView instanceof VideoThumbnailView
- && attachmentView.getVisibility() == View.VISIBLE) {
- final VideoThumbnailView videoView = (VideoThumbnailView) attachmentView;
- if (isSelected()) {
- videoView.setColorFilter(selectedImageTint);
- } else {
- videoView.clearColorFilter();
- }
- }
- }
-
- // If there are multiple attachment bubbles in a single message, add some separation.
- final int multipleAttachmentPadding =
- getResources().getDimensionPixelSize(R.dimen.message_padding_same_author);
-
- boolean previousVisibleView = false;
- for (int i = 0, size = mMessageAttachmentsView.getChildCount(); i < size; i++) {
- final View attachmentView = mMessageAttachmentsView.getChildAt(i);
- if (attachmentView.getVisibility() == View.VISIBLE) {
- final int margin = previousVisibleView ? multipleAttachmentPadding : 0;
- ((LinearLayout.LayoutParams) attachmentView.getLayoutParams()).topMargin = margin;
- // updateViewAppearance calls requestLayout() at the end, so we don't need to here
- previousVisibleView = true;
- }
- }
- }
-
- private void updateTextAppearance() {
- int messageColorResId;
- int statusColorResId = -1;
- int infoColorResId = -1;
- int timestampColorResId;
- int subjectLabelColorResId;
- if (isSelected()) {
- messageColorResId = R.color.message_text_color_incoming;
- statusColorResId = R.color.message_action_status_text;
- infoColorResId = R.color.message_action_info_text;
- if (shouldShowMessageTextBubble()) {
- timestampColorResId = R.color.message_action_timestamp_text;
- subjectLabelColorResId = R.color.message_action_timestamp_text;
- } else {
- // If there's no text, the timestamp will be shown below the attachments,
- // against the conversation view background.
- timestampColorResId = R.color.timestamp_text_outgoing;
- subjectLabelColorResId = R.color.timestamp_text_outgoing;
- }
- } else {
- messageColorResId = (mData.getIsIncoming() ?
- R.color.message_text_color_incoming : R.color.message_text_color_outgoing);
- statusColorResId = messageColorResId;
- infoColorResId = R.color.timestamp_text_incoming;
- switch(mData.getStatus()) {
-
- case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
- case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
- timestampColorResId = R.color.message_failed_timestamp_text;
- subjectLabelColorResId = R.color.timestamp_text_outgoing;
- break;
-
- case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND:
- case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
- case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
- case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
- case MessageData.BUGLE_STATUS_OUTGOING_COMPLETE:
- case MessageData.BUGLE_STATUS_OUTGOING_DELIVERED:
- timestampColorResId = R.color.timestamp_text_outgoing;
- subjectLabelColorResId = R.color.timestamp_text_outgoing;
- break;
-
- case MessageData.BUGLE_STATUS_INCOMING_EXPIRED_OR_NOT_AVAILABLE:
- case MessageData.BUGLE_STATUS_INCOMING_DOWNLOAD_FAILED:
- messageColorResId = R.color.message_text_color_incoming_download_failed;
- timestampColorResId = R.color.message_download_failed_timestamp_text;
- subjectLabelColorResId = R.color.message_text_color_incoming_download_failed;
- statusColorResId = R.color.message_download_failed_status_text;
- infoColorResId = R.color.message_info_text_incoming_download_failed;
- break;
-
- case MessageData.BUGLE_STATUS_INCOMING_AUTO_DOWNLOADING:
- case MessageData.BUGLE_STATUS_INCOMING_MANUAL_DOWNLOADING:
- case MessageData.BUGLE_STATUS_INCOMING_RETRYING_AUTO_DOWNLOAD:
- case MessageData.BUGLE_STATUS_INCOMING_RETRYING_MANUAL_DOWNLOAD:
- case MessageData.BUGLE_STATUS_INCOMING_YET_TO_MANUAL_DOWNLOAD:
- timestampColorResId = R.color.message_text_color_incoming;
- subjectLabelColorResId = R.color.message_text_color_incoming;
- infoColorResId = R.color.timestamp_text_incoming;
- break;
-
- case MessageData.BUGLE_STATUS_INCOMING_COMPLETE:
- default:
- timestampColorResId = R.color.timestamp_text_incoming;
- subjectLabelColorResId = R.color.timestamp_text_incoming;
- infoColorResId = -1; // Not used
- break;
- }
- }
- final int messageColor = getResources().getColor(messageColorResId);
- mMessageTextView.setTextColor(messageColor);
- mMessageTextView.setLinkTextColor(messageColor);
- mSubjectText.setTextColor(messageColor);
- if (statusColorResId >= 0) {
- mTitleTextView.setTextColor(getResources().getColor(statusColorResId));
- }
- if (infoColorResId >= 0) {
- mMmsInfoTextView.setTextColor(getResources().getColor(infoColorResId));
- }
- if (timestampColorResId == R.color.timestamp_text_incoming &&
- mData.hasAttachments() && !shouldShowMessageTextBubble()) {
- timestampColorResId = R.color.timestamp_text_outgoing;
- }
- mStatusTextView.setTextColor(getResources().getColor(timestampColorResId));
-
- mSubjectLabel.setTextColor(getResources().getColor(subjectLabelColorResId));
- mSenderNameTextView.setTextColor(getResources().getColor(timestampColorResId));
- }
-
- /**
- * If we don't know the size of the image, we want to show it in a fixed-sized frame to
- * avoid janks when the image is loaded and resized. Otherwise, we can set the imageview to
- * take on normal layout params.
- */
- private void adjustImageViewBounds(final MessagePartData imageAttachment) {
- Assert.isTrue(ContentType.isImageType(imageAttachment.getContentType()));
- final ViewGroup.LayoutParams layoutParams = mMessageImageView.getLayoutParams();
- if (imageAttachment.getWidth() == MessagePartData.UNSPECIFIED_SIZE ||
- imageAttachment.getHeight() == MessagePartData.UNSPECIFIED_SIZE) {
- // We don't know the size of the image attachment, enable letterboxing on the image
- // and show a fixed sized attachment. This should happen at most once per image since
- // after the image is loaded we then save the image dimensions to the db so that the
- // next time we can display the full size.
- layoutParams.width = getResources()
- .getDimensionPixelSize(R.dimen.image_attachment_fallback_width);
- layoutParams.height = getResources()
- .getDimensionPixelSize(R.dimen.image_attachment_fallback_height);
- mMessageImageView.setScaleType(ScaleType.CENTER_CROP);
- } else {
- layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
- layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
- // ScaleType.CENTER_INSIDE and FIT_CENTER behave similarly for most images. However,
- // FIT_CENTER works better for small images as it enlarges the image such that the
- // minimum size ("android:minWidth" etc) is honored.
- mMessageImageView.setScaleType(ScaleType.FIT_CENTER);
- }
- }
-
- @Override
- public void onClick(final View view) {
- final Object tag = view.getTag();
- if (tag instanceof MessagePartData) {
- final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view);
- onAttachmentClick((MessagePartData) tag, bounds, false /* longPress */);
- } else if (tag instanceof String) {
- // Currently the only object that would make a tag of a string is a youtube preview
- // image
- UIIntents.get().launchBrowserForUrl(getContext(), (String) tag);
- }
- }
-
- @Override
- public boolean onLongClick(final View view) {
- if (view == mMessageTextView) {
- // Preemptively handle the long click event on message text so it's not handled by
- // the link spans.
- return performLongClick();
- }
-
- final Object tag = view.getTag();
- if (tag instanceof MessagePartData) {
- final Rect bounds = UiUtils.getMeasuredBoundsOnScreen(view);
- return onAttachmentClick((MessagePartData) tag, bounds, true /* longPress */);
- }
-
- return false;
- }
-
- @Override
- public boolean onAttachmentClick(final MessagePartData attachment,
- final Rect viewBoundsOnScreen, final boolean longPress) {
- return mHost.onAttachmentClick(this, attachment, viewBoundsOnScreen, longPress);
- }
-
- public ContactIconView getContactIconView() {
- return mContactIconView;
- }
-
- // Sort photos in MultiAttachLayout in the same order as the ConversationImagePartsView
- static final Comparator<MessagePartData> sImageComparator = new Comparator<MessagePartData>(){
- @Override
- public int compare(final MessagePartData x, final MessagePartData y) {
- return x.getPartId().compareTo(y.getPartId());
- }
- };
-
- static final Predicate<MessagePartData> sVideoFilter = new Predicate<MessagePartData>() {
- @Override
- public boolean apply(final MessagePartData part) {
- return part.isVideo();
- }
- };
-
- static final Predicate<MessagePartData> sAudioFilter = new Predicate<MessagePartData>() {
- @Override
- public boolean apply(final MessagePartData part) {
- return part.isAudio();
- }
- };
-
- static final Predicate<MessagePartData> sVCardFilter = new Predicate<MessagePartData>() {
- @Override
- public boolean apply(final MessagePartData part) {
- return part.isVCard();
- }
- };
-
- static final Predicate<MessagePartData> sImageFilter = new Predicate<MessagePartData>() {
- @Override
- public boolean apply(final MessagePartData part) {
- return part.isImage();
- }
- };
-
- interface AttachmentViewBinder {
- void bindView(View view, MessagePartData attachment);
- void unbind(View view);
- }
-
- final AttachmentViewBinder mVideoViewBinder = new AttachmentViewBinder() {
- @Override
- public void bindView(final View view, final MessagePartData attachment) {
- ((VideoThumbnailView) view).setSource(attachment, mData.getIsIncoming());
- }
-
- @Override
- public void unbind(final View view) {
- ((VideoThumbnailView) view).setSource((Uri) null, mData.getIsIncoming());
- }
- };
-
- final AttachmentViewBinder mAudioViewBinder = new AttachmentViewBinder() {
- @Override
- public void bindView(final View view, final MessagePartData attachment) {
- final AudioAttachmentView audioView = (AudioAttachmentView) view;
- audioView.bindMessagePartData(attachment, isSelected() || mData.getIsIncoming());
- audioView.setBackground(ConversationDrawables.get().getBubbleDrawable(
- isSelected(), mData.getIsIncoming(), false /* needArrow */,
- mData.hasIncomingErrorStatus()));
- }
-
- @Override
- public void unbind(final View view) {
- ((AudioAttachmentView) view).bindMessagePartData(null, mData.getIsIncoming());
- }
- };
-
- final AttachmentViewBinder mVCardViewBinder = new AttachmentViewBinder() {
- @Override
- public void bindView(final View view, final MessagePartData attachment) {
- final PersonItemView personView = (PersonItemView) view;
- personView.bind(DataModel.get().createVCardContactItemData(getContext(),
- attachment));
- personView.setBackground(ConversationDrawables.get().getBubbleDrawable(
- isSelected(), mData.getIsIncoming(), false /* needArrow */,
- mData.hasIncomingErrorStatus()));
- final int nameTextColorRes;
- final int detailsTextColorRes;
- if (isSelected()) {
- nameTextColorRes = R.color.message_text_color_incoming;
- detailsTextColorRes = R.color.message_text_color_incoming;
- } else {
- nameTextColorRes = mData.getIsIncoming() ? R.color.message_text_color_incoming
- : R.color.message_text_color_outgoing;
- detailsTextColorRes = mData.getIsIncoming() ? R.color.timestamp_text_incoming
- : R.color.timestamp_text_outgoing;
- }
- personView.setNameTextColor(getResources().getColor(nameTextColorRes));
- personView.setDetailsTextColor(getResources().getColor(detailsTextColorRes));
- }
-
- @Override
- public void unbind(final View view) {
- ((PersonItemView) view).bind(null);
- }
- };
-
- /**
- * A helper class that allows us to handle long clicks on linkified message text view (i.e. to
- * select the message) so it's not handled by the link spans to launch apps for the links.
- */
- private static class IgnoreLinkLongClickHelper implements OnLongClickListener, OnTouchListener {
- private boolean mIsLongClick;
- private final OnLongClickListener mDelegateLongClickListener;
-
- /**
- * Ignore long clicks on linkified texts for a given text view.
- * @param textView the TextView to ignore long clicks on
- * @param longClickListener a delegate OnLongClickListener to be called when the view is
- * long clicked.
- */
- public static void ignoreLinkLongClick(final TextView textView,
- @Nullable final OnLongClickListener longClickListener) {
- final IgnoreLinkLongClickHelper helper =
- new IgnoreLinkLongClickHelper(longClickListener);
- textView.setOnLongClickListener(helper);
- textView.setOnTouchListener(helper);
- }
-
- private IgnoreLinkLongClickHelper(@Nullable final OnLongClickListener longClickListener) {
- mDelegateLongClickListener = longClickListener;
- }
-
- @Override
- public boolean onLongClick(final View v) {
- // Record that this click is a long click.
- mIsLongClick = true;
- if (mDelegateLongClickListener != null) {
- return mDelegateLongClickListener.onLongClick(v);
- }
- return false;
- }
-
- @Override
- public boolean onTouch(final View v, final MotionEvent event) {
- if (event.getActionMasked() == MotionEvent.ACTION_UP && mIsLongClick) {
- // This touch event is a long click, preemptively handle this touch event so that
- // the link span won't get a onClicked() callback.
- mIsLongClick = false;
- return true;
- }
-
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
- mIsLongClick = false;
- }
- return false;
- }
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/ConversationSimSelector.java b/src/com/android/messaging/ui/conversation/ConversationSimSelector.java
deleted file mode 100644
index fc43a46..0000000
--- a/src/com/android/messaging/ui/conversation/ConversationSimSelector.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.messaging.ui.conversation;
-
-import android.content.Context;
-import android.support.v4.util.Pair;
-import android.text.TextUtils;
-
-import com.android.messaging.Factory;
-import com.android.messaging.R;
-import com.android.messaging.datamodel.data.SubscriptionListData;
-import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
-import com.android.messaging.ui.conversation.SimSelectorView.SimSelectorViewListener;
-import com.android.messaging.util.AccessibilityUtil;
-import com.android.messaging.util.Assert;
-import com.android.messaging.util.OsUtil;
-import com.android.messaging.util.ThreadUtil;
-
-/**
- * Manages showing/hiding the SIM selector in conversation.
- */
-abstract class ConversationSimSelector extends ConversationInput {
- private SimSelectorView mSimSelectorView;
- private Pair<Boolean /* show */, Boolean /* animate */> mPendingShow;
- private boolean mDataReady;
- private String mSelectedSimText;
-
- public ConversationSimSelector(ConversationInputBase baseHost) {
- super(baseHost, false);
- }
-
- public void onSubscriptionListDataLoaded(final SubscriptionListData subscriptionListData) {
- ensureSimSelectorView();
- mSimSelectorView.bind(subscriptionListData);
- mDataReady = subscriptionListData != null && subscriptionListData.hasData();
- if (mPendingShow != null && mDataReady) {
- Assert.isTrue(OsUtil.isAtLeastL_MR1());
- final boolean show = mPendingShow.first;
- final boolean animate = mPendingShow.second;
- ThreadUtil.getMainThreadHandler().post(new Runnable() {
- @Override
- public void run() {
- // This will No-Op if we are no longer attached to the host.
- mConversationInputBase.showHideInternal(ConversationSimSelector.this,
- show, animate);
- }
- });
- mPendingShow = null;
- }
- }
-
- private void announcedSelectedSim() {
- final Context context = Factory.get().getApplicationContext();
- if (AccessibilityUtil.isTouchExplorationEnabled(context) &&
- !TextUtils.isEmpty(mSelectedSimText)) {
- AccessibilityUtil.announceForAccessibilityCompat(
- mSimSelectorView, null,
- context.getString(R.string.selected_sim_content_message, mSelectedSimText));
- }
- }
-
- public void setSelected(final SubscriptionListEntry subEntry) {
- mSelectedSimText = subEntry == null ? null : subEntry.displayName;
- }
-
- @Override
- public boolean show(boolean animate) {
- announcedSelectedSim();
- return showHide(true, animate);
- }
-
- @Override
- public boolean hide(boolean animate) {
- return showHide(false, animate);
- }
-
- private boolean showHide(final boolean show, final boolean animate) {
- if (!OsUtil.isAtLeastL_MR1()) {
- return false;
- }
-
- if (mDataReady) {
- mSimSelectorView.showOrHide(show, animate);
- return mSimSelectorView.isOpen() == show;
- } else {
- mPendingShow = Pair.create(show, animate);
- return false;
- }
- }
-
- private void ensureSimSelectorView() {
- if (mSimSelectorView == null) {
- // Grab the SIM selector view from the host. This class assumes ownership of it.
- mSimSelectorView = getSimSelectorView();
- mSimSelectorView.setItemLayoutId(getSimSelectorItemLayoutId());
- mSimSelectorView.setListener(new SimSelectorViewListener() {
-
- @Override
- public void onSimSelectorVisibilityChanged(boolean visible) {
- onVisibilityChanged(visible);
- }
-
- @Override
- public void onSimItemClicked(SubscriptionListEntry item) {
- selectSim(item);
- }
- });
- }
- }
-
- protected abstract SimSelectorView getSimSelectorView();
- protected abstract void selectSim(final SubscriptionListEntry item);
- protected abstract int getSimSelectorItemLayoutId();
-
-}
diff --git a/src/com/android/messaging/ui/conversation/EnterSelfPhoneNumberDialog.java b/src/com/android/messaging/ui/conversation/EnterSelfPhoneNumberDialog.java
deleted file mode 100644
index e3ad601..0000000
--- a/src/com/android/messaging/ui/conversation/EnterSelfPhoneNumberDialog.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.messaging.ui.conversation;
-
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.app.DialogFragment;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.widget.EditText;
-
-import com.android.messaging.R;
-import com.android.messaging.datamodel.ParticipantRefresh;
-import com.android.messaging.util.BuglePrefs;
-import com.android.messaging.util.UiUtils;
-
-/**
- * The dialog for the user to enter the phone number of their sim.
- */
-public class EnterSelfPhoneNumberDialog extends DialogFragment {
- private EditText mEditText;
- private int mSubId;
-
- public static EnterSelfPhoneNumberDialog newInstance(final int subId) {
- final EnterSelfPhoneNumberDialog dialog = new EnterSelfPhoneNumberDialog();
- dialog.mSubId = subId;
- return dialog;
- }
-
- @Override
- public Dialog onCreateDialog(final Bundle savedInstanceState) {
- final Context context = getActivity();
- final LayoutInflater inflater = LayoutInflater.from(context);
- mEditText = (EditText) inflater.inflate(R.layout.enter_phone_number_view, null, false);
-
- final AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(R.string.enter_phone_number_title)
- .setMessage(R.string.enter_phone_number_text)
- .setView(mEditText)
- .setNegativeButton(android.R.string.cancel,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(final DialogInterface dialog,
- final int button) {
- dismiss();
- }
- })
- .setPositiveButton(android.R.string.ok,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(final DialogInterface dialog,
- final int button) {
- final String newNumber = mEditText.getText().toString();
- dismiss();
- if (!TextUtils.isEmpty(newNumber)) {
- savePhoneNumberInPrefs(newNumber);
- // TODO: Remove this toast and just auto-send
- // the message instead
- UiUtils.showToast(
- R.string
- .toast_after_setting_default_sms_app_for_message_send);
- }
- }
- });
- return builder.create();
- }
-
- private void savePhoneNumberInPrefs(final String newPhoneNumber) {
- final BuglePrefs subPrefs = BuglePrefs.getSubscriptionPrefs(mSubId);
- subPrefs.putString(getString(R.string.mms_phone_number_pref_key),
- newPhoneNumber);
- // Update the self participants so the new phone number will be reflected
- // everywhere in the UI.
- ParticipantRefresh.refreshSelfParticipants();
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/LaunchConversationActivity.java b/src/com/android/messaging/ui/conversation/LaunchConversationActivity.java
deleted file mode 100644
index 8af9f75..0000000
--- a/src/com/android/messaging/ui/conversation/LaunchConversationActivity.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.messaging.ui.conversation;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.text.TextUtils;
-
-import com.android.messaging.Factory;
-import com.android.messaging.R;
-import com.android.messaging.datamodel.DataModel;
-import com.android.messaging.datamodel.binding.Binding;
-import com.android.messaging.datamodel.binding.BindingBase;
-import com.android.messaging.datamodel.data.LaunchConversationData;
-import com.android.messaging.ui.UIIntents;
-import com.android.messaging.util.ContentType;
-import com.android.messaging.util.LogUtil;
-import com.android.messaging.util.UiUtils;
-import com.android.messaging.util.UriUtil;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
-
-/**
- * Launches ConversationActivity for sending a message to, or viewing messages from, a specific
- * recipient.
- * <p>
- * (This activity should be marked noHistory="true" in AndroidManifest.xml)
- */
-public class LaunchConversationActivity extends Activity implements
- LaunchConversationData.LaunchConversationDataListener {
- static final String SMS_BODY = "sms_body";
- static final String ADDRESS = "address";
- final Binding<LaunchConversationData> mBinding = BindingBase.createBinding(this);
- String mSmsBody;
-
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if (UiUtils.redirectToPermissionCheckIfNeeded(this)) {
- return;
- }
-
- final Intent intent = getIntent();
- final String action = intent.getAction();
- if (Intent.ACTION_SENDTO.equals(action) || Intent.ACTION_VIEW.equals(action)) {
- String[] recipients = UriUtil.parseRecipientsFromSmsMmsUri(intent.getData());
- final boolean haveAddress = !TextUtils.isEmpty(intent.getStringExtra(ADDRESS));
- final boolean haveEmail = !TextUtils.isEmpty(intent.getStringExtra(Intent.EXTRA_EMAIL));
- if (recipients == null && (haveAddress || haveEmail)) {
- if (haveAddress) {
- recipients = new String[] { intent.getStringExtra(ADDRESS) };
- } else {
- recipients = new String[] { intent.getStringExtra(Intent.EXTRA_EMAIL) };
- }
- }
- mSmsBody = intent.getStringExtra(SMS_BODY);
- if (TextUtils.isEmpty(mSmsBody)) {
- // Used by intents sent from the web YouTube (and perhaps others).
- mSmsBody = getBody(intent.getData());
- if (TextUtils.isEmpty(mSmsBody)) {
- // If that fails, try yet another method apps use to share text
- if (ContentType.TEXT_PLAIN.equals(intent.getType())) {
- mSmsBody = intent.getStringExtra(Intent.EXTRA_TEXT);
- }
- }
- }
- if (recipients != null) {
- mBinding.bind(DataModel.get().createLaunchConversationData(this));
- mBinding.getData().getOrCreateConversation(mBinding, recipients);
- } else {
- // No recipients were specified in the intent.
- // Start a new conversation with contact picker. The new conversation will be
- // primed with the (optional) message in mSmsBody.
- onGetOrCreateNewConversation(null);
- }
- } else {
- LogUtil.w(LogUtil.BUGLE_TAG, "Unsupported conversation intent action : " + action);
- }
- // As of M, activities without a visible window must finish before onResume completes.
- finish();
- }
-
- private String getBody(final Uri uri) {
- if (uri == null) {
- return null;
- }
- String urlStr = uri.getSchemeSpecificPart();
- if (!urlStr.contains("?")) {
- return null;
- }
- urlStr = urlStr.substring(urlStr.indexOf('?') + 1);
- final String[] params = urlStr.split("&");
- for (final String p : params) {
- if (p.startsWith("body=")) {
- try {
- return URLDecoder.decode(p.substring(5), "UTF-8");
- } catch (final UnsupportedEncodingException e) {
- // Invalid URL, ignore
- }
- }
- }
- return null;
- }
-
- @Override
- public void onGetOrCreateNewConversation(final String conversationId) {
- final Context context = Factory.get().getApplicationContext();
- UIIntents.get().launchConversationActivityWithParentStack(context, conversationId,
- mSmsBody);
- }
-
- @Override
- public void onGetOrCreateNewConversationFailed() {
- UiUtils.showToastAtBottom(R.string.conversation_creation_failure);
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/MessageBubbleBackground.java b/src/com/android/messaging/ui/conversation/MessageBubbleBackground.java
deleted file mode 100644
index 4c22970..0000000
--- a/src/com/android/messaging/ui/conversation/MessageBubbleBackground.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.messaging.ui.conversation;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.LinearLayout;
-
-import com.android.messaging.R;
-
-public class MessageBubbleBackground extends LinearLayout {
- private final int mSnapWidthPixels;
-
- public MessageBubbleBackground(Context context, AttributeSet attrs) {
- super(context, attrs);
- mSnapWidthPixels = context.getResources().getDimensionPixelSize(
- R.dimen.conversation_bubble_width_snap);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- final int widthPadding = getPaddingLeft() + getPaddingRight();
- int bubbleWidth = getMeasuredWidth() - widthPadding;
- final int maxWidth = MeasureSpec.getSize(widthMeasureSpec) - widthPadding;
- // Round up to next snapWidthPixels
- bubbleWidth = Math.min(maxWidth,
- (int) (Math.ceil(bubbleWidth / (float) mSnapWidthPixels) * mSnapWidthPixels));
- super.onMeasure(
- MeasureSpec.makeMeasureSpec(bubbleWidth + widthPadding, MeasureSpec.EXACTLY),
- heightMeasureSpec);
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/MessageDetailsDialog.java b/src/com/android/messaging/ui/conversation/MessageDetailsDialog.java
deleted file mode 100644
index 89b9148..0000000
--- a/src/com/android/messaging/ui/conversation/MessageDetailsDialog.java
+++ /dev/null
@@ -1,381 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.messaging.ui.conversation;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.res.Resources;
-import android.net.Uri;
-import android.text.TextUtils;
-import android.text.format.Formatter;
-
-import com.android.messaging.Factory;
-import com.android.messaging.R;
-import com.android.messaging.datamodel.BugleDatabaseOperations;
-import com.android.messaging.datamodel.DataModel;
-import com.android.messaging.datamodel.data.ConversationMessageData;
-import com.android.messaging.datamodel.data.ConversationParticipantsData;
-import com.android.messaging.datamodel.data.ParticipantData;
-import com.android.messaging.mmslib.pdu.PduHeaders;
-import com.android.messaging.sms.DatabaseMessages.MmsMessage;
-import com.android.messaging.sms.MmsUtils;
-import com.android.messaging.util.Assert;
-import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
-import com.android.messaging.util.Dates;
-import com.android.messaging.util.DebugUtils;
-import com.android.messaging.util.OsUtil;
-import com.android.messaging.util.PhoneUtils;
-import com.android.messaging.util.SafeAsyncTask;
-
-import java.util.List;
-
-public class MessageDetailsDialog {
- private static final String RECIPIENT_SEPARATOR = ", ";
-
- // All methods are static, no creating this class
- private MessageDetailsDialog() {
- }
-
- public static void show(final Context context, final ConversationMessageData data,
- final ConversationParticipantsData participants, final ParticipantData self) {
- if (DebugUtils.isDebugEnabled()) {
- new SafeAsyncTask<Void, Void, String>() {
- @Override
- protected String doInBackgroundTimed(Void... params) {
- return getMessageDetails(context, data, participants, self);
- }
-
- @Override
- protected void onPostExecute(String messageDetails) {
- showDialog(context, messageDetails);
- }
- }.executeOnThreadPool(null, null, null);
- } else {
- String messageDetails = getMessageDetails(context, data, participants, self);
- showDialog(context, messageDetails);
- }
- }
-
- private static String getMessageDetails(final Context context,
- final ConversationMessageData data,
- final ConversationParticipantsData participants, final ParticipantData self) {
- String messageDetails = null;
- if (data.getIsSms()) {
- messageDetails = getSmsMessageDetails(data, participants, self);
- } else {
- // TODO: Handle SMS_TYPE_MMS_PUSH_NOTIFICATION type differently?
- messageDetails = getMmsMessageDetails(context, data, participants, self);
- }
-
- return messageDetails;
- }
-
- private static void showDialog(final Context context, String messageDetails) {
- if (!TextUtils.isEmpty(messageDetails)) {
- new AlertDialog.Builder(context)
- .setTitle(R.string.message_details_title)
- .setMessage(messageDetails)
- .setCancelable(true)
- .show();
- }
- }
-
- /**
- * Return a string, separated by newlines, that contains a number of labels and values
- * for this sms message. The string will be displayed in a modal dialog.
- * @return string list of various message properties
- */
- private static String getSmsMessageDetails(final ConversationMessageData data,
- final ConversationParticipantsData participants, final ParticipantData self) {
- final Resources res = Factory.get().getApplicationContext().getResources();
- final StringBuilder details = new StringBuilder();
-
- // Type: Text message
- details.append(res.getString(R.string.message_type_label));
- details.append(res.getString(R.string.text_message));
-
- // From: +1425xxxxxxx
- // or To: +1425xxxxxxx
- final String rawSender = data.getSenderNormalizedDestination();
- if (!TextUtils.isEmpty(rawSender)) {
- details.append('\n');
- details.append(res.getString(R.string.from_label));
- details.append(rawSender);
- }
- final String rawRecipients = getRecipientParticipantString(participants,
- data.getParticipantId(), data.getIsIncoming(), data.getSelfParticipantId());
- if (!TextUtils.isEmpty(rawRecipients)) {
- details.append('\n');
- details.append(res.getString(R.string.to_address_label));
- details.append(rawRecipients);
- }
-
- // Sent: Mon 11:42AM
- if (data.getIsIncoming()) {
- if (data.getSentTimeStamp() != MmsUtils.INVALID_TIMESTAMP) {
- details.append('\n');
- details.append(res.getString(R.string.sent_label));
- details.append(
- Dates.getMessageDetailsTimeString(data.getSentTimeStamp()).toString());
- }
- }
-
- // Sent: Mon 11:43AM
- // or Received: Mon 11:43AM
- appendSentOrReceivedTimestamp(res, details, data);
-
- appendSimInfo(res, self, details);
-
- if (DebugUtils.isDebugEnabled()) {
- appendDebugInfo(details, data);
- }
-
- return details.toString();
- }
-
- /**
- * Return a string, separated by newlines, that contains a number of labels and values
- * for this mms message. The string will be displayed in a modal dialog.
- * @return string list of various message properties
- */
- private static String getMmsMessageDetails(Context context, final ConversationMessageData data,
- final ConversationParticipantsData participants, final ParticipantData self) {
- final Resources res = Factory.get().getApplicationContext().getResources();
- // TODO: when we support non-auto-download of mms messages, we'll have to handle
- // the case when the message is a PduHeaders.MESSAGE_TYPE_NOTIFICATION_IND and display
- // something different. See the Messaging app's MessageUtils.getNotificationIndDetails()
-
- final StringBuilder details = new StringBuilder();
-
- // Type: Multimedia message.
- details.append(res.getString(R.string.message_type_label));
- details.append(res.getString(R.string.multimedia_message));
-
- // From: +1425xxxxxxx
- final String rawSender = data.getSenderNormalizedDestination();
- details.append('\n');
- details.append(res.getString(R.string.from_label));
- details.append(!TextUtils.isEmpty(rawSender) ? rawSender :
- res.getString(R.string.hidden_sender_address));
-
- // To: +1425xxxxxxx
- final String rawRecipients = getRecipientParticipantString(participants,
- data.getParticipantId(), data.getIsIncoming(), data.getSelfParticipantId());
- if (!TextUtils.isEmpty(rawRecipients)) {
- details.append('\n');
- details.append(res.getString(R.string.to_address_label));
- details.append(rawRecipients);
- }
-
- // Sent: Tue 3:05PM
- // or Received: Tue 3:05PM
- appendSentOrReceivedTimestamp(res, details, data);
-
- // Subject: You're awesome
- details.append('\n');
- details.append(res.getString(R.string.subject_label));
- if (!TextUtils.isEmpty(MmsUtils.cleanseMmsSubject(res, data.getMmsSubject()))) {
- details.append(data.getMmsSubject());
- }
-
- // Priority: High/Normal/Low
- details.append('\n');
- details.append(res.getString(R.string.priority_label));
- details.append(getPriorityDescription(res, data.getSmsPriority()));
-
- // Message size: 30 KB
- if (data.getSmsMessageSize() > 0) {
- details.append('\n');
- details.append(res.getString(R.string.message_size_label));
- details.append(Formatter.formatFileSize(context, data.getSmsMessageSize()));
- }
-
- appendSimInfo(res, self, details);
-
- if (DebugUtils.isDebugEnabled()) {
- appendDebugInfo(details, data);
- }
-
- return details.toString();
- }
-
- private static void appendSentOrReceivedTimestamp(Resources res, StringBuilder details,
- ConversationMessageData data) {
- int labelId = -1;
- if (data.getIsIncoming()) {
- labelId = R.string.received_label;
- } else if (data.getIsSendComplete()) {
- labelId = R.string.sent_label;
- }
- if (labelId >= 0) {
- details.append('\n');
- details.append(res.getString(labelId));
- details.append(
- Dates.getMessageDetailsTimeString(data.getReceivedTimeStamp()).toString());
- }
- }
-
- @DoesNotRunOnMainThread
- private static void appendDebugInfo(StringBuilder details, ConversationMessageData data) {
- // We grab the thread id from the database, so this needs to run in the background
- Assert.isNotMainThread();
- details.append("\n\n");
- details.append("DEBUG");
-
- details.append('\n');
- details.append("Message id: ");
- details.append(data.getMessageId());
-
- final String telephonyUri = data.getSmsMessageUri();
- details.append('\n');
- details.append("Telephony uri: ");
- details.append(telephonyUri);
-
- final String conversationId = data.getConversationId();
-
- if (conversationId == null) {
- return;
- }
-
- details.append('\n');
- details.append("Conversation id: ");
- details.append(conversationId);
-
- final long threadId = BugleDatabaseOperations.getThreadId(DataModel.get().getDatabase(),
- conversationId);
-
- details.append('\n');
- details.append("Conversation telephony thread id: ");
- details.append(threadId);
-
- MmsMessage mms = null;
-
- if (data.getIsMms()) {
- if (telephonyUri == null) {
- return;
- }
- mms = MmsUtils.loadMms(Uri.parse(telephonyUri));
- if (mms == null) {
- return;
- }
-
- // We log the thread id again to check that they are internally consistent
- final long mmsThreadId = mms.mThreadId;
- details.append('\n');
- details.append("Telephony thread id: ");
- details.append(mmsThreadId);
-
- // Log the MMS content location
- final String mmsContentLocation = mms.mContentLocation;
- details.append('\n');
- details.append("Content location URL: ");
- details.append(mmsContentLocation);
- }
-
- final String recipientsString = MmsUtils.getRawRecipientIdsForThread(threadId);
- if (recipientsString != null) {
- details.append('\n');
- details.append("Thread recipient ids: ");
- details.append(recipientsString);
- }
-
- final List<String> recipients = MmsUtils.getRecipientsByThread(threadId);
- if (recipients != null) {
- details.append('\n');
- details.append("Thread recipients: ");
- details.append(recipients.toString());
-
- if (mms != null) {
- final String from = MmsUtils.getMmsSender(recipients, mms.getUri());
- details.append('\n');
- details.append("Sender: ");
- details.append(from);
- }
- }
- }
-
- private static String getRecipientParticipantString(
- final ConversationParticipantsData participants, final String senderId,
- final boolean addSelf, final String selfId) {
- final StringBuilder recipients = new StringBuilder();
- for (final ParticipantData participant : participants) {
- if (TextUtils.equals(participant.getId(), senderId)) {
- // Don't add sender
- continue;
- }
- if (participant.isSelf() &&
- (!participant.getId().equals(selfId) || !addSelf)) {
- // For self participants, don't add the one that's not relevant to this message
- // or if we are asked not to add self
- continue;
- }
- final String phoneNumber = participant.getNormalizedDestination();
- // Don't add empty number. This should not happen. But if that happens
- // we should not add it.
- if (!TextUtils.isEmpty(phoneNumber)) {
- if (recipients.length() > 0) {
- recipients.append(RECIPIENT_SEPARATOR);
- }
- recipients.append(phoneNumber);
- }
- }
- return recipients.toString();
- }
-
- /**
- * Convert the numeric mms priority into a human-readable string
- * @param res
- * @param priorityValue coded PduHeader priority
- * @return string representation of the priority
- */
- private static String getPriorityDescription(final Resources res, final int priorityValue) {
- switch(priorityValue) {
- case PduHeaders.PRIORITY_HIGH:
- return res.getString(R.string.priority_high);
- case PduHeaders.PRIORITY_LOW:
- return res.getString(R.string.priority_low);
- case PduHeaders.PRIORITY_NORMAL:
- default:
- return res.getString(R.string.priority_normal);
- }
- }
-
- private static void appendSimInfo(final Resources res,
- final ParticipantData self, final StringBuilder outString) {
- if (!OsUtil.isAtLeastL_MR1()
- || self == null
- || PhoneUtils.getDefault().getActiveSubscriptionCount() < 2) {
- return;
- }
- // The appended SIM info would look like:
- // SIM: SUB 01
- // or SIM: SIM 1
- // or SIM: Unknown
- Assert.isTrue(self.isSelf());
- outString.append('\n');
- outString.append(res.getString(R.string.sim_label));
- if (self.isActiveSubscription() && !self.isDefaultSelf()) {
- final String subscriptionName = self.getSubscriptionName();
- if (TextUtils.isEmpty(subscriptionName)) {
- outString.append(res.getString(R.string.sim_slot_identifier,
- self.getDisplaySlotId()));
- } else {
- outString.append(subscriptionName);
- }
- }
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/SimIconView.java b/src/com/android/messaging/ui/conversation/SimIconView.java
deleted file mode 100644
index e2e446c..0000000
--- a/src/com/android/messaging/ui/conversation/SimIconView.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.messaging.ui.conversation;
-
-import android.content.Context;
-import android.graphics.Outline;
-import android.net.Uri;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewOutlineProvider;
-
-import com.android.messaging.ui.ContactIconView;
-import com.android.messaging.util.Assert;
-import com.android.messaging.util.AvatarUriUtil;
-import com.android.messaging.util.OsUtil;
-
-/**
- * Shows SIM avatar icon in the SIM switcher / Self-send button.
- */
-public class SimIconView extends ContactIconView {
- public SimIconView(Context context, AttributeSet attrs) {
- super(context, attrs);
- if (OsUtil.isAtLeastL()) {
- setOutlineProvider(new ViewOutlineProvider() {
- @Override
- public void getOutline(View v, Outline outline) {
- outline.setOval(0, 0, v.getWidth(), v.getHeight());
- }
- });
- }
- }
-
- @Override
- protected void maybeInitializeOnClickListener() {
- // TODO: SIM icon view shouldn't consume or handle clicks, but it should if
- // this is the send button for the only SIM in the device or if MSIM is not supported.
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/SimSelectorItemView.java b/src/com/android/messaging/ui/conversation/SimSelectorItemView.java
deleted file mode 100644
index 3058d31..0000000
--- a/src/com/android/messaging/ui/conversation/SimSelectorItemView.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.messaging.ui.conversation;
-
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.messaging.R;
-import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
-import com.android.messaging.util.Assert;
-
-/**
- * Shows a view for a SIM in the SIM selector.
- */
-public class SimSelectorItemView extends LinearLayout {
- public interface HostInterface {
- void onSimItemClicked(SubscriptionListEntry item);
- }
-
- private SubscriptionListEntry mData;
- private TextView mNameTextView;
- private TextView mDetailsTextView;
- private SimIconView mSimIconView;
- private HostInterface mHost;
-
- public SimSelectorItemView(final Context context, final AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onFinishInflate() {
- mNameTextView = (TextView) findViewById(R.id.name);
- mDetailsTextView = (TextView) findViewById(R.id.details);
- mSimIconView = (SimIconView) findViewById(R.id.sim_icon);
- setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- mHost.onSimItemClicked(mData);
- }
- });
- }
-
- public void bind(final SubscriptionListEntry simEntry) {
- Assert.notNull(simEntry);
- mData = simEntry;
- updateViewAppearance();
- }
-
- public void setHostInterface(final HostInterface host) {
- mHost = host;
- }
-
- private void updateViewAppearance() {
- Assert.notNull(mData);
- final String displayName = mData.displayName;
- if (TextUtils.isEmpty(displayName)) {
- mNameTextView.setVisibility(GONE);
- } else {
- mNameTextView.setVisibility(VISIBLE);
- mNameTextView.setText(displayName);
- }
-
- final String details = mData.displayDestination;
- if (TextUtils.isEmpty(details)) {
- mDetailsTextView.setVisibility(GONE);
- } else {
- mDetailsTextView.setVisibility(VISIBLE);
- mDetailsTextView.setText(details);
- }
-
- mSimIconView.setImageResourceUri(mData.iconUri);
- }
-}
diff --git a/src/com/android/messaging/ui/conversation/SimSelectorView.java b/src/com/android/messaging/ui/conversation/SimSelectorView.java
deleted file mode 100644
index b07ff19..0000000
--- a/src/com/android/messaging/ui/conversation/SimSelectorView.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.messaging.ui.conversation;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.Animation;
-import android.view.animation.TranslateAnimation;
-import android.widget.ArrayAdapter;
-import android.widget.FrameLayout;
-import android.widget.ListView;
-
-import com.android.messaging.R;
-import com.android.messaging.datamodel.data.SubscriptionListData;
-import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
-import com.android.messaging.util.UiUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Displays a SIM selector above the compose message view and overlays the message list.
- */
-public class SimSelectorView extends FrameLayout implements SimSelectorItemView.HostInterface {
- public interface SimSelectorViewListener {
- void onSimItemClicked(SubscriptionListEntry item);
- void onSimSelectorVisibilityChanged(boolean visible);
- }
-
- private ListView mSimListView;
- private final SimSelectorAdapter mAdapter;
- private boolean mShow;
- private SimSelectorViewListener mListener;
- private int mItemLayoutId;
-
- public SimSelectorView(Context context, AttributeSet attrs) {
- super(context, attrs);
- mAdapter = new SimSelectorAdapter(getContext());
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mSimListView = (ListView) findViewById(R.id.sim_list);
- mSimListView.setAdapter(mAdapter);
-
- // Clicking anywhere outside the switcher list should dismiss.
- setOnClickListener(new OnClickListener() {
- @Override
- public void onClick(View v) {
- showOrHide(false, true);
- }
- });
- }
-
- public void bind(final SubscriptionListData data) {
- mAdapter.bindData(data.getActiveSubscriptionEntriesExcludingDefault());
- }
-
- public void setItemLayoutId(final int layoutId) {
- mItemLayoutId = layoutId;
- }
-
- public void setListener(final SimSelectorViewListener listener) {
- mListener = listener;
- }
-
- public void toggleVisibility() {
- showOrHide(!mShow, true);
- }
-
- public void showOrHide(final boolean show, final boolean animate) {
- final boolean oldShow = mShow;
- mShow = show && mAdapter.getCount() > 1;
- if (oldShow != mShow) {
- if (mListener != null) {
- mListener.onSimSelectorVisibilityChanged(mShow);
- }
-
- if (animate) {
- // Fade in the background pane.
- setVisibility(VISIBLE);
- setAlpha(mShow ? 0.0f : 1.0f);
- animate().alpha(mShow ? 1.0f : 0.0f)
- .setDuration(UiUtils.REVEAL_ANIMATION_DURATION)
- .withEndAction(new Runnable() {
- @Override
- public void run() {
- setAlpha(1.0f);
- setVisibility(mShow ? VISIBLE : GONE);
- }
- });
- } else {
- setVisibility(mShow ? VISIBLE : GONE);
- }
-
- // Slide in the SIM selector list via a translate animation.
- mSimListView.setVisibility(mShow ? VISIBLE : GONE);
- if (animate) {
- mSimListView.clearAnimation();
- final TranslateAnimation translateAnimation = new TranslateAnimation(
- Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
- Animation.RELATIVE_TO_SELF, mShow ? 1.0f : 0.0f,
- Animation.RELATIVE_TO_SELF, mShow ? 0.0f : 1.0f);
- translateAnimation.setInterpolator(UiUtils.EASE_OUT_INTERPOLATOR);
- translateAnimation.setDuration(UiUtils.REVEAL_ANIMATION_DURATION);
- mSimListView.startAnimation(translateAnimation);
- }
- }
- }
-
- /**
- * An adapter that takes a list of SubscriptionListEntry and displays them as a list of
- * available SIMs in the SIM selector.
- */
- private class SimSelectorAdapter extends ArrayAdapter<SubscriptionListEntry> {
- public SimSelectorAdapter(final Context context) {
- super(context, R.layout.sim_selector_item_view, new ArrayList<SubscriptionListEntry>());
- }
-
- public void bindData(final List<SubscriptionListEntry> newList) {
- clear();
- addAll(newList);
- notifyDataSetChanged();
- }
-
- @Override
- public View getView(final int position, final View convertView, final ViewGroup parent) {
- SimSelectorItemView itemView;
- if (convertView != null && convertView instanceof SimSelectorItemView) {
- itemView = (SimSelectorItemView) convertView;
- } else {
- final LayoutInflater inflater = (LayoutInflater) getContext()
- .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- itemView = (SimSelectorItemView) inflater.inflate(mItemLayoutId,
- parent, false);
- itemView.setHostInterface(SimSelectorView.this);
- }
- itemView.bind(getItem(position));
- return itemView;
- }
- }
-
- @Override
- public void onSimItemClicked(SubscriptionListEntry item) {
- mListener.onSimItemClicked(item);
- showOrHide(false, true);
- }
-
- public boolean isOpen() {
- return mShow;
- }
-}