/*
* Copyright (C) 2009 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.mms.data;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SqliteWrapper;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.provider.Telephony.Mms;
import android.provider.Telephony.MmsSms;
import android.provider.Telephony.MmsSms.PendingMessages;
import android.provider.Telephony.Sms;
import android.telephony.SmsMessage;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.android.common.contacts.DataUsageStatUpdater;
import com.android.common.userhappiness.UserHappinessSignals;
import com.android.mms.ContentRestrictionException;
import com.android.mms.ExceedMessageSizeException;
import com.android.mms.LogTag;
import com.android.mms.MmsApp;
import com.android.mms.MmsConfig;
import com.android.mms.ResolutionException;
import com.android.mms.UnsupportContentTypeException;
import com.android.mms.model.ImageModel;
import com.android.mms.model.SlideModel;
import com.android.mms.model.SlideshowModel;
import com.android.mms.model.TextModel;
import com.android.mms.transaction.MessageSender;
import com.android.mms.transaction.MmsMessageSender;
import com.android.mms.transaction.SmsMessageSender;
import com.android.mms.ui.ComposeMessageActivity;
import com.android.mms.ui.MessageUtils;
import com.android.mms.ui.MessagingPreferenceActivity;
import com.android.mms.ui.SlideshowEditor;
import com.android.mms.util.DraftCache;
import com.android.mms.util.Recycler;
import com.android.mms.util.ThumbnailManager;
import com.android.mms.widget.MmsWidgetProvider;
import com.google.android.mms.ContentType;
import com.google.android.mms.MmsException;
import com.google.android.mms.pdu.EncodedStringValue;
import com.google.android.mms.pdu.PduBody;
import com.google.android.mms.pdu.PduHeaders;
import com.google.android.mms.pdu.PduPersister;
import com.google.android.mms.pdu.SendReq;
/**
* Contains all state related to a message being edited by the user.
*/
public class WorkingMessage {
private static final String TAG = "WorkingMessage";
private static final boolean DEBUG = false;
// Public intents
public static final String ACTION_SENDING_SMS = "android.intent.action.SENDING_SMS";
// Intent extras
public static final String EXTRA_SMS_MESSAGE = "android.mms.extra.MESSAGE";
public static final String EXTRA_SMS_RECIPIENTS = "android.mms.extra.RECIPIENTS";
public static final String EXTRA_SMS_THREAD_ID = "android.mms.extra.THREAD_ID";
// Database access stuff
private final Activity mActivity;
private final ContentResolver mContentResolver;
// States that can require us to save or send a message as MMS.
private static final int RECIPIENTS_REQUIRE_MMS = (1 << 0); // 1
private static final int HAS_SUBJECT = (1 << 1); // 2
private static final int HAS_ATTACHMENT = (1 << 2); // 4
private static final int LENGTH_REQUIRES_MMS = (1 << 3); // 8
private static final int FORCE_MMS = (1 << 4); // 16
private static final int MULTIPLE_RECIPIENTS = (1 << 5); // 32
// A bitmap of the above indicating different properties of the message;
// any bit set will require the message to be sent via MMS.
private int mMmsState;
// Errors from setAttachment()
public static final int OK = 0;
public static final int UNKNOWN_ERROR = -1;
public static final int MESSAGE_SIZE_EXCEEDED = -2;
public static final int UNSUPPORTED_TYPE = -3;
public static final int IMAGE_TOO_LARGE = -4;
// Attachment types
public static final int TEXT = 0;
public static final int IMAGE = 1;
public static final int VIDEO = 2;
public static final int AUDIO = 3;
public static final int SLIDESHOW = 4;
// Current attachment type of the message; one of the above values.
private int mAttachmentType;
// Conversation this message is targeting.
private Conversation mConversation;
// Text of the message.
private CharSequence mText;
// Slideshow for this message, if applicable. If it's a simple attachment,
// i.e. not SLIDESHOW, it will contain only one slide.
private SlideshowModel mSlideshow;
// Data URI of an MMS message if we have had to save it.
private Uri mMessageUri;
// MMS subject line for this message
private CharSequence mSubject;
// Set to true if this message has been discarded.
private boolean mDiscarded = false;
// Track whether we have drafts
private volatile boolean mHasMmsDraft;
private volatile boolean mHasSmsDraft;
// Cached value of mms enabled flag
private static boolean sMmsEnabled = MmsConfig.getMmsEnabled();
// Our callback interface
private final MessageStatusListener mStatusListener;
private List<String> mWorkingRecipients;
// Message sizes in Outbox
private static final String[] MMS_OUTBOX_PROJECTION = {
Mms._ID, // 0
Mms.MESSAGE_SIZE // 1
};
private static final int MMS_MESSAGE_SIZE_INDEX = 1;
/**
* Callback interface for communicating important state changes back to
* ComposeMessageActivity.
*/
public interface MessageStatusListener {
/**
* Called when the protocol for sending the message changes from SMS
* to MMS, and vice versa.
*
* @param mms If true, it changed to MMS. If false, to SMS.
*/
void onProtocolChanged(boolean mms);
/**
* Called when an attachment on the message has changed.
*/
void onAttachmentChanged();
/**
* Called just before the process of sending a message.
*/
void onPreMessageSent();
/**
* Called once the process of sending a message, triggered by
* {@link send} has completed. This doesn't mean the send succeeded,
* just that it has been dispatched to the network.
*/
void onMessageSent();
/**
* Called if there are too many unsent messages in the queue and we're not allowing
* any more Mms's to be sent.
*/
void onMaxPendingMessagesReached();
/**
* Called if there's an attachment error while resizing the images just before sending.
*/
void onAttachmentError(int error);
}
private WorkingMessage(ComposeMessageActivity activity) {
mActivity = activity;
mContentResolver = mActivity.getContentResolver();
mStatusListener = activity;
mAttachmentType = TEXT;
mText = "";
}
/**
* Creates a new working message.
*/
public static WorkingMessage createEmpty(ComposeMessageActivity activity) {
// Make a new empty working message.
WorkingMessage msg = new WorkingMessage(activity);
return msg;
}
/**
* Create a new WorkingMessage from the specified data URI, which typically
* contains an MMS message.
*/
public static WorkingMessage load(ComposeMessageActivity activity, Uri uri) {
// If the message is not already in the draft box, move it there.
if (!uri.toString().startsWith(Mms.Draft.CONTENT_URI.toString())) {
PduPersister persister = PduPersister.getPduPersister(activity);
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("load: moving %s to drafts", uri);
}
try {
uri = persister.move(uri, Mms.Draft.CONTENT_URI);
} catch (MmsException e) {
LogTag.error("Can't move %s to drafts", uri);
return null;
}
}
WorkingMessage msg = new WorkingMessage(activity);
if (msg.loadFromUri(uri)) {
msg.mHasMmsDraft = true;
return msg;
}
return null;
}
private void correctAttachmentState() {
int slideCount = mSlideshow.size();
// If we get an empty slideshow, tear down all MMS
// state and discard the unnecessary message Uri.
if (slideCount == 0) {
removeAttachment(false);
} else if (slideCount > 1) {
mAttachmentType = SLIDESHOW;
} else {
SlideModel slide = mSlideshow.get(0);
if (slide.hasImage()) {
mAttachmentType = IMAGE;
} else if (slide.hasVideo()) {
mAttachmentType = VIDEO;
} else if (slide.hasAudio()) {
mAttachmentType = AUDIO;
}
}
updateState(HAS_ATTACHMENT, hasAttachment(), false);
}
private boolean loadFromUri(Uri uri) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("loadFromUri %s", uri);
try {
mSlideshow = SlideshowModel.createFromMessageUri(mActivity, uri);
} catch (MmsException e) {
LogTag.error("Couldn't load URI %s", uri);
return false;
}
mMessageUri = uri;
// Make sure all our state is as expected.
syncTextFromSlideshow();
correctAttachmentState();
return true;
}
/**
* Load the draft message for the specified conversation, or a new empty message if
* none exists.
*/
public static WorkingMessage loadDraft(ComposeMessageActivity activity,
final Conversation conv,
final Runnable onDraftLoaded) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("loadDraft %s", conv);
final WorkingMessage msg = createEmpty(activity);
if (conv.getThreadId() <= 0) {
if (onDraftLoaded != null) {
onDraftLoaded.run();
}
return msg;
}
new AsyncTask<Void, Void, Pair<String, String>>() {
// Return a Pair where:
// first - non-empty String representing the text of an SMS draft
// second - non-null String representing the text of an MMS subject
@Override
protected Pair<String, String> doInBackground(Void... none) {
// Look for an SMS draft first.
String draftText = msg.readDraftSmsMessage(conv);
String subject = null;
if (TextUtils.isEmpty(draftText)) {
// No SMS draft so look for an MMS draft.
StringBuilder sb = new StringBuilder();
Uri uri = readDraftMmsMessage(msg.mActivity, conv, sb);
if (uri != null) {
if (msg.loadFromUri(uri)) {
// If there was an MMS message, readDraftMmsMessage
// will put the subject in our supplied StringBuilder.
subject = sb.toString();
}
}
}
Pair<String, String> result = new Pair<String, String>(draftText, subject);
return result;
}
@Override
protected void onPostExecute(Pair<String, String> result) {
if (!TextUtils.isEmpty(result.first)) {
msg.mHasSmsDraft = true;
msg.setText(result.first);
}
if (result.second != null) {
msg.mHasMmsDraft = true;
if (!TextUtils.isEmpty(result.second)) {
msg.setSubject(result.second, false);
}
}
if (onDraftLoaded != null) {
onDraftLoaded.run();
}
}
}.execute();
return msg;
}
/**
* Sets the text of the message to the specified CharSequence.
*/
public void setText(CharSequence s) {
mText = s;
}
/**
* Returns the current message text.
*/
public CharSequence getText() {
return mText;
}
/**
* @return True if the message has any text. A message with just whitespace is not considered
* to have text.
*/
public boolean hasText() {
return mText != null && TextUtils.getTrimmedLength(mText) > 0;
}
public void removeAttachment(boolean notify) {
removeThumbnailsFromCache(mSlideshow);
mAttachmentType = TEXT;
mSlideshow = null;
if (mMessageUri != null) {
asyncDelete(mMessageUri, null, null);
mMessageUri = null;
}
// mark this message as no longer having an attachment
updateState(HAS_ATTACHMENT, false, notify);
if (notify) {
// Tell ComposeMessageActivity (or other listener) that the attachment has changed.
// In the case of ComposeMessageActivity, it will remove its attachment panel because
// this working message no longer has an attachment.
mStatusListener.onAttachmentChanged();
}
}
public static void removeThumbnailsFromCache(SlideshowModel slideshow) {
if (slideshow != null) {
ThumbnailManager thumbnailManager = MmsApp.getApplication().getThumbnailManager();
boolean removedSomething = false;
Iterator<SlideModel> iterator = slideshow.iterator();
while (iterator.hasNext()) {
SlideModel slideModel = iterator.next();
if (slideModel.hasImage()) {
thumbnailManager.removeThumbnail(slideModel.getImage().getUri());
removedSomething = true;
} else if (slideModel.hasVideo()) {
thumbnailManager.removeThumbnail(slideModel.getVideo().getUri());
removedSomething = true;
}
}
if (removedSomething) {
// HACK: the keys to the thumbnail cache are the part uris, such as mms/part/3
// Because the part table doesn't have auto-increment ids, the part ids are reused
// when a message or thread is deleted. For now, we're clearing the whole thumbnail
// cache so we don't retrieve stale images when part ids are reused. This will be
// fixed in the next release in the mms provider.
MmsApp.getApplication().getThumbnailManager().clearBackingStore();
}
}
}
/**
* Adds an attachment to the message, replacing an old one if it existed.
* @param type Type of this attachment, such as {@link IMAGE}
* @param dataUri Uri containing the attachment data (or null for {@link TEXT})
* @param append true if we should add the attachment to a new slide
* @return An error code such as {@link UNKNOWN_ERROR} or {@link OK} if successful
*/
public int setAttachment(int type, Uri dataUri, boolean append) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("setAttachment type=%d uri %s", type, dataUri);
}
int result = OK;
SlideshowEditor slideShowEditor = new SlideshowEditor(mActivity, mSlideshow);
// Special case for deleting a slideshow. When ComposeMessageActivity gets told to
// remove an attachment (search for AttachmentEditor.MSG_REMOVE_ATTACHMENT), it calls
// this function setAttachment with a type of TEXT and a null uri. Basically, it's turning
// the working message from an MMS back to a simple SMS. The various attachment types
// use slide[0] as a special case. The call to ensureSlideshow below makes sure there's
// a slide zero. In the case of an already attached slideshow, ensureSlideshow will do
// nothing and the slideshow will remain such that if a user adds a slideshow again, they'll
// see their old slideshow they previously deleted. Here we really delete the slideshow.
if (type == TEXT && mAttachmentType == SLIDESHOW && mSlideshow != null && dataUri == null
&& !append) {
slideShowEditor.removeAllSlides();
}
// Make sure mSlideshow is set up and has a slide.
ensureSlideshow(); // mSlideshow can be null before this call, won't be afterwards
slideShowEditor.setSlideshow(mSlideshow);
// Change the attachment
result = append ? appendMedia(type, dataUri, slideShowEditor)
: changeMedia(type, dataUri, slideShowEditor);
// If we were successful, update mAttachmentType and notify
// the listener than there was a change.
if (result == OK) {
mAttachmentType = type;
}
correctAttachmentState(); // this can remove the slideshow if there are no attachments
if (mSlideshow != null && type == IMAGE) {
// Prime the image's cache; helps A LOT when the image is coming from the network
// (e.g. Picasa album). See b/5445690.
int numSlides = mSlideshow.size();
if (numSlides > 0) {
ImageModel imgModel = mSlideshow.get(numSlides - 1).getImage();
if (imgModel != null) {
cancelThumbnailLoading();
imgModel.loadThumbnailBitmap(null);
}
}
}
mStatusListener.onAttachmentChanged(); // have to call whether succeeded or failed,
// because a replace that fails, removes the slide
if (!append && mAttachmentType == TEXT && type == TEXT) {
int[] params = SmsMessage.calculateLength(getText(), false);
/* SmsMessage.calculateLength returns an int[4] with:
* int[0] being the number of SMS's required,
* int[1] the number of code units used,
* int[2] is the number of code units remaining until the next message.
* int[3] is the encoding type that should be used for the message.
*/
int smsSegmentCount = params[0];
if (!MmsConfig.getMultipartSmsEnabled()) {
// The provider doesn't support multi-part sms's so as soon as the user types
// an sms longer than one segment, we have to turn the message into an mms.
setLengthRequiresMms(smsSegmentCount > 1, false);
} else {
int threshold = MmsConfig.getSmsToMmsTextThreshold();
setLengthRequiresMms(threshold > 0 && smsSegmentCount > threshold, false);
}
} else {
// Set HAS_ATTACHMENT if we need it.
updateState(HAS_ATTACHMENT, hasAttachment(), true);
}
return result;
}
/**
* Returns true if this message contains anything worth saving.
*/
public boolean isWorthSaving() {
// If it actually contains anything, it's of course not empty.
if (hasText() || hasSubject() || hasAttachment() || hasSlideshow()) {
return true;
}
// When saveAsMms() has been called, we set FORCE_MMS to represent
// sort of an "invisible attachment" so that the message isn't thrown
// away when we are shipping it off to other activities.
if (isFakeMmsForDraft()) {
return true;
}
return false;
}
private void cancelThumbnailLoading() {
int numSlides = mSlideshow != null ? mSlideshow.size() : 0;
if (numSlides > 0) {
ImageModel imgModel = mSlideshow.get(numSlides - 1).getImage();
if (imgModel != null) {
imgModel.cancelThumbnailLoading();
}
}
}
/**
* Returns true if FORCE_MMS is set.
* When saveAsMms() has been called, we set FORCE_MMS to represent
* sort of an "invisible attachment" so that the message isn't thrown
* away when we are shipping it off to other activities.
*/
public boolean isFakeMmsForDraft() {
return (mMmsState & FORCE_MMS) > 0;
}
/**
* Makes sure mSlideshow is set up.
*/
private void ensureSlideshow() {
if (mSlideshow != null) {
return;
}
SlideshowModel slideshow = SlideshowModel.createNew(mActivity);
SlideModel slide = new SlideModel(slideshow);
slideshow.add(slide);
mSlideshow = slideshow;
}
/**
* Change the message's attachment to the data in the specified Uri.
* Used only for single-slide ("attachment mode") messages. If the attachment fails to
* attach, restore the slide to its original state.
*/
private int changeMedia(int type, Uri uri, SlideshowEditor slideShowEditor) {
SlideModel originalSlide = mSlideshow.get(0);
if (originalSlide != null) {
slideShowEditor.removeSlide(0); // remove the original slide
}
slideShowEditor.addNewSlide(0);
SlideModel slide = mSlideshow.get(0); // get the new empty slide
int result = OK;
if (slide == null) {
Log.w(LogTag.TAG, "[WorkingMessage] changeMedia: no slides!");
return result;
}
// Clear the attachment type since we removed all the attachments. If this isn't cleared
// and the slide.add fails (for instance, a selected video could be too big), we'll be
// left in a state where we think we have an attachment, but it's been removed from the
// slide.
mAttachmentType = TEXT;
// If we're changing to text, just bail out.
if (type == TEXT) {
return result;
}
result = internalChangeMedia(type, uri, 0, slideShowEditor);
if (result != OK) {
slideShowEditor.removeSlide(0); // remove the failed slide
if (originalSlide != null) {
slideShowEditor.addSlide(0, originalSlide); // restore the original slide.
}
}
return result;
}
/**
* Add the message's attachment to the data in the specified Uri to a new slide.
*/
private int appendMedia(int type, Uri uri, SlideshowEditor slideShowEditor) {
int result = OK;
// If we're changing to text, just bail out.
if (type == TEXT) {
return result;
}
// The first time this method is called, mSlideshow.size() is going to be
// one (a newly initialized slideshow has one empty slide). The first time we
// attach the picture/video to that first empty slide. From then on when this
// function is called, we've got to create a new slide and add the picture/video
// to that new slide.
boolean addNewSlide = true;
if (mSlideshow.size() == 1 && !mSlideshow.isSimple()) {
addNewSlide = false;
}
if (addNewSlide) {
if (!slideShowEditor.addNewSlide()) {
return result;
}
}
int slideNum = mSlideshow.size() - 1;
result = internalChangeMedia(type, uri, slideNum, slideShowEditor);
if (result != OK) {
// We added a new slide and what we attempted to insert on the slide failed.
// Delete that slide, otherwise we could end up with a bunch of blank slides.
// It's ok that we're removing the slide even if we didn't add it (because it was
// the first default slide). If adding the first slide fails, we want to remove it.
slideShowEditor.removeSlide(slideNum);
}
return result;
}
private int internalChangeMedia(int type, Uri uri, int slideNum,
SlideshowEditor slideShowEditor) {
int result = OK;
try {
if (type == IMAGE) {
slideShowEditor.changeImage(slideNum, uri);
} else if (type == VIDEO) {
slideShowEditor.changeVideo(slideNum, uri);
} else if (type == AUDIO) {
slideShowEditor.changeAudio(slideNum, uri);
} else {
result = UNSUPPORTED_TYPE;
}
} catch (MmsException e) {
Log.e(TAG, "internalChangeMedia:", e);
result = UNKNOWN_ERROR;
} catch (UnsupportContentTypeException e) {
Log.e(TAG, "internalChangeMedia:", e);
result = UNSUPPORTED_TYPE;
} catch (ExceedMessageSizeException e) {
Log.e(TAG, "internalChangeMedia:", e);
result = MESSAGE_SIZE_EXCEEDED;
} catch (ResolutionException e) {
Log.e(TAG, "internalChangeMedia:", e);
result = IMAGE_TOO_LARGE;
}
return result;
}
/**
* Returns true if the message has an attachment (including slideshows).
*/
public boolean hasAttachment() {
return (mAttachmentType > TEXT);
}
/**
* Returns the slideshow associated with this message.
*/
public SlideshowModel getSlideshow() {
return mSlideshow;
}
/**
* Returns true if the message has a real slideshow, as opposed to just
* one image attachment, for example.
*/
public boolean hasSlideshow() {
return (mAttachmentType == SLIDESHOW);
}
/**
* Sets the MMS subject of the message. Passing null indicates that there
* is no subject. Passing "" will result in an empty subject being added
* to the message, possibly triggering a conversion to MMS. This extra
* bit of state is needed to support ComposeMessageActivity converting to
* MMS when the user adds a subject. An empty subject will be removed
* before saving to disk or sending, however.
*/
public void setSubject(CharSequence s, boolean notify) {
mSubject = s;
updateState(HAS_SUBJECT, (s != null), notify);
}
/**
* Returns the MMS subject of the message.
*/
public CharSequence getSubject() {
return mSubject;
}
/**
* Returns true if this message has an MMS subject. A subject has to be more than just
* whitespace.
* @return
*/
public boolean hasSubject() {
return mSubject != null && TextUtils.getTrimmedLength(mSubject) > 0;
}
/**
* Moves the message text into the slideshow. Should be called any time
* the message is about to be sent or written to disk.
*/
private void syncTextToSlideshow() {
if (mSlideshow == null || mSlideshow.size() != 1)
return;
SlideModel slide = mSlideshow.get(0);
TextModel text;
if (!slide.hasText()) {
// Add a TextModel to slide 0 if one doesn't already exist
text = new TextModel(mActivity, ContentType.TEXT_PLAIN, "text_0.txt",
mSlideshow.getLayout().getTextRegion());
slide.add(text);
} else {
// Otherwise just reuse the existing one.
text = slide.getText();
}
text.setText(mText);
}
/**
* Sets the message text out of the slideshow. Should be called any time
* a slideshow is loaded from disk.
*/
private void syncTextFromSlideshow() {
// Don't sync text for real slideshows.
if (mSlideshow.size() != 1) {
return;
}
SlideModel slide = mSlideshow.get(0);
if (slide == null || !slide.hasText()) {
return;
}
mText = slide.getText().getText();
}
/**
* Removes the subject if it is empty, possibly converting back to SMS.
*/
private void removeSubjectIfEmpty(boolean notify) {
if (!hasSubject()) {
setSubject(null, notify);
}
}
/**
* Gets internal message state ready for storage. Should be called any
* time the message is about to be sent or written to disk.
*/
private void prepareForSave(boolean notify) {
// Make sure our working set of recipients is resolved
// to first-class Contact objects before we save.
syncWorkingRecipients();
if (hasMmsContentToSave()) {
ensureSlideshow();
syncTextToSlideshow();
}
}
/**
* Resolve the temporary working set of recipients to a ContactList.
*/
public void syncWorkingRecipients() {
if (mWorkingRecipients != null) {
ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
mConversation.setRecipients(recipients); // resets the threadId to zero
setHasMultipleRecipients(recipients.size() > 1, true);
mWorkingRecipients = null;
}
}
public String getWorkingRecipients() {
// this function is used for DEBUG only
if (mWorkingRecipients == null) {
return null;
}
ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
return recipients.serialize();
}
// Call when we've returned from adding an attachment. We're no longer forcing the message
// into a Mms message. At this point we either have the goods to make the message a Mms
// or we don't. No longer fake it.
public void removeFakeMmsForDraft() {
updateState(FORCE_MMS, false, false);
}
/**
* Force the message to be saved as MMS and return the Uri of the message.
* Typically used when handing a message off to another activity.
*/
public Uri saveAsMms(boolean notify) {
if (DEBUG) LogTag.debug("saveAsMms mConversation=%s", mConversation);
// If we have discarded the message, just bail out.
if (mDiscarded) {
LogTag.warn("saveAsMms mDiscarded: true mConversation: " + mConversation +
" returning NULL uri and bailing");
return null;
}
// FORCE_MMS behaves as sort of an "invisible attachment", making
// the message seem non-empty (and thus not discarded). This bit
// is sticky until the last other MMS bit is removed, at which
// point the message will fall back to SMS.
updateState(FORCE_MMS, true, notify);
// Collect our state to be written to disk.
prepareForSave(true /* notify */);
try {
// Make sure we are saving to the correct thread ID.
DraftCache.getInstance().setSavingDraft(true);
if (!mConversation.getRecipients().isEmpty()) {
mConversation.ensureThreadId();
}
mConversation.setDraftState(true);
PduPersister persister = PduPersister.getPduPersister(mActivity);
SendReq sendReq = makeSendReq(mConversation, mSubject);
// If we don't already have a Uri lying around, make a new one. If we do
// have one already, make sure it is synced to disk.
if (mMessageUri == null) {
mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow, null,
mActivity, null);
} else {
updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq, null);
}
mHasMmsDraft = true;
} finally {
DraftCache.getInstance().setSavingDraft(false);
}
return mMessageUri;
}
/**
* Save this message as a draft in the conversation previously specified
* to {@link setConversation}.
*/
public void saveDraft(final boolean isStopping) {
// If we have discarded the message, just bail out.
if (mDiscarded) {
LogTag.warn("saveDraft mDiscarded: true mConversation: " + mConversation +
" skipping saving draft and bailing");
return;
}
// Make sure setConversation was called.
if (mConversation == null) {
throw new IllegalStateException("saveDraft() called with no conversation");
}
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("saveDraft for mConversation " + mConversation);
}
// Get ready to write to disk. But don't notify message status when saving draft
prepareForSave(false /* notify */);
if (requiresMms()) {
if (hasMmsContentToSave()) {
asyncUpdateDraftMmsMessage(mConversation, isStopping);
mHasMmsDraft = true;
}
} else {
String content = mText.toString();
// bug 2169583: don't bother creating a thread id only to delete the thread
// because the content is empty. When we delete the thread in updateDraftSmsMessage,
// we didn't nullify conv.mThreadId, causing a temperary situation where conv
// is holding onto a thread id that isn't in the database. If a new message arrives
// and takes that thread id (because it's the next thread id to be assigned), the
// new message will be merged with the draft message thread, causing confusion!
if (!TextUtils.isEmpty(content)) {
asyncUpdateDraftSmsMessage(mConversation, content, isStopping);
mHasSmsDraft = true;
} else {
// When there's no associated text message, we have to handle the case where there
// might have been a previous mms draft for this message. This can happen when a
// user turns an mms back into a sms, such as creating an mms draft with a picture,
// then removing the picture.
asyncDeleteDraftMmsMessage(mConversation);
mMessageUri = null;
}
}
}
synchronized public void discard() {
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("[WorkingMessage] discard");
}
if (mDiscarded == true) {
return;
}
// Mark this message as discarded in order to make saveDraft() no-op.
mDiscarded = true;
cancelThumbnailLoading();
// Delete any associated drafts if there are any.
if (mHasMmsDraft) {
asyncDeleteDraftMmsMessage(mConversation);
}
if (mHasSmsDraft) {
asyncDeleteDraftSmsMessage(mConversation);
}
clearConversation(mConversation, true);
}
public void unDiscard() {
if (DEBUG) LogTag.debug("unDiscard");
mDiscarded = false;
}
/**
* Returns true if discard() has been called on this message.
*/
public boolean isDiscarded() {
return mDiscarded;
}
/**
* To be called from our Activity's onSaveInstanceState() to give us a chance
* to stow our state away for later retrieval.
*
* @param bundle The Bundle passed in to onSaveInstanceState
*/
public void writeStateToBundle(Bundle bundle) {
if (hasSubject()) {
bundle.putString("subject", mSubject.toString());
}
if (mMessageUri != null) {
bundle.putParcelable("msg_uri", mMessageUri);
} else if (hasText()) {
bundle.putString("sms_body", mText.toString());
}
}
/**
* To be called from our Activity's onCreate() if the activity manager
* has given it a Bundle to reinflate
* @param bundle The Bundle passed in to onCreate
*/
public void readStateFromBundle(Bundle bundle) {
if (bundle == null) {
return;
}
String subject = bundle.getString("subject");
setSubject(subject, false);
Uri uri = (Uri)bundle.getParcelable("msg_uri");
if (uri != null) {
loadFromUri(uri);
return;
} else {
String body = bundle.getString("sms_body");
mText = body;
}
}
/**
* Update the temporary list of recipients, used when setting up a
* new conversation. Will be converted to a ContactList on any
* save event (send, save draft, etc.)
*/
public void setWorkingRecipients(List<String> numbers) {
mWorkingRecipients = numbers;
String s = null;
if (numbers != null) {
int size = numbers.size();
switch (size) {
case 1:
s = numbers.get(0);
break;
case 0:
s = "empty";
break;
default:
s = "{...} len=" + size;
}
}
}
private void dumpWorkingRecipients() {
Log.i(TAG, "-- mWorkingRecipients:");
if (mWorkingRecipients != null) {
int count = mWorkingRecipients.size();
for (int i=0; i<count; i++) {
Log.i(TAG, " [" + i + "] " + mWorkingRecipients.get(i));
}
Log.i(TAG, "");
}
}
public void dump() {
Log.i(TAG, "WorkingMessage:");
dumpWorkingRecipients();
if (mConversation != null) {
Log.i(TAG, "mConversation: " + mConversation.toString());
}
}
/**
* Set the conversation associated with this message.
*/
public void setConversation(Conversation conv) {
if (DEBUG) LogTag.debug("setConversation %s -> %s", mConversation, conv);
mConversation = conv;
// Convert to MMS if there are any email addresses in the recipient list.
ContactList contactList = conv.getRecipients();
setHasEmail(contactList.containsEmail(), false);
setHasMultipleRecipients(contactList.size() > 1, false);
}
public Conversation getConversation() {
return mConversation;
}
/**
* Hint whether or not this message will be delivered to an
* an email address.
*/
public void setHasEmail(boolean hasEmail, boolean notify) {
if (MmsConfig.getEmailGateway() != null) {
updateState(RECIPIENTS_REQUIRE_MMS, false, notify);
} else {
updateState(RECIPIENTS_REQUIRE_MMS, hasEmail, notify);
}
}
/**
* Set whether this message will be sent to multiple recipients. This is a hint whether the
* message needs to be sent as an mms or not. If MmsConfig.getGroupMmsEnabled is false, then
* the fact that the message is sent to multiple recipients is not a factor in determining
* whether the message is sent as an mms, but the other factors (such as, "has a picture
* attachment") still hold true.
*/
public void setHasMultipleRecipients(boolean hasMultipleRecipients, boolean notify) {
updateState(MULTIPLE_RECIPIENTS,
hasMultipleRecipients &&
MessagingPreferenceActivity.getIsGroupMmsEnabled(mActivity),
notify);
}
/**
* Returns true if this message would require MMS to send.
*/
public boolean requiresMms() {
return (mMmsState > 0);
}
/**
* Returns true if this message has been turned into an mms because it has a subject or
* an attachment, but not just because it has multiple recipients.
*/
private boolean hasMmsContentToSave() {
if (mMmsState == 0) {
return false;
}
if (mMmsState == MULTIPLE_RECIPIENTS && !hasText()) {
// If this message is only mms because of multiple recipients and there's no text
// to save, don't bother saving.
return false;
}
return true;
}
/**
* Set whether or not we want to send this message via MMS in order to
* avoid sending an excessive number of concatenated SMS messages.
* @param: mmsRequired is the value for the LENGTH_REQUIRES_MMS bit.
* @param: notify Whether or not to notify the user.
*/
public void setLengthRequiresMms(boolean mmsRequired, boolean notify) {
updateState(LENGTH_REQUIRES_MMS, mmsRequired, notify);
}
private static String stateString(int state) {
if (state == 0)
return "<none>";
StringBuilder sb = new StringBuilder();
if ((state & RECIPIENTS_REQUIRE_MMS) > 0)
sb.append("RECIPIENTS_REQUIRE_MMS | ");
if ((state & HAS_SUBJECT) > 0)
sb.append("HAS_SUBJECT | ");
if ((state & HAS_ATTACHMENT) > 0)
sb.append("HAS_ATTACHMENT | ");
if ((state & LENGTH_REQUIRES_MMS) > 0)
sb.append("LENGTH_REQUIRES_MMS | ");
if ((state & FORCE_MMS) > 0)
sb.append("FORCE_MMS | ");
if ((state & MULTIPLE_RECIPIENTS) > 0)
sb.append("MULTIPLE_RECIPIENTS | ");
sb.delete(sb.length() - 3, sb.length());
return sb.toString();
}
/**
* Sets the current state of our various "MMS required" bits.
*
* @param state The bit to change, such as {@link HAS_ATTACHMENT}
* @param on If true, set it; if false, clear it
* @param notify Whether or not to notify the user
*/
private void updateState(int state, boolean on, boolean notify) {
if (!sMmsEnabled) {
// If Mms isn't enabled, the rest of the Messaging UI should not be using any
// feature that would cause us to to turn on any Mms flag and show the
// "Converting to multimedia..." message.
return;
}
int oldState = mMmsState;
if (on) {
mMmsState |= state;
} else {
mMmsState &= ~state;
}
// If we are clearing the last bit that is not FORCE_MMS,
// expire the FORCE_MMS bit.
if (mMmsState == FORCE_MMS && ((oldState & ~FORCE_MMS) > 0)) {
mMmsState = 0;
}
// Notify the listener if we are moving from SMS to MMS
// or vice versa.
if (notify) {
if (oldState == 0 && mMmsState != 0) {
mStatusListener.onProtocolChanged(true);
} else if (oldState != 0 && mMmsState == 0) {
mStatusListener.onProtocolChanged(false);
}
}
if (oldState != mMmsState) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("updateState: %s%s = %s",
on ? "+" : "-",
stateString(state), stateString(mMmsState));
}
}
/**
* Send this message over the network. Will call back with onMessageSent() once
* it has been dispatched to the telephony stack. This WorkingMessage object is
* no longer useful after this method has been called.
*
* @throws ContentRestrictionException if sending an MMS and uaProfUrl is not defined
* in mms_config.xml.
*/
public void send(final String recipientsInUI) {
long origThreadId = mConversation.getThreadId();
if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
LogTag.debug("send origThreadId: " + origThreadId);
}
removeSubjectIfEmpty(true /* notify */);
// Get ready to write to disk.
prepareForSave(true /* notify */);
// We need the recipient list for both SMS and MMS.
final Conversation conv = mConversation;
String msgTxt = mText.toString();
if (requiresMms() || addressContainsEmailToMms(conv, msgTxt)) {
// uaProfUrl setting in mms_config.xml must be present to send an MMS.
// However, SMS service will still work in the absence of a uaProfUrl address.
if (MmsConfig.getUaProfUrl() == null) {
String err = "WorkingMessage.send MMS sending failure. mms_config.xml is " +
"missing uaProfUrl setting. uaProfUrl is required for MMS service, " +
"but can be absent for SMS.";
RuntimeException ex = new NullPointerException(err);
Log.e(TAG, err, ex);
// now, let's just crash.
throw ex;
}
// Make local copies of the bits we need for sending a message,
// because we will be doing it off of the main thread, which will
// immediately continue on to resetting some of this state.
final Uri mmsUri = mMessageUri;
final PduPersister persister = PduPersister.getPduPersister(mActivity);
final SlideshowModel slideshow = mSlideshow;
final CharSequence subject = mSubject;
final boolean textOnly = mAttachmentType == TEXT;
if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
LogTag.debug("Send mmsUri: " + mmsUri);
}
// Do the dirty work of sending the message off of the main UI thread.
new Thread(new Runnable() {
@Override
public void run() {
final SendReq sendReq = makeSendReq(conv, subject);
// Make sure the text in slide 0 is no longer holding onto a reference to
// the text in the message text box.
slideshow.prepareForSend();
sendMmsWorker(conv, mmsUri, persister, slideshow, sendReq, textOnly);
updateSendStats(conv);
}
}, "WorkingMessage.send MMS").start();
} else {
// Same rules apply as above.
final String msgText = mText.toString();
new Thread(new Runnable() {
@Override
public void run() {
preSendSmsWorker(conv, msgText, recipientsInUI);
updateSendStats(conv);
}
}, "WorkingMessage.send SMS").start();
}
// update the Recipient cache with the new to address, if it's different
RecipientIdCache.updateNumbers(conv.getThreadId(), conv.getRecipients());
// Mark the message as discarded because it is "off the market" after being sent.
mDiscarded = true;
}
// Be sure to only call this on a background thread.
private void updateSendStats(final Conversation conv) {
String[] dests = conv.getRecipients().getNumbers();
final ArrayList<String> phoneNumbers = new ArrayList<String>(Arrays.asList(dests));
DataUsageStatUpdater updater = new DataUsageStatUpdater(mActivity);
updater.updateWithPhoneNumber(phoneNumbers);
}
private boolean addressContainsEmailToMms(Conversation conv, String text) {
if (MmsConfig.getEmailGateway() != null) {
String[] dests = conv.getRecipients().getNumbers();
int length = dests.length;
for (int i = 0; i < length; i++) {
if (Mms.isEmailAddress(dests[i]) || MessageUtils.isAlias(dests[i])) {
String mtext = dests[i] + " " + text;
int[] params = SmsMessage.calculateLength(mtext, false);
if (params[0] > 1) {
updateState(RECIPIENTS_REQUIRE_MMS, true, true);
ensureSlideshow();
syncTextToSlideshow();
return true;
}
}
}
}
return false;
}
// Message sending stuff
private void preSendSmsWorker(Conversation conv, String msgText, String recipientsInUI) {
// If user tries to send the message, it's a signal the inputted text is what they wanted.
UserHappinessSignals.userAcceptedImeText(mActivity);
mStatusListener.onPreMessageSent();
long origThreadId = conv.getThreadId();
// Make sure we are still using the correct thread ID for our recipient set.
long threadId = conv.ensureThreadId();
String semiSepRecipients = conv.getRecipients().serialize();
// recipientsInUI can be empty when the user types in a number and hits send
if (LogTag.SEVERE_WARNING && ((origThreadId != 0 && origThreadId != threadId) ||
(!semiSepRecipients.equals(recipientsInUI) && !TextUtils.isEmpty(recipientsInUI)))) {
String msg = origThreadId != 0 && origThreadId != threadId ?
"WorkingMessage.preSendSmsWorker threadId changed or " +
"recipients changed. origThreadId: " +
origThreadId + " new threadId: " + threadId +
" also mConversation.getThreadId(): " +
mConversation.getThreadId()
:
"Recipients in window: \"" +
recipientsInUI + "\" differ from recipients from conv: \"" +
semiSepRecipients + "\"";
LogTag.warnPossibleRecipientMismatch(msg, mActivity);
}
// just do a regular send. We're already on a non-ui thread so no need to fire
// off another thread to do this work.
sendSmsWorker(msgText, semiSepRecipients, threadId);
// Be paranoid and clean any draft SMS up.
deleteDraftSmsMessage(threadId);
}
private void sendSmsWorker(String msgText, String semiSepRecipients, long threadId) {
String[] dests = TextUtils.split(semiSepRecipients, ";");
if (LogTag.VERBOSE || Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
Log.d(LogTag.TRANSACTION, "sendSmsWorker sending message: recipients=" +
semiSepRecipients + ", threadId=" + threadId);
}
MessageSender sender = new SmsMessageSender(mActivity, dests, msgText, threadId);
try {
sender.sendMessage(threadId);
// Make sure this thread isn't over the limits in message count
Recycler.getSmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
} catch (Exception e) {
Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e);
}
mStatusListener.onMessageSent();
MmsWidgetProvider.notifyDatasetChanged(mActivity);
}
private void sendMmsWorker(Conversation conv, Uri mmsUri, PduPersister persister,
SlideshowModel slideshow, SendReq sendReq, boolean textOnly) {
long threadId = 0;
Cursor cursor = null;
boolean newMessage = false;
try {
// Put a placeholder message in the database first
DraftCache.getInstance().setSavingDraft(true);
mStatusListener.onPreMessageSent();
// Make sure we are still using the correct thread ID for our
// recipient set.
threadId = conv.ensureThreadId();
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("sendMmsWorker: update draft MMS message " + mmsUri +
" threadId: " + threadId);
}
// One last check to verify the address of the recipient.
String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
if (dests.length == 1) {
// verify the single address matches what's in the database. If we get a different
// address back, jam the new value back into the SendReq.
String newAddress =
Conversation.verifySingleRecipient(mActivity, conv.getThreadId(), dests[0]);
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("sendMmsWorker: newAddress " + newAddress +
" dests[0]: " + dests[0]);
}
if (!newAddress.equals(dests[0])) {
dests[0] = newAddress;
EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
if (encodedNumbers != null) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("sendMmsWorker: REPLACING number!!!");
}
sendReq.setTo(encodedNumbers);
}
}
}
newMessage = mmsUri == null;
if (newMessage) {
// Write something in the database so the new message will appear as sending
ContentValues values = new ContentValues();
values.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_OUTBOX);
values.put(Mms.THREAD_ID, threadId);
values.put(Mms.MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ);
if (textOnly) {
values.put(Mms.TEXT_ONLY, 1);
}
mmsUri = SqliteWrapper.insert(mActivity, mContentResolver, Mms.Outbox.CONTENT_URI,
values);
}
mStatusListener.onMessageSent();
// If user tries to send the message, it's a signal the inputted text is
// what they wanted.
UserHappinessSignals.userAcceptedImeText(mActivity);
// First make sure we don't have too many outstanding unsent message.
cursor = SqliteWrapper.query(mActivity, mContentResolver,
Mms.Outbox.CONTENT_URI, MMS_OUTBOX_PROJECTION, null, null, null);
if (cursor != null) {
long maxMessageSize = MmsConfig.getMaxSizeScaleForPendingMmsAllowed() *
MmsConfig.getMaxMessageSize();
long totalPendingSize = 0;
while (cursor.moveToNext()) {
totalPendingSize += cursor.getLong(MMS_MESSAGE_SIZE_INDEX);
}
if (totalPendingSize >= maxMessageSize) {
unDiscard(); // it wasn't successfully sent. Allow it to be saved as a draft.
mStatusListener.onMaxPendingMessagesReached();
markMmsMessageWithError(mmsUri);
return;
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
try {
if (newMessage) {
// Create a new MMS message if one hasn't been made yet.
mmsUri = createDraftMmsMessage(persister, sendReq, slideshow, mmsUri,
mActivity, null);
} else {
// Otherwise, sync the MMS message in progress to disk.
updateDraftMmsMessage(mmsUri, persister, slideshow, sendReq, null);
}
// Be paranoid and clean any draft SMS up.
deleteDraftSmsMessage(threadId);
} finally {
DraftCache.getInstance().setSavingDraft(false);
}
// Resize all the resizeable attachments (e.g. pictures) to fit
// in the remaining space in the slideshow.
int error = 0;
try {
slideshow.finalResize(mmsUri);
} catch (ExceedMessageSizeException e1) {
error = MESSAGE_SIZE_EXCEEDED;
} catch (MmsException e1) {
error = UNKNOWN_ERROR;
}
if (error != 0) {
markMmsMessageWithError(mmsUri);
mStatusListener.onAttachmentError(error);
return;
}
MessageSender sender = new MmsMessageSender(mActivity, mmsUri,
slideshow.getCurrentMessageSize());
try {
if (!sender.sendMessage(threadId)) {
// The message was sent through SMS protocol, we should
// delete the copy which was previously saved in MMS drafts.
SqliteWrapper.delete(mActivity, mContentResolver, mmsUri, null, null);
}
// Make sure this thread isn't over the limits in message count
Recycler.getMmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
} catch (Exception e) {
Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e);
}
MmsWidgetProvider.notifyDatasetChanged(mActivity);
}
private void markMmsMessageWithError(Uri mmsUri) {
try {
PduPersister p = PduPersister.getPduPersister(mActivity);
// Move the message into MMS Outbox. A trigger will create an entry in
// the "pending_msgs" table.
p.move(mmsUri, Mms.Outbox.CONTENT_URI);
// Now update the pending_msgs table with an error for that new item.
ContentValues values = new ContentValues(1);
values.put(PendingMessages.ERROR_TYPE, MmsSms.ERR_TYPE_GENERIC_PERMANENT);
long msgId = ContentUris.parseId(mmsUri);
SqliteWrapper.update(mActivity, mContentResolver,
PendingMessages.CONTENT_URI,
values, PendingMessages.MSG_ID + "=" + msgId, null);
} catch (MmsException e) {
// Not much we can do here. If the p.move throws an exception, we'll just
// leave the message in the draft box.
Log.e(TAG, "Failed to move message to outbox and mark as error: " + mmsUri, e);
}
}
// Draft message stuff
private static final String[] MMS_DRAFT_PROJECTION = {
Mms._ID, // 0
Mms.SUBJECT, // 1
Mms.SUBJECT_CHARSET // 2
};
private static final int MMS_ID_INDEX = 0;
private static final int MMS_SUBJECT_INDEX = 1;
private static final int MMS_SUBJECT_CS_INDEX = 2;
private static Uri readDraftMmsMessage(Context context, Conversation conv, StringBuilder sb) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("readDraftMmsMessage conv: " + conv);
}
Cursor cursor;
ContentResolver cr = context.getContentResolver();
final String selection = Mms.THREAD_ID + " = " + conv.getThreadId();
cursor = SqliteWrapper.query(context, cr,
Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION,
selection, null, null);
Uri uri;
try {
if (cursor.moveToFirst()) {
uri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI,
cursor.getLong(MMS_ID_INDEX));
String subject = MessageUtils.extractEncStrFromCursor(cursor, MMS_SUBJECT_INDEX,
MMS_SUBJECT_CS_INDEX);
if (subject != null) {
sb.append(subject);
}
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("readDraftMmsMessage uri: ", uri);
}
return uri;
}
} finally {
cursor.close();
}
return null;
}
/**
* makeSendReq should always return a non-null SendReq, whether the dest addresses are
* valid or not.
*/
private static SendReq makeSendReq(Conversation conv, CharSequence subject) {
String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
SendReq req = new SendReq();
EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
if (encodedNumbers != null) {
req.setTo(encodedNumbers);
}
if (!TextUtils.isEmpty(subject)) {
req.setSubject(new EncodedStringValue(subject.toString()));
}
req.setDate(System.currentTimeMillis() / 1000L);
return req;
}
private static Uri createDraftMmsMessage(PduPersister persister, SendReq sendReq,
SlideshowModel slideshow, Uri preUri, Context context,
HashMap<Uri, InputStream> preOpenedFiles) {
if (slideshow == null) {
return null;
}
try {
PduBody pb = slideshow.toPduBody();
sendReq.setBody(pb);
Uri res = persister.persist(sendReq, preUri == null ? Mms.Draft.CONTENT_URI : preUri,
true, MessagingPreferenceActivity.getIsGroupMmsEnabled(context),
preOpenedFiles);
slideshow.sync(pb);
return res;
} catch (MmsException e) {
return null;
}
}
private void asyncUpdateDraftMmsMessage(final Conversation conv, final boolean isStopping) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("asyncUpdateDraftMmsMessage conv=%s mMessageUri=%s", conv, mMessageUri);
}
final HashMap<Uri, InputStream> preOpenedFiles =
mSlideshow.openPartFiles(mContentResolver);
new Thread(new Runnable() {
@Override
public void run() {
try {
DraftCache.getInstance().setSavingDraft(true);
final PduPersister persister = PduPersister.getPduPersister(mActivity);
final SendReq sendReq = makeSendReq(conv, mSubject);
if (mMessageUri == null) {
mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow, null,
mActivity, preOpenedFiles);
} else {
updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq,
preOpenedFiles);
}
ensureThreadIdIfNeeded(conv, isStopping);
conv.setDraftState(true);
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("asyncUpdateDraftMmsMessage conv: " + conv +
" uri: " + mMessageUri);
}
// Be paranoid and delete any SMS drafts that might be lying around. Must do
// this after ensureThreadId so conv has the correct thread id.
asyncDeleteDraftSmsMessage(conv);
} finally {
DraftCache.getInstance().setSavingDraft(false);
closePreOpenedFiles(preOpenedFiles);
}
}
}, "WorkingMessage.asyncUpdateDraftMmsMessage").start();
}
private static void updateDraftMmsMessage(Uri uri, PduPersister persister,
SlideshowModel slideshow, SendReq sendReq, HashMap<Uri, InputStream> preOpenedFiles) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("updateDraftMmsMessage uri=%s", uri);
}
if (uri == null) {
Log.e(TAG, "updateDraftMmsMessage null uri");
return;
}
persister.updateHeaders(uri, sendReq);
final PduBody pb = slideshow.toPduBody();
try {
persister.updateParts(uri, pb, preOpenedFiles);
} catch (MmsException e) {
Log.e(TAG, "updateDraftMmsMessage: cannot update message " + uri);
}
slideshow.sync(pb);
}
private static void closePreOpenedFiles(HashMap<Uri, InputStream> preOpenedFiles) {
if (preOpenedFiles == null) {
return;
}
Set<Uri> uris = preOpenedFiles.keySet();
for (Uri uri : uris) {
InputStream is = preOpenedFiles.get(uri);
if (is != null) {
try {
is.close();
} catch (IOException e) {
}
}
}
}
private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT;
private static final String[] SMS_BODY_PROJECTION = { Sms.BODY };
private static final int SMS_BODY_INDEX = 0;
/**
* Reads a draft message for the given thread ID from the database,
* if there is one, deletes it from the database, and returns it.
* @return The draft message or an empty string.
*/
private String readDraftSmsMessage(Conversation conv) {
long thread_id = conv.getThreadId();
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.d(TAG, "readDraftSmsMessage conv: " + conv);
}
// If it's an invalid thread or we know there's no draft, don't bother.
if (thread_id <= 0 || !conv.hasDraft()) {
return "";
}
Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id);
String body = "";
Cursor c = SqliteWrapper.query(mActivity, mContentResolver,
thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null);
boolean haveDraft = false;
if (c != null) {
try {
if (c.moveToFirst()) {
body = c.getString(SMS_BODY_INDEX);
haveDraft = true;
}
} finally {
c.close();
}
}
// We found a draft, and if there are no messages in the conversation,
// that means we deleted the thread, too. Must reset the thread id
// so we'll eventually create a new thread.
if (haveDraft && conv.getMessageCount() == 0) {
asyncDeleteDraftSmsMessage(conv);
// Clean out drafts for this thread -- if the recipient set changes,
// we will lose track of the original draft and be unable to delete
// it later. The message will be re-saved if necessary upon exit of
// the activity.
clearConversation(conv, true);
}
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("readDraftSmsMessage haveDraft: ", !TextUtils.isEmpty(body));
}
return body;
}
public void clearConversation(final Conversation conv, boolean resetThreadId) {
if (resetThreadId && conv.getMessageCount() == 0) {
if (DEBUG) LogTag.debug("clearConversation calling clearThreadId");
conv.clearThreadId();
}
conv.setDraftState(false);
}
private void asyncUpdateDraftSmsMessage(final Conversation conv, final String contents,
final boolean isStopping) {
new Thread(new Runnable() {
@Override
public void run() {
try {
DraftCache.getInstance().setSavingDraft(true);
if (conv.getRecipients().isEmpty()) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("asyncUpdateDraftSmsMessage no recipients, not saving");
}
return;
}
ensureThreadIdIfNeeded(conv, isStopping);
conv.setDraftState(true);
updateDraftSmsMessage(conv, contents);
} finally {
DraftCache.getInstance().setSavingDraft(false);
}
}
}, "WorkingMessage.asyncUpdateDraftSmsMessage").start();
}
private void updateDraftSmsMessage(final Conversation conv, String contents) {
final long threadId = conv.getThreadId();
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("updateDraftSmsMessage tid=%d, contents=\"%s\"", threadId, contents);
}
// If we don't have a valid thread, there's nothing to do.
if (threadId <= 0) {
return;
}
ContentValues values = new ContentValues(3);
values.put(Sms.THREAD_ID, threadId);
values.put(Sms.BODY, contents);
values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT);
SqliteWrapper.insert(mActivity, mContentResolver, Sms.CONTENT_URI, values);
asyncDeleteDraftMmsMessage(conv);
mMessageUri = null;
}
private void asyncDelete(final Uri uri, final String selection, final String[] selectionArgs) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("asyncDelete %s where %s", uri, selection);
}
new Thread(new Runnable() {
@Override
public void run() {
SqliteWrapper.delete(mActivity, mContentResolver, uri, selection, selectionArgs);
}
}, "WorkingMessage.asyncDelete").start();
}
public void asyncDeleteDraftSmsMessage(Conversation conv) {
mHasSmsDraft = false;
final long threadId = conv.getThreadId();
if (threadId > 0) {
asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
SMS_DRAFT_WHERE, null);
}
}
private void deleteDraftSmsMessage(long threadId) {
SqliteWrapper.delete(mActivity, mContentResolver,
ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
SMS_DRAFT_WHERE, null);
}
private void asyncDeleteDraftMmsMessage(Conversation conv) {
mHasMmsDraft = false;
final long threadId = conv.getThreadId();
// If the thread id is < 1, then the thread_id in the pdu will be "" or NULL. We have
// to clear those messages as well as ones with a valid thread id.
final String where = Mms.THREAD_ID + (threadId > 0 ? " = " + threadId : " IS NULL");
asyncDelete(Mms.Draft.CONTENT_URI, where, null);
}
/**
* Ensure the thread id in conversation if needed, when we try to save a draft with a orphaned
* one.
* @param conv The conversation we are in.
* @param isStopping Whether we are saving the draft in CMA'a onStop
*/
private void ensureThreadIdIfNeeded(final Conversation conv, final boolean isStopping) {
if (isStopping && conv.getMessageCount() == 0) {
// We need to save the drafts in an unorphaned thread id. When the user goes
// back to ConversationList while we're saving a draft from CMA's.onStop,
// ConversationList will delete all threads from the thread table that
// don't have associated sms or pdu entries. In case our thread got deleted,
// well call clearThreadId() so ensureThreadId will query the db for the new
// thread.
conv.clearThreadId(); // force us to get the updated thread id
}
if (!conv.getRecipients().isEmpty()) {
conv.ensureThreadId();
}
}
}