diff options
Diffstat (limited to 'src/com/android/messaging/ui/conversation')
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; - } -} |