blob: 6eb708922fa87f5f04bb7b663f76dae07e2a7888 [file] [log] [blame]
/*
* 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 androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.core.text.BidiFormatter;
import androidx.core.text.TextDirectionHeuristicsCompat;
import androidx.appcompat.app.ActionBar;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import android.telephony.PhoneNumberUtils;
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 (isBound() && 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);
dispatchAddFinished(holder);
}
});
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);
// Can't make a call to emergency numbers using ACTION_CALL.
if (PhoneNumberUtils.isEmergencyNumber(phoneNumber)) {
UiUtils.showToast(R.string.disallow_emergency_call);
} else {
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;
}
}