package com.moez.QKSMS.ui.messagelist; import android.app.AlertDialog; import android.app.LoaderManager; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.CursorLoader; import android.content.DialogInterface; import android.content.Intent; import android.content.Loader; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.database.sqlite.SqliteWrapper; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorEventListener; import android.hardware.SensorManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Vibrator; import android.provider.Telephony; import android.support.annotation.IntegerRes; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.URLSpan; import android.text.util.Linkify; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.Toast; import com.google.android.mms.ContentType; import com.moez.QKSMS.LogTag; import com.moez.QKSMS.MmsConfig; import com.moez.QKSMS.QKSMSApp; import com.moez.QKSMS.R; import com.moez.QKSMS.common.CIELChEvaluator; import com.moez.QKSMS.common.ConversationPrefsHelper; import com.moez.QKSMS.common.DialogHelper; import com.moez.QKSMS.common.LiveViewManager; import com.moez.QKSMS.common.QKPreferences; import com.moez.QKSMS.common.utils.KeyboardUtils; import com.moez.QKSMS.common.utils.MessageUtils; import com.moez.QKSMS.common.vcard.ContactOperations; import com.moez.QKSMS.data.Contact; import com.moez.QKSMS.data.ContactList; import com.moez.QKSMS.data.Conversation; import com.moez.QKSMS.data.ConversationLegacy; import com.moez.QKSMS.data.Message; import com.moez.QKSMS.enums.QKPreference; import com.moez.QKSMS.interfaces.ActivityLauncher; import com.moez.QKSMS.transaction.NotificationManager; import com.moez.QKSMS.transaction.SmsHelper; import com.moez.QKSMS.ui.MainActivity; import com.moez.QKSMS.ui.SwipeBackLayout; import com.moez.QKSMS.ui.ThemeManager; import com.moez.QKSMS.ui.base.QKFragment; import com.moez.QKSMS.ui.base.RecyclerCursorAdapter; import com.moez.QKSMS.ui.delivery.DeliveryReportHelper; import com.moez.QKSMS.ui.delivery.DeliveryReportItem; import com.moez.QKSMS.ui.dialog.AsyncDialog; import com.moez.QKSMS.ui.dialog.ConversationSettingsDialog; import com.moez.QKSMS.ui.dialog.QKDialog; import com.moez.QKSMS.ui.dialog.conversationdetails.ConversationDetailsDialog; import com.moez.QKSMS.ui.settings.SettingsFragment; import com.moez.QKSMS.ui.view.ComposeView; import com.moez.QKSMS.ui.view.MessageListRecyclerView; import com.moez.QKSMS.ui.view.SmoothLinearLayoutManager; import com.moez.QKSMS.ui.widget.WidgetProvider; import ezvcard.Ezvcard; import ezvcard.VCard; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import static android.R.attr.data; public class MessageListFragment extends QKFragment implements ActivityLauncher, SensorEventListener, LoaderManager.LoaderCallbacks<Cursor>, RecyclerCursorAdapter.MultiSelectListener, SwipeBackLayout.ScrollChangedListener, RecyclerCursorAdapter.ItemClickListener<MessageItem> { public static final String TAG = "MessageListFragment"; private static final int MESSAGE_LIST_QUERY_TOKEN = 9527; private static final int MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN = 9528; private static final int DELETE_MESSAGE_TOKEN = 9700; private static final int MENU_EDIT_MESSAGE = 14; private static final int MENU_VIEW_SLIDESHOW = 16; private static final int MENU_VIEW_MESSAGE_DETAILS = 17; private static final int MENU_DELETE_MESSAGE = 18; private static final int MENU_SEARCH = 19; private static final int MENU_DELIVERY_REPORT = 20; private static final int MENU_FORWARD_MESSAGE = 21; private static final int MENU_CALL_BACK = 22; private static final int MENU_SEND_EMAIL = 23; private static final int MENU_COPY_MESSAGE_TEXT = 24; private static final int MENU_COPY_TO_SDCARD = 25; private static final int MENU_ADD_ADDRESS_TO_CONTACTS = 27; private static final int MENU_LOCK_MESSAGE = 28; private static final int MENU_UNLOCK_MESSAGE = 29; private static final int MENU_SAVE_RINGTONE = 30; private static final int MENU_PREFERENCES = 31; private static final int MENU_GROUP_PARTICIPANTS = 32; private boolean mIsSmsEnabled; private Cursor mCursor; private CIELChEvaluator mCIELChEvaluator; private MessageListAdapter mAdapter; private SmoothLinearLayoutManager mLayoutManager; private MessageListRecyclerView mRecyclerView; private Conversation mConversation; private ConversationLegacy mConversationLegacy; private Sensor mProxSensor; private SensorManager mSensorManager; private AsyncDialog mAsyncDialog; private ComposeView mComposeView; private ConversationPrefsHelper mConversationPrefs; private ConversationDetailsDialog mConversationDetailsDialog; private int mSavedScrollPosition = -1; // we save the ListView's scroll position in onPause(), // so we can remember it after re-entering the activity. // If the value >= 0, then we jump to that line. If the // value is maxint, then we jump to the end. private BackgroundQueryHandler mBackgroundQueryHandler; private long mThreadId; private long mRowId; private String mHighlight; private boolean mShowImmediate; protected static MessageListFragment getInstance(long threadId, long rowId, String highlight, boolean showImmediate) { Bundle args = new Bundle(); args.putLong(MessageListActivity.ARG_THREAD_ID, threadId); args.putLong(MessageListActivity.ARG_ROW_ID, rowId); args.putString(MessageListActivity.ARG_HIGHLIGHT, highlight); args.putBoolean(MessageListActivity.ARG_SHOW_IMMEDIATE, showImmediate); MessageListFragment fragment = new MessageListFragment(); fragment.setArguments(args); return fragment; } public MessageListFragment() { } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { Bundle args = getArguments(); mThreadId = args.getLong(MessageListActivity.ARG_THREAD_ID, -1); mRowId = args.getLong(MessageListActivity.ARG_ROW_ID, -1); mHighlight = args.getString(MessageListActivity.ARG_HIGHLIGHT, null); mShowImmediate = args.getBoolean(MessageListActivity.ARG_SHOW_IMMEDIATE, false); } else if (savedInstanceState != null) { mThreadId = savedInstanceState.getLong(MessageListActivity.ARG_THREAD_ID, -1); mRowId = savedInstanceState.getLong(MessageListActivity.ARG_ROW_ID, -1); mHighlight = savedInstanceState.getString(MessageListActivity.ARG_HIGHLIGHT, null); mShowImmediate = savedInstanceState.getBoolean(MessageListActivity.ARG_SHOW_IMMEDIATE, false); } mConversationPrefs = new ConversationPrefsHelper(mContext, mThreadId); mIsSmsEnabled = MmsConfig.isSmsEnabled(mContext); mConversationDetailsDialog = new ConversationDetailsDialog(mContext, getFragmentManager()); onOpenConversation(); setHasOptionsMenu(true); LiveViewManager.registerView(QKPreference.CONVERSATION_THEME, this, key -> { mCIELChEvaluator = new CIELChEvaluator(mConversationPrefs.getColor(), ThemeManager.getThemeColor()); }); mSensorManager = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); mProxSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); if (QKPreferences.getBoolean(QKPreference.PROXIMITY_SENSOR)) { mSensorManager.registerListener(this, mProxSensor, SensorManager.SENSOR_DELAY_NORMAL); } mBackgroundQueryHandler = new BackgroundQueryHandler(mContext.getContentResolver()); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_conversation, container, false); mRecyclerView = (MessageListRecyclerView) view.findViewById(R.id.conversation); mAdapter = new MessageListAdapter(mContext); mAdapter.setItemClickListener(this); mAdapter.setMultiSelectListener(this); mAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { private long mLastMessageId = -1; @Override public void onChanged() { LinearLayoutManager manager = (LinearLayoutManager) mRecyclerView.getLayoutManager(); int position; if (mRowId != -1 && mCursor != null) { // Scroll to the position in the conversation for that message. position = SmsHelper.getPositionForMessageId(mCursor, "sms", mRowId, mAdapter.getColumnsMap()); // Be sure to reset the row ID here---we only want to scroll to the message // the first time the cursor is loaded after the row ID is set. mRowId = -1; } else { position = mAdapter.getItemCount() - 1; } if(mAdapter.getCount() > 0) { MessageItem lastMessage = mAdapter.getItem(mAdapter.getCount() - 1); if (mLastMessageId >= 0 && mLastMessageId != lastMessage.getMessageId()) { // Scroll to bottom only if a new message was inserted in this conversation if (position != -1) { manager.smoothScrollToPosition(mRecyclerView, null, position); } } mLastMessageId = lastMessage.getMessageId(); } } }); mRecyclerView.setAdapter(mAdapter); mLayoutManager = new SmoothLinearLayoutManager(mContext); mLayoutManager.setStackFromEnd(true); mRecyclerView.setLayoutManager(mLayoutManager); mComposeView = (ComposeView) view.findViewById(R.id.compose_view); mComposeView.setActivityLauncher(this); mComposeView.setLabel("MessageList"); mRecyclerView.setComposeView(mComposeView); return view; } @Override public void onResume() { super.onResume(); if (QKPreferences.getBoolean(QKPreference.PROXIMITY_SENSOR)) { mSensorManager.registerListener(this, mProxSensor, SensorManager.SENSOR_DELAY_NORMAL); } ThemeManager.setActiveColor(mConversationPrefs.getColor()); } @Override public void onSaveInstanceState(Bundle outState) { outState.putLong(MessageListActivity.ARG_THREAD_ID, mThreadId); outState.putLong(MessageListActivity.ARG_ROW_ID, mRowId); outState.putString(MessageListActivity.ARG_HIGHLIGHT, mHighlight); outState.putBoolean(MessageListActivity.ARG_SHOW_IMMEDIATE, mShowImmediate); } public long getThreadId() { return mThreadId; } /** * To be called when the user opens a conversation. Initializes the Conversation objects, sets * up the draft, and marks the conversation as read. * <p> * Note: This will have no effect if the context has not been initialized yet. */ private void onOpenConversation() { new LoadConversationTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null); } private void setTitle() { if (mContext != null && mConversation != null) { mContext.setTitle(mConversation.getRecipients().formatNames(", ")); } } @Override public void onItemClick(final MessageItem messageItem, View view) { if (mAdapter.isInMultiSelectMode()) { mAdapter.toggleSelection(messageItem.getMessageId(), messageItem); } else { if (view.getId() == R.id.image_view || view.getId() == R.id.play_slideshow_button) { switch (messageItem.mAttachmentType) { case SmsHelper.IMAGE: case SmsHelper.AUDIO: case SmsHelper.SLIDESHOW: MessageUtils.viewMmsMessageAttachment(getActivity(), messageItem.mMessageUri, messageItem.mSlideshow, getAsyncDialog()); break; case SmsHelper.VIDEO: new QKDialog() .setContext(mContext) .setTitle(R.string.warning) .setMessage(R.string.stagefright_warning) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.yes, new View.OnClickListener() { @Override public void onClick(View view) { MessageUtils.viewMmsMessageAttachment(getActivity(), messageItem.mMessageUri, messageItem.mSlideshow, getAsyncDialog()); } }) .show(); break; } } else if (messageItem != null && messageItem.isOutgoingMessage() && messageItem.isFailedMessage()) { showMessageResendOptions(messageItem); } else if (messageItem != null && ContentType.TEXT_VCARD.equals(messageItem.mTextContentType)) { openVcard(messageItem); } else { showMessageDetails(messageItem); } } } @Override public void onItemLongClick(MessageItem messageItem, View view) { QKDialog dialog = new QKDialog(); dialog.setContext(mContext); dialog.setTitle(R.string.message_options); MsgListMenuClickListener l = new MsgListMenuClickListener(messageItem); // It is unclear what would make most sense for copying an MMS message // to the clipboard, so we currently do SMS only. if (messageItem.isSms()) { // Message type is sms. Only allow "edit" if the message has a single recipient if (getRecipients().size() == 1 && (messageItem.mBoxId == Telephony.Sms.MESSAGE_TYPE_OUTBOX || messageItem.mBoxId == Telephony.Sms.MESSAGE_TYPE_FAILED)) { dialog.addMenuItem(R.string.menu_edit, MENU_EDIT_MESSAGE); } dialog.addMenuItem(R.string.copy_message_text, MENU_COPY_MESSAGE_TEXT); } addCallAndContactMenuItems(dialog, messageItem); // Forward is not available for undownloaded messages. if (messageItem.isDownloaded() && (messageItem.isSms() || MessageUtils.isForwardable(mContext, messageItem.getMessageId())) && mIsSmsEnabled) { dialog.addMenuItem(R.string.menu_forward, MENU_FORWARD_MESSAGE); } if (messageItem.isMms()) { switch (messageItem.mBoxId) { case Telephony.Mms.MESSAGE_BOX_INBOX: break; case Telephony.Mms.MESSAGE_BOX_OUTBOX: // Since we currently break outgoing messages to multiple // recipients into one message per recipient, only allow // editing a message for single-recipient conversations. if (getRecipients().size() == 1) { dialog.addMenuItem(R.string.menu_edit, MENU_EDIT_MESSAGE); } break; } switch (messageItem.mAttachmentType) { case SmsHelper.TEXT: break; case SmsHelper.VIDEO: case SmsHelper.IMAGE: if (MessageUtils.haveSomethingToCopyToSDCard(mContext, messageItem.mMsgId)) { dialog.addMenuItem(R.string.copy_to_sdcard, MENU_COPY_TO_SDCARD); } break; case SmsHelper.SLIDESHOW: default: dialog.addMenuItem(R.string.view_slideshow, MENU_VIEW_SLIDESHOW); if (MessageUtils.haveSomethingToCopyToSDCard(mContext, messageItem.mMsgId)) { dialog.addMenuItem(R.string.copy_to_sdcard, MENU_COPY_TO_SDCARD); } if (MessageUtils.isDrmRingtoneWithRights(mContext, messageItem.mMsgId)) { dialog.addMenuItem(MessageUtils.getDrmMimeMenuStringRsrc(mContext, messageItem.mMsgId), MENU_SAVE_RINGTONE); } break; } } if (messageItem.mLocked && mIsSmsEnabled) { dialog.addMenuItem(R.string.menu_unlock, MENU_UNLOCK_MESSAGE); } else if (mIsSmsEnabled) { dialog.addMenuItem(R.string.menu_lock, MENU_LOCK_MESSAGE); } dialog.addMenuItem(R.string.view_message_details, MENU_VIEW_MESSAGE_DETAILS); if (messageItem.mDeliveryStatus != MessageItem.DeliveryStatus.NONE || messageItem.mReadReport) { dialog.addMenuItem(R.string.view_delivery_report, MENU_DELIVERY_REPORT); } if (mIsSmsEnabled) { dialog.addMenuItem(R.string.delete_message, MENU_DELETE_MESSAGE); } dialog.buildMenu(l); dialog.show(); } private void addCallAndContactMenuItems(QKDialog dialog, MessageItem msgItem) { if (TextUtils.isEmpty(msgItem.mBody)) { return; } SpannableString msg = new SpannableString(msgItem.mBody); Linkify.addLinks(msg, Linkify.ALL); ArrayList<String> uris = MessageUtils.extractUris(msg.getSpans(0, msg.length(), URLSpan.class)); // Remove any dupes so they don't get added to the menu multiple times HashSet<String> collapsedUris = new HashSet<>(); for (String uri : uris) { collapsedUris.add(uri.toLowerCase()); } for (String uriString : collapsedUris) { String prefix = null; int sep = uriString.indexOf(":"); if (sep >= 0) { prefix = uriString.substring(0, sep); uriString = uriString.substring(sep + 1); } Uri contactUri = null; boolean knownPrefix = true; if ("mailto".equalsIgnoreCase(prefix)) { contactUri = MessageUtils.getContactUriForEmail(mContext, uriString); } else if ("tel".equalsIgnoreCase(prefix)) { contactUri = MessageUtils.getContactUriForPhoneNumber(uriString); } else { knownPrefix = false; } if (knownPrefix && contactUri == null) { Intent intent = MainActivity.createAddContactIntent(uriString); String addContactString = getString(R.string.menu_add_address_to_contacts, uriString); dialog.addMenuItem(addContactString, MENU_ADD_ADDRESS_TO_CONTACTS); } } } private ContactList getRecipients() { return mConversation.getRecipients(); } AsyncDialog getAsyncDialog() { if (mAsyncDialog == null) { mAsyncDialog = new AsyncDialog(getActivity()); } return mAsyncDialog; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_call: makeCall(); return true; case R.id.menu_notifications: boolean notificationMuted = mConversationPrefs.getNotificationsEnabled(); mConversationPrefs.putBoolean(SettingsFragment.NOTIFICATIONS, !notificationMuted); mContext.invalidateOptionsMenu(); vibrateOnConversationStateChanged(notificationMuted); return true; case R.id.menu_details: mConversationDetailsDialog.showDetails(mConversation); return true; case R.id.menu_notification_settings: ConversationSettingsDialog.newInstance(mThreadId, mConversation.getRecipients().formatNames(", ")) .setContext(mContext) .show(); return true; case R.id.menu_delete_conversation: DialogHelper.showDeleteConversationDialog(mContext, mThreadId); return true; } return super.onOptionsItemSelected(item); } private void makeCall() { Intent openDialerIntent = new Intent(Intent.ACTION_CALL); openDialerIntent.setData(Uri.parse("tel:" + mConversationLegacy.getAddress())); startActivity(openDialerIntent); } private void vibrateOnConversationStateChanged(final boolean notificationMuted) { final int vibrateTime = 70; Toast.makeText(getActivity(), notificationMuted ? R.string.notification_mute_off : R.string.notification_mute_on, Toast.LENGTH_SHORT).show(); Vibrator v = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE); v.vibrate(vibrateTime); } /** * Photo Selection result */ public void onActivityResult(int requestCode, int resultCode, final Intent data) { if (!mComposeView.onActivityResult(requestCode, resultCode, data)) { // Wasn't handled by ComposeView } } /** * Should only be called for failed messages. Deletes the message, placing the text from the * message back in the edit box to be updated and then sent. * <p> * Assumes that cursor points to the correct MessageItem. * * @param msgItem */ private void editMessageItem(MessageItem msgItem) { String body = msgItem.mBody; // Delete the message and put the text back into the edit text. deleteMessageItem(msgItem); // Set the text and open the keyboard KeyboardUtils.show(mContext); mComposeView.setText(body); } /** * Should only be called for failed messages. Deletes the message and resends it. * * @param msgItem */ public void resendMessageItem(final MessageItem msgItem) { String body = msgItem.mBody; deleteMessageItem(msgItem); mComposeView.setText(body); mComposeView.sendSms(); } /** * Deletes the message from the conversation list and the conversation history. * * @param msgItem */ public void deleteMessageItem(final MessageItem msgItem) { new AsyncTask<Void, Void, Void>() { protected Void doInBackground(Void... none) { if (msgItem.isMms()) { MessageUtils.removeThumbnailsFromCache(msgItem.getSlideshow()); QKSMSApp.getApplication().getPduLoaderManager().removePdu(msgItem.mMessageUri); // Delete the message *after* we've removed the thumbnails because we // need the pdu and slideshow for removeThumbnailsFromCache to work. } // Determine if we're deleting the last item in the cursor. Boolean deletingLastItem = false; if (mAdapter != null && mAdapter.getCursor() != null) { mCursor = mAdapter.getCursor(); mCursor.moveToLast(); long msgId = mCursor.getLong(MessageColumns.COLUMN_ID); deletingLastItem = msgId == msgItem.mMsgId; } mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN, deletingLastItem, msgItem.mMessageUri, msgItem.mLocked ? null : "locked=0", null); return null; } }.execute(); } private void initLoaderManager() { getLoaderManager().initLoader(QKSMSApp.LOADER_MESSAGES, null, this); } @Override public void onPause() { super.onPause(); mComposeView.saveDraft(); if (mSensorManager != null) { mSensorManager.unregisterListener(this); } if (mConversationLegacy != null) { mConversationLegacy.markRead(); } if (mConversation != null) { mConversation.blockMarkAsRead(true); mConversation.markAsRead(); mComposeView.saveDraft(); } ThemeManager.setActiveColor(ThemeManager.getThemeColor()); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); menu.findItem(R.id.menu_notifications).setTitle(mConversationPrefs.getNotificationsEnabled() ? R.string.menu_notifications : R.string.menu_notifications_off); menu.findItem(R.id.menu_notifications).setIcon(mConversationPrefs.getNotificationsEnabled() ? R.drawable.ic_notifications : R.drawable.ic_notifications_muted); } @Override public void onSensorChanged(SensorEvent event) { if (event.values[0] == 0 && isAdded()) { makeCall(); } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // Ignored } @Override public Loader<Cursor> onCreateLoader(int id, Bundle args) { if (id == QKSMSApp.LOADER_MESSAGES) { return new CursorLoader(mContext, Uri.withAppendedPath(Message.MMS_SMS_CONTENT_PROVIDER, String.valueOf(mThreadId)), MessageColumns.PROJECTION, null, null, "normalized_date ASC"); } else { return null; } } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { if (mAdapter != null && loader.getId() == QKSMSApp.LOADER_MESSAGES) { // Swap the new cursor in. (The framework will take care of closing the, old cursor once we return.) mAdapter.changeCursor(data); } } @Override public void onLoaderReset(Loader<Cursor> loader) { if (mAdapter != null && loader.getId() == QKSMSApp.LOADER_MESSAGES) { mAdapter.changeCursor(null); } } @Override public void onMultiSelectStateChanged(boolean enabled) { } @Override public void onItemAdded(long id) { } @Override public void onItemRemoved(long id) { } @Override public void onScrollChanged(float scrollPercent) { if (mConversationPrefs != null) { ThemeManager.setActiveColor(mCIELChEvaluator.evaluate(scrollPercent)); } } private class DeleteMessageListener implements DialogInterface.OnClickListener { private final MessageItem mMessageItem; public DeleteMessageListener(MessageItem messageItem) { mMessageItem = messageItem; } @Override public void onClick(DialogInterface dialog, int whichButton) { dialog.dismiss(); deleteMessageItem(mMessageItem); } } /** * Context menu handlers for the message list view. */ private final class MsgListMenuClickListener implements AdapterView.OnItemClickListener { private MessageItem mMsgItem; public MsgListMenuClickListener(MessageItem msgItem) { mMsgItem = msgItem; } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if (mMsgItem == null) { return; } switch ((int) id) { case MENU_EDIT_MESSAGE: editMessageItem(mMsgItem); break; case MENU_COPY_MESSAGE_TEXT: MessageUtils.copyToClipboard(mContext, mMsgItem.mBody); break; case MENU_FORWARD_MESSAGE: MessageUtils.forwardMessage(mContext, mMsgItem); break; case MENU_VIEW_SLIDESHOW: MessageUtils.viewMmsMessageAttachment(getActivity(), ContentUris.withAppendedId(Telephony.Mms.CONTENT_URI, mMsgItem.mMsgId), null, getAsyncDialog()); break; case MENU_VIEW_MESSAGE_DETAILS: showMessageDetails(mMsgItem); break; case MENU_DELETE_MESSAGE: DeleteMessageListener l = new DeleteMessageListener(mMsgItem); confirmDeleteDialog(l, mMsgItem.mLocked); break; case MENU_DELIVERY_REPORT: showDeliveryReport(mMsgItem.mMsgId, mMsgItem.mType); break; case MENU_COPY_TO_SDCARD: { int resId = MessageUtils.copyMedia(mContext, mMsgItem.mMsgId) ? R.string.copy_to_sdcard_success : R.string.copy_to_sdcard_fail; Toast.makeText(mContext, resId, Toast.LENGTH_SHORT).show(); break; } case MENU_SAVE_RINGTONE: { int resId = MessageUtils.getDrmMimeSavedStringRsrc(mContext, mMsgItem.mMsgId, MessageUtils.saveRingtone(mContext, mMsgItem.mMsgId)); Toast.makeText(mContext, resId, Toast.LENGTH_SHORT).show(); break; } case MENU_ADD_ADDRESS_TO_CONTACTS: MessageUtils.addToContacts(mContext, mMsgItem); break; case MENU_LOCK_MESSAGE: MessageUtils.lockMessage(mContext, mMsgItem, true); break; case MENU_UNLOCK_MESSAGE: MessageUtils.lockMessage(mContext, mMsgItem, false); break; } } } private boolean showMessageResendOptions(final MessageItem msgItem) { final Cursor cursor = mAdapter.getCursorForItem(msgItem); if (cursor == null) { return false; } KeyboardUtils.hide(mContext, mComposeView); new QKDialog() .setContext(mContext) .setTitle(R.string.failed_message_title) .setItems(R.array.resend_menu, (parent, view, position, id) -> { switch (position) { case 0: // Resend message resendMessageItem(msgItem); break; case 1: // Edit message editMessageItem(msgItem); break; case 2: // Delete message confirmDeleteDialog(new DeleteMessageListener(msgItem), false); break; } }).show(); return true; } private void openVcard(MessageItem messageItem) { Log.d(TAG, "Vcard: " + messageItem.mBody); VCard vCard = Ezvcard.parse(messageItem.mBody).first(); ContactOperations operations = new ContactOperations(mContext); try { operations.insertContact(vCard); } catch (Exception e) { e.printStackTrace(); } } private boolean showMessageDetails(MessageItem msgItem) { Cursor cursor = mAdapter.getCursorForItem(msgItem); if (cursor == null) { return false; } String messageDetails = MessageUtils.getMessageDetails(mContext, cursor, msgItem.mMessageSize); new QKDialog() .setContext(mContext) .setTitle(R.string.message_details_title) .setMessage(messageDetails) .setCancelOnTouchOutside(true) .show(); return true; } private void confirmDeleteDialog(DialogInterface.OnClickListener listener, boolean locked) { AlertDialog.Builder builder = new AlertDialog.Builder(mContext); builder.setCancelable(true); builder.setMessage(locked ? R.string.confirm_delete_locked_message : R.string.confirm_delete_message); builder.setPositiveButton(R.string.delete, listener); builder.setNegativeButton(R.string.cancel, null); builder.show(); } private void showDeliveryReport(long messageId, String type) { DeliveryReportHelper deliveryReportHelper = new DeliveryReportHelper(mContext, messageId, type); List<DeliveryReportItem> deliveryReportItems = deliveryReportHelper.getListItems(); String[] items = new String[deliveryReportItems.size() * 3]; for (int i = 0; i < deliveryReportItems.size() * 3; i++) { switch (i % 3) { case 0: items[i] = deliveryReportItems.get(i - (i / 3)).recipient; break; case 1: items[i] = deliveryReportItems.get(i - 1 - ((i - 1) / 3)).status; break; case 2: items[i] = deliveryReportItems.get(i - 2 - ((i - 2) / 3)).deliveryDate; break; } } new QKDialog() .setContext(mContext) .setTitle(R.string.delivery_header_title) .setItems(items, null) .setPositiveButton(R.string.okay, null) .show(); } private void startMsgListQuery(int token) { /*if (mSendDiscreetMode) { return; }*/ Uri conversationUri = mConversation.getUri(); if (conversationUri == null) { Log.v(TAG, "##### startMsgListQuery: conversationUri is null, bail!"); return; } long threadId = mConversation.getThreadId(); if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { Log.v(TAG, "startMsgListQuery for " + conversationUri + ", threadId=" + threadId + " token: " + token + " mConversation: " + mConversation); } // Cancel any pending queries mBackgroundQueryHandler.cancelOperation(token); try { // Kick off the new query mBackgroundQueryHandler.startQuery( token, threadId /* cookie */, conversationUri, MessageColumns.PROJECTION, null, null, null); } catch (SQLiteException e) { SqliteWrapper.checkSQLiteException(mContext, e); } } private final class BackgroundQueryHandler extends Conversation.ConversationQueryHandler { public BackgroundQueryHandler(ContentResolver contentResolver) { super(contentResolver, mContext); } @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { switch (token) { case MainActivity.HAVE_LOCKED_MESSAGES_TOKEN: if (mContext.isFinishing()) { Log.w(TAG, "ComposeMessageActivity is finished, do nothing "); if (cursor != null) { cursor.close(); } return; } @SuppressWarnings("unchecked") ArrayList<Long> threadIds = (ArrayList<Long>) cookie; MainActivity.confirmDeleteThreadDialog( new MainActivity.DeleteThreadListener(threadIds, mBackgroundQueryHandler, mContext), threadIds, cursor != null && cursor.getCount() > 0, mContext); if (cursor != null) { cursor.close(); } break; case MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN: // check consistency between the query result and 'mConversation' long tid = (Long) cookie; if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { Log.v(TAG, "##### onQueryComplete (after delete): msg history result for threadId " + tid); } if (cursor == null) { return; } if (tid > 0 && cursor.getCount() == 0) { // We just deleted the last message and the thread will get deleted // by a trigger in the database. Clear the threadId so next time we // need the threadId a new thread will get created. Log.v(TAG, "##### MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN clearing thread id: " + tid); Conversation conv = Conversation.get(mContext, tid, false); if (conv != null) { conv.clearThreadId(); conv.setDraftState(false); } mContext.onBackPressed(); } cursor.close(); } } @Override protected void onDeleteComplete(int token, Object cookie, int result) { super.onDeleteComplete(token, cookie, result); switch (token) { case MainActivity.DELETE_CONVERSATION_TOKEN: mConversation.setMessageCount(0); // fall through case DELETE_MESSAGE_TOKEN: // Update the notification for new messages since they may be deleted. NotificationManager.update(mContext); // TODO Update the notification for failed messages since they may be deleted. //updateSendFailedNotification(); break; } // If we're deleting the whole conversation, throw away our current working message and bail. if (token == MainActivity.DELETE_CONVERSATION_TOKEN) { ContactList recipients = mConversation.getRecipients(); // Remove any recipients referenced by this single thread from the It's possible for two or more // threads to reference the same contact. That's ok if we remove it. We'll recreate that contact // when we init all Conversations below. if (recipients != null) { for (Contact contact : recipients) { contact.removeFromCache(); } } // Make sure the conversation cache reflects the threads in the DB. Conversation.init(mContext); // Go back to the conversation list mContext.onBackPressed(); } else if (token == DELETE_MESSAGE_TOKEN) { // Check to see if we just deleted the last message startMsgListQuery(MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN); } WidgetProvider.notifyDatasetChanged(mContext); } } private class LoadConversationTask extends AsyncTask<Void, Void, Void> { public LoadConversationTask() { Log.d(TAG, "LoadConversationTask"); } @Override protected Void doInBackground(Void... params) { Log.d(TAG, "Loading conversation"); mConversation = Conversation.get(mContext, mThreadId, true); mConversationLegacy = new ConversationLegacy(mContext, mThreadId); mConversationLegacy.markRead(); mConversation.blockMarkAsRead(true); mConversation.markAsRead(); return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); Log.d(TAG, "Conversation loaded"); mComposeView.onOpenConversation(mConversation, mConversationLegacy); setTitle(); mAdapter.setIsGroupConversation(mConversation.getRecipients().size() > 1); if (isAdded()) { initLoaderManager(); } } } }