• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1  /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.mms.data;
18 
19 import java.util.List;
20 
21 import android.app.Activity;
22 import android.content.ContentResolver;
23 import android.content.ContentUris;
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.database.Cursor;
27 import android.database.sqlite.SqliteWrapper;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.provider.Telephony.Mms;
31 import android.provider.Telephony.MmsSms;
32 import android.provider.Telephony.Sms;
33 import android.provider.Telephony.MmsSms.PendingMessages;
34 import android.telephony.SmsMessage;
35 import android.text.TextUtils;
36 import android.util.Log;
37 
38 import com.android.common.userhappiness.UserHappinessSignals;
39 import com.android.mms.ExceedMessageSizeException;
40 import com.android.mms.LogTag;
41 import com.android.mms.MmsConfig;
42 import com.android.mms.ResolutionException;
43 import com.android.mms.UnsupportContentTypeException;
44 import com.android.mms.model.AudioModel;
45 import com.android.mms.model.ImageModel;
46 import com.android.mms.model.MediaModel;
47 import com.android.mms.model.SlideModel;
48 import com.android.mms.model.SlideshowModel;
49 import com.android.mms.model.TextModel;
50 import com.android.mms.model.VideoModel;
51 import com.android.mms.transaction.MessageSender;
52 import com.android.mms.transaction.MmsMessageSender;
53 import com.android.mms.transaction.SmsMessageSender;
54 import com.android.mms.ui.ComposeMessageActivity;
55 import com.android.mms.ui.MessageUtils;
56 import com.android.mms.ui.SlideshowEditor;
57 import com.android.mms.util.Recycler;
58 import com.google.android.mms.ContentType;
59 import com.google.android.mms.MmsException;
60 import com.google.android.mms.pdu.EncodedStringValue;
61 import com.google.android.mms.pdu.PduBody;
62 import com.google.android.mms.pdu.PduPersister;
63 import com.google.android.mms.pdu.SendReq;
64 
65 import android.telephony.SmsMessage;
66 
67 /**
68  * Contains all state related to a message being edited by the user.
69  */
70 public class WorkingMessage {
71     private static final String TAG = "WorkingMessage";
72     private static final boolean DEBUG = false;
73 
74     // Public intents
75     public static final String ACTION_SENDING_SMS = "android.intent.action.SENDING_SMS";
76 
77     // Intent extras
78     public static final String EXTRA_SMS_MESSAGE = "android.mms.extra.MESSAGE";
79     public static final String EXTRA_SMS_RECIPIENTS = "android.mms.extra.RECIPIENTS";
80     public static final String EXTRA_SMS_THREAD_ID = "android.mms.extra.THREAD_ID";
81 
82     // Database access stuff
83     private final Activity mActivity;
84     private final ContentResolver mContentResolver;
85 
86     // States that can require us to save or send a message as MMS.
87     private static final int RECIPIENTS_REQUIRE_MMS = (1 << 0);     // 1
88     private static final int HAS_SUBJECT = (1 << 1);                // 2
89     private static final int HAS_ATTACHMENT = (1 << 2);             // 4
90     private static final int LENGTH_REQUIRES_MMS = (1 << 3);        // 8
91     private static final int FORCE_MMS = (1 << 4);                  // 16
92 
93     // A bitmap of the above indicating different properties of the message;
94     // any bit set will require the message to be sent via MMS.
95     private int mMmsState;
96 
97     // Errors from setAttachment()
98     public static final int OK = 0;
99     public static final int UNKNOWN_ERROR = -1;
100     public static final int MESSAGE_SIZE_EXCEEDED = -2;
101     public static final int UNSUPPORTED_TYPE = -3;
102     public static final int IMAGE_TOO_LARGE = -4;
103 
104     // Attachment types
105     public static final int TEXT = 0;
106     public static final int IMAGE = 1;
107     public static final int VIDEO = 2;
108     public static final int AUDIO = 3;
109     public static final int SLIDESHOW = 4;
110 
111     // Current attachment type of the message; one of the above values.
112     private int mAttachmentType;
113 
114     // Conversation this message is targeting.
115     private Conversation mConversation;
116 
117     // Text of the message.
118     private CharSequence mText;
119     // Slideshow for this message, if applicable.  If it's a simple attachment,
120     // i.e. not SLIDESHOW, it will contain only one slide.
121     private SlideshowModel mSlideshow;
122     // Data URI of an MMS message if we have had to save it.
123     private Uri mMessageUri;
124     // MMS subject line for this message
125     private CharSequence mSubject;
126 
127     // Set to true if this message has been discarded.
128     private boolean mDiscarded = false;
129 
130     // Cached value of mms enabled flag
131     private static boolean sMmsEnabled = MmsConfig.getMmsEnabled();
132 
133     // Our callback interface
134     private final MessageStatusListener mStatusListener;
135     private List<String> mWorkingRecipients;
136 
137     // Message sizes in Outbox
138     private static final String[] MMS_OUTBOX_PROJECTION = {
139         Mms._ID,            // 0
140         Mms.MESSAGE_SIZE    // 1
141     };
142 
143     private static final int MMS_MESSAGE_SIZE_INDEX  = 1;
144 
145     /**
146      * Callback interface for communicating important state changes back to
147      * ComposeMessageActivity.
148      */
149     public interface MessageStatusListener {
150         /**
151          * Called when the protocol for sending the message changes from SMS
152          * to MMS, and vice versa.
153          *
154          * @param mms If true, it changed to MMS.  If false, to SMS.
155          */
onProtocolChanged(boolean mms)156         void onProtocolChanged(boolean mms);
157 
158         /**
159          * Called when an attachment on the message has changed.
160          */
onAttachmentChanged()161         void onAttachmentChanged();
162 
163         /**
164          * Called just before the process of sending a message.
165          */
onPreMessageSent()166         void onPreMessageSent();
167 
168         /**
169          * Called once the process of sending a message, triggered by
170          * {@link send} has completed. This doesn't mean the send succeeded,
171          * just that it has been dispatched to the network.
172          */
onMessageSent()173         void onMessageSent();
174 
175         /**
176          * Called if there are too many unsent messages in the queue and we're not allowing
177          * any more Mms's to be sent.
178          */
onMaxPendingMessagesReached()179         void onMaxPendingMessagesReached();
180 
181         /**
182          * Called if there's an attachment error while resizing the images just before sending.
183          */
onAttachmentError(int error)184         void onAttachmentError(int error);
185     }
186 
WorkingMessage(ComposeMessageActivity activity)187     private WorkingMessage(ComposeMessageActivity activity) {
188         mActivity = activity;
189         mContentResolver = mActivity.getContentResolver();
190         mStatusListener = activity;
191         mAttachmentType = TEXT;
192         mText = "";
193     }
194 
195     /**
196      * Creates a new working message.
197      */
createEmpty(ComposeMessageActivity activity)198     public static WorkingMessage createEmpty(ComposeMessageActivity activity) {
199         // Make a new empty working message.
200         WorkingMessage msg = new WorkingMessage(activity);
201         return msg;
202     }
203 
204     /**
205      * Create a new WorkingMessage from the specified data URI, which typically
206      * contains an MMS message.
207      */
load(ComposeMessageActivity activity, Uri uri)208     public static WorkingMessage load(ComposeMessageActivity activity, Uri uri) {
209         // If the message is not already in the draft box, move it there.
210         if (!uri.toString().startsWith(Mms.Draft.CONTENT_URI.toString())) {
211             PduPersister persister = PduPersister.getPduPersister(activity);
212             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
213                 LogTag.debug("load: moving %s to drafts", uri);
214             }
215             try {
216                 uri = persister.move(uri, Mms.Draft.CONTENT_URI);
217             } catch (MmsException e) {
218                 LogTag.error("Can't move %s to drafts", uri);
219                 return null;
220             }
221         }
222 
223         WorkingMessage msg = new WorkingMessage(activity);
224         if (msg.loadFromUri(uri)) {
225             return msg;
226         }
227 
228         return null;
229     }
230 
correctAttachmentState()231     private void correctAttachmentState() {
232         int slideCount = mSlideshow.size();
233 
234         // If we get an empty slideshow, tear down all MMS
235         // state and discard the unnecessary message Uri.
236         if (slideCount == 0) {
237             mAttachmentType = TEXT;
238             mSlideshow = null;
239             if (mMessageUri != null) {
240                 asyncDelete(mMessageUri, null, null);
241                 mMessageUri = null;
242             }
243         } else if (slideCount > 1) {
244             mAttachmentType = SLIDESHOW;
245         } else {
246             SlideModel slide = mSlideshow.get(0);
247             if (slide.hasImage()) {
248                 mAttachmentType = IMAGE;
249             } else if (slide.hasVideo()) {
250                 mAttachmentType = VIDEO;
251             } else if (slide.hasAudio()) {
252                 mAttachmentType = AUDIO;
253             }
254         }
255 
256         updateState(HAS_ATTACHMENT, hasAttachment(), false);
257     }
258 
loadFromUri(Uri uri)259     private boolean loadFromUri(Uri uri) {
260         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("loadFromUri %s", uri);
261         try {
262             mSlideshow = SlideshowModel.createFromMessageUri(mActivity, uri);
263         } catch (MmsException e) {
264             LogTag.error("Couldn't load URI %s", uri);
265             return false;
266         }
267 
268         mMessageUri = uri;
269 
270         // Make sure all our state is as expected.
271         syncTextFromSlideshow();
272         correctAttachmentState();
273 
274         return true;
275     }
276 
277     /**
278      * Load the draft message for the specified conversation, or a new empty message if
279      * none exists.
280      */
loadDraft(ComposeMessageActivity activity, Conversation conv)281     public static WorkingMessage loadDraft(ComposeMessageActivity activity,
282                                            Conversation conv) {
283         WorkingMessage msg = new WorkingMessage(activity);
284         if (msg.loadFromConversation(conv)) {
285             return msg;
286         } else {
287             return createEmpty(activity);
288         }
289     }
290 
loadFromConversation(Conversation conv)291     private boolean loadFromConversation(Conversation conv) {
292         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("loadFromConversation %s", conv);
293 
294         long threadId = conv.getThreadId();
295         if (threadId <= 0) {
296             return false;
297         }
298 
299         // Look for an SMS draft first.
300         mText = readDraftSmsMessage(conv);
301         if (!TextUtils.isEmpty(mText)) {
302             return true;
303         }
304 
305         // Then look for an MMS draft.
306         StringBuilder sb = new StringBuilder();
307         Uri uri = readDraftMmsMessage(mActivity, threadId, sb);
308         if (uri != null) {
309             if (loadFromUri(uri)) {
310                 // If there was an MMS message, readDraftMmsMessage
311                 // will put the subject in our supplied StringBuilder.
312                 if (sb.length() > 0) {
313                     setSubject(sb.toString(), false);
314                 }
315                 return true;
316             }
317         }
318 
319         return false;
320     }
321 
322     /**
323      * Sets the text of the message to the specified CharSequence.
324      */
setText(CharSequence s)325     public void setText(CharSequence s) {
326         mText = s;
327     }
328 
329     /**
330      * Returns the current message text.
331      */
getText()332     public CharSequence getText() {
333         return mText;
334     }
335 
336     /**
337      * Returns true if the message has any text. A message with just whitespace is not considered
338      * to have text.
339      * @return
340      */
hasText()341     public boolean hasText() {
342         return mText != null && TextUtils.getTrimmedLength(mText) > 0;
343     }
344 
345     /**
346      * Adds an attachment to the message, replacing an old one if it existed.
347      * @param type Type of this attachment, such as {@link IMAGE}
348      * @param dataUri Uri containing the attachment data (or null for {@link TEXT})
349      * @param append true if we should add the attachment to a new slide
350      * @return An error code such as {@link UNKNOWN_ERROR} or {@link OK} if successful
351      */
setAttachment(int type, Uri dataUri, boolean append)352     public int setAttachment(int type, Uri dataUri, boolean append) {
353         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
354             LogTag.debug("setAttachment type=%d uri %s", type, dataUri);
355         }
356         int result = OK;
357 
358         // Make sure mSlideshow is set up and has a slide.
359         ensureSlideshow();
360 
361         // Change the attachment and translate the various underlying
362         // exceptions into useful error codes.
363         try {
364             if (append) {
365                 appendMedia(type, dataUri);
366             } else {
367                 changeMedia(type, dataUri);
368             }
369         } catch (MmsException e) {
370             result = UNKNOWN_ERROR;
371         } catch (UnsupportContentTypeException e) {
372             result = UNSUPPORTED_TYPE;
373         } catch (ExceedMessageSizeException e) {
374             result = MESSAGE_SIZE_EXCEEDED;
375         } catch (ResolutionException e) {
376             result = IMAGE_TOO_LARGE;
377         }
378 
379         // If we were successful, update mAttachmentType and notify
380         // the listener than there was a change.
381         if (result == OK) {
382             mAttachmentType = type;
383             mStatusListener.onAttachmentChanged();
384         } else if (append) {
385             // We added a new slide and what we attempted to insert on the slide failed.
386             // Delete that slide, otherwise we could end up with a bunch of blank slides.
387             SlideshowEditor slideShowEditor = new SlideshowEditor(mActivity, mSlideshow);
388             slideShowEditor.removeSlide(mSlideshow.size() - 1);
389         }
390 
391         if (!MmsConfig.getMultipartSmsEnabled()) {
392             if (!append && mAttachmentType == TEXT && type == TEXT) {
393                 int[] params = SmsMessage.calculateLength(getText(), false);
394                 /* SmsMessage.calculateLength returns an int[4] with:
395                 *   int[0] being the number of SMS's required,
396                 *   int[1] the number of code units used,
397                 *   int[2] is the number of code units remaining until the next message.
398                 *   int[3] is the encoding type that should be used for the message.
399                 */
400                 int msgCount = params[0];
401 
402                 if (msgCount >= MmsConfig.getSmsToMmsTextThreshold()) {
403                     setLengthRequiresMms(true, false);
404                 } else {
405                     updateState(HAS_ATTACHMENT, hasAttachment(), true);
406                 }
407             } else {
408                 updateState(HAS_ATTACHMENT, hasAttachment(), true);
409             }
410         } else {
411             // Set HAS_ATTACHMENT if we need it.
412             updateState(HAS_ATTACHMENT, hasAttachment(), true);
413         }
414         correctAttachmentState();
415         return result;
416     }
417 
418     /**
419      * Returns true if this message contains anything worth saving.
420      */
isWorthSaving()421     public boolean isWorthSaving() {
422         // If it actually contains anything, it's of course not empty.
423         if (hasText() || hasSubject() || hasAttachment() || hasSlideshow()) {
424             return true;
425         }
426 
427         // When saveAsMms() has been called, we set FORCE_MMS to represent
428         // sort of an "invisible attachment" so that the message isn't thrown
429         // away when we are shipping it off to other activities.
430         if (isFakeMmsForDraft()) {
431             return true;
432         }
433 
434         return false;
435     }
436 
437     /**
438      * Returns true if FORCE_MMS is set.
439      * When saveAsMms() has been called, we set FORCE_MMS to represent
440      * sort of an "invisible attachment" so that the message isn't thrown
441      * away when we are shipping it off to other activities.
442      */
isFakeMmsForDraft()443     public boolean isFakeMmsForDraft() {
444         return (mMmsState & FORCE_MMS) > 0;
445     }
446 
447     /**
448      * Makes sure mSlideshow is set up.
449      */
ensureSlideshow()450     private void ensureSlideshow() {
451         if (mSlideshow != null) {
452             return;
453         }
454 
455         SlideshowModel slideshow = SlideshowModel.createNew(mActivity);
456         SlideModel slide = new SlideModel(slideshow);
457         slideshow.add(slide);
458 
459         mSlideshow = slideshow;
460     }
461 
462     /**
463      * Change the message's attachment to the data in the specified Uri.
464      * Used only for single-slide ("attachment mode") messages.
465      */
changeMedia(int type, Uri uri)466     private void changeMedia(int type, Uri uri) throws MmsException {
467         SlideModel slide = mSlideshow.get(0);
468         MediaModel media;
469 
470         if (slide == null) {
471             Log.w(LogTag.TAG, "[WorkingMessage] changeMedia: no slides!");
472             return;
473         }
474 
475         // Remove any previous attachments.
476         slide.removeImage();
477         slide.removeVideo();
478         slide.removeAudio();
479 
480         // If we're changing to text, just bail out.
481         if (type == TEXT) {
482             return;
483         }
484 
485         // Make a correct MediaModel for the type of attachment.
486         if (type == IMAGE) {
487             media = new ImageModel(mActivity, uri, mSlideshow.getLayout().getImageRegion());
488         } else if (type == VIDEO) {
489             media = new VideoModel(mActivity, uri, mSlideshow.getLayout().getImageRegion());
490         } else if (type == AUDIO) {
491             media = new AudioModel(mActivity, uri);
492         } else {
493             throw new IllegalArgumentException("changeMedia type=" + type + ", uri=" + uri);
494         }
495 
496         // Add it to the slide.
497         slide.add(media);
498 
499         // For video and audio, set the duration of the slide to
500         // that of the attachment.
501         if (type == VIDEO || type == AUDIO) {
502             slide.updateDuration(media.getDuration());
503         }
504     }
505 
506     /**
507      * Add the message's attachment to the data in the specified Uri to a new slide.
508      */
appendMedia(int type, Uri uri)509     private void appendMedia(int type, Uri uri) throws MmsException {
510 
511         // If we're changing to text, just bail out.
512         if (type == TEXT) {
513             return;
514         }
515 
516         // The first time this method is called, mSlideshow.size() is going to be
517         // one (a newly initialized slideshow has one empty slide). The first time we
518         // attach the picture/video to that first empty slide. From then on when this
519         // function is called, we've got to create a new slide and add the picture/video
520         // to that new slide.
521         boolean addNewSlide = true;
522         if (mSlideshow.size() == 1 && !mSlideshow.isSimple()) {
523             addNewSlide = false;
524         }
525         if (addNewSlide) {
526             SlideshowEditor slideShowEditor = new SlideshowEditor(mActivity, mSlideshow);
527             if (!slideShowEditor.addNewSlide()) {
528                 return;
529             }
530         }
531         // Make a correct MediaModel for the type of attachment.
532         MediaModel media;
533         SlideModel slide = mSlideshow.get(mSlideshow.size() - 1);
534         if (type == IMAGE) {
535             media = new ImageModel(mActivity, uri, mSlideshow.getLayout().getImageRegion());
536         } else if (type == VIDEO) {
537             media = new VideoModel(mActivity, uri, mSlideshow.getLayout().getImageRegion());
538         } else if (type == AUDIO) {
539             media = new AudioModel(mActivity, uri);
540         } else {
541             throw new IllegalArgumentException("changeMedia type=" + type + ", uri=" + uri);
542         }
543 
544         // Add it to the slide.
545         slide.add(media);
546 
547         // For video and audio, set the duration of the slide to
548         // that of the attachment.
549         if (type == VIDEO || type == AUDIO) {
550             slide.updateDuration(media.getDuration());
551         }
552     }
553 
554     /**
555      * Returns true if the message has an attachment (including slideshows).
556      */
hasAttachment()557     public boolean hasAttachment() {
558         return (mAttachmentType > TEXT);
559     }
560 
561     /**
562      * Returns the slideshow associated with this message.
563      */
getSlideshow()564     public SlideshowModel getSlideshow() {
565         return mSlideshow;
566     }
567 
568     /**
569      * Returns true if the message has a real slideshow, as opposed to just
570      * one image attachment, for example.
571      */
hasSlideshow()572     public boolean hasSlideshow() {
573         return (mAttachmentType == SLIDESHOW);
574     }
575 
576     /**
577      * Sets the MMS subject of the message.  Passing null indicates that there
578      * is no subject.  Passing "" will result in an empty subject being added
579      * to the message, possibly triggering a conversion to MMS.  This extra
580      * bit of state is needed to support ComposeMessageActivity converting to
581      * MMS when the user adds a subject.  An empty subject will be removed
582      * before saving to disk or sending, however.
583      */
setSubject(CharSequence s, boolean notify)584     public void setSubject(CharSequence s, boolean notify) {
585         mSubject = s;
586         updateState(HAS_SUBJECT, (s != null), notify);
587     }
588 
589     /**
590      * Returns the MMS subject of the message.
591      */
getSubject()592     public CharSequence getSubject() {
593         return mSubject;
594     }
595 
596     /**
597      * Returns true if this message has an MMS subject. A subject has to be more than just
598      * whitespace.
599      * @return
600      */
hasSubject()601     public boolean hasSubject() {
602         return mSubject != null && TextUtils.getTrimmedLength(mSubject) > 0;
603     }
604 
605     /**
606      * Moves the message text into the slideshow.  Should be called any time
607      * the message is about to be sent or written to disk.
608      */
syncTextToSlideshow()609     private void syncTextToSlideshow() {
610         if (mSlideshow == null || mSlideshow.size() != 1)
611             return;
612 
613         SlideModel slide = mSlideshow.get(0);
614         TextModel text;
615         if (!slide.hasText()) {
616             // Add a TextModel to slide 0 if one doesn't already exist
617             text = new TextModel(mActivity, ContentType.TEXT_PLAIN, "text_0.txt",
618                                            mSlideshow.getLayout().getTextRegion());
619             slide.add(text);
620         } else {
621             // Otherwise just reuse the existing one.
622             text = slide.getText();
623         }
624         text.setText(mText);
625     }
626 
627     /**
628      * Sets the message text out of the slideshow.  Should be called any time
629      * a slideshow is loaded from disk.
630      */
syncTextFromSlideshow()631     private void syncTextFromSlideshow() {
632         // Don't sync text for real slideshows.
633         if (mSlideshow.size() != 1) {
634             return;
635         }
636 
637         SlideModel slide = mSlideshow.get(0);
638         if (slide == null || !slide.hasText()) {
639             return;
640         }
641 
642         mText = slide.getText().getText();
643     }
644 
645     /**
646      * Removes the subject if it is empty, possibly converting back to SMS.
647      */
removeSubjectIfEmpty(boolean notify)648     private void removeSubjectIfEmpty(boolean notify) {
649         if (!hasSubject()) {
650             setSubject(null, notify);
651         }
652     }
653 
654     /**
655      * Gets internal message state ready for storage.  Should be called any
656      * time the message is about to be sent or written to disk.
657      */
prepareForSave(boolean notify)658     private void prepareForSave(boolean notify) {
659         // Make sure our working set of recipients is resolved
660         // to first-class Contact objects before we save.
661         syncWorkingRecipients();
662 
663         if (requiresMms()) {
664             ensureSlideshow();
665             syncTextToSlideshow();
666             removeSubjectIfEmpty(notify);
667         }
668     }
669 
670     /**
671      * Resolve the temporary working set of recipients to a ContactList.
672      */
syncWorkingRecipients()673     public void syncWorkingRecipients() {
674         if (mWorkingRecipients != null) {
675             ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
676             mConversation.setRecipients(recipients);    // resets the threadId to zero
677             mWorkingRecipients = null;
678         }
679     }
680 
getWorkingRecipients()681     public String getWorkingRecipients() {
682         // this function is used for DEBUG only
683         if (mWorkingRecipients == null) {
684             return null;
685         }
686         ContactList recipients = ContactList.getByNumbers(mWorkingRecipients, false);
687         return recipients.serialize();
688     }
689 
690     // Call when we've returned from adding an attachment. We're no longer forcing the message
691     // into a Mms message. At this point we either have the goods to make the message a Mms
692     // or we don't. No longer fake it.
removeFakeMmsForDraft()693     public void removeFakeMmsForDraft() {
694         updateState(FORCE_MMS, false, false);
695     }
696 
697     /**
698      * Force the message to be saved as MMS and return the Uri of the message.
699      * Typically used when handing a message off to another activity.
700      */
saveAsMms(boolean notify)701     public Uri saveAsMms(boolean notify) {
702         if (DEBUG) LogTag.debug("save mConversation=%s", mConversation);
703 
704         if (mDiscarded) {
705             throw new IllegalStateException("save() called after discard()");
706         }
707 
708         // FORCE_MMS behaves as sort of an "invisible attachment", making
709         // the message seem non-empty (and thus not discarded).  This bit
710         // is sticky until the last other MMS bit is removed, at which
711         // point the message will fall back to SMS.
712         updateState(FORCE_MMS, true, notify);
713 
714         // Collect our state to be written to disk.
715         prepareForSave(true /* notify */);
716 
717         // Make sure we are saving to the correct thread ID.
718         mConversation.ensureThreadId();
719         mConversation.setDraftState(true);
720 
721         PduPersister persister = PduPersister.getPduPersister(mActivity);
722         SendReq sendReq = makeSendReq(mConversation, mSubject);
723 
724         // If we don't already have a Uri lying around, make a new one.  If we do
725         // have one already, make sure it is synced to disk.
726         if (mMessageUri == null) {
727             mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow);
728         } else {
729             updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq);
730         }
731 
732         return mMessageUri;
733     }
734 
735     /**
736      * Save this message as a draft in the conversation previously specified
737      * to {@link setConversation}.
738      */
saveDraft()739     public void saveDraft() {
740         // If we have discarded the message, just bail out.
741         if (mDiscarded) {
742             return;
743         }
744 
745         // Make sure setConversation was called.
746         if (mConversation == null) {
747             throw new IllegalStateException("saveDraft() called with no conversation");
748         }
749 
750         if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
751             LogTag.debug("saveDraft for mConversation " + mConversation);
752         }
753 
754         // Get ready to write to disk. But don't notify message status when saving draft
755         prepareForSave(false /* notify */);
756 
757         if (requiresMms()) {
758             asyncUpdateDraftMmsMessage(mConversation);
759         } else {
760             String content = mText.toString();
761 
762             // bug 2169583: don't bother creating a thread id only to delete the thread
763             // because the content is empty. When we delete the thread in updateDraftSmsMessage,
764             // we didn't nullify conv.mThreadId, causing a temperary situation where conv
765             // is holding onto a thread id that isn't in the database. If a new message arrives
766             // and takes that thread id (because it's the next thread id to be assigned), the
767             // new message will be merged with the draft message thread, causing confusion!
768             if (!TextUtils.isEmpty(content)) {
769                 asyncUpdateDraftSmsMessage(mConversation, content);
770             }
771         }
772 
773         // Update state of the draft cache.
774         mConversation.setDraftState(true);
775     }
776 
discard()777     synchronized public void discard() {
778         if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
779             LogTag.debug("[WorkingMessage] discard");
780         }
781 
782         if (mDiscarded == true) {
783             return;
784         }
785 
786         // Mark this message as discarded in order to make saveDraft() no-op.
787         mDiscarded = true;
788 
789         // Delete our MMS message, if there is one.
790         if (mMessageUri != null) {
791             asyncDelete(mMessageUri, null, null);
792         }
793 
794         clearConversation(mConversation);
795     }
796 
unDiscard()797     public void unDiscard() {
798         if (DEBUG) LogTag.debug("unDiscard");
799 
800         mDiscarded = false;
801     }
802 
803     /**
804      * Returns true if discard() has been called on this message.
805      */
isDiscarded()806     public boolean isDiscarded() {
807         return mDiscarded;
808     }
809 
810     /**
811      * To be called from our Activity's onSaveInstanceState() to give us a chance
812      * to stow our state away for later retrieval.
813      *
814      * @param bundle The Bundle passed in to onSaveInstanceState
815      */
writeStateToBundle(Bundle bundle)816     public void writeStateToBundle(Bundle bundle) {
817         if (hasSubject()) {
818             bundle.putString("subject", mSubject.toString());
819         }
820 
821         if (mMessageUri != null) {
822             bundle.putParcelable("msg_uri", mMessageUri);
823         } else if (hasText()) {
824             bundle.putString("sms_body", mText.toString());
825         }
826     }
827 
828     /**
829      * To be called from our Activity's onCreate() if the activity manager
830      * has given it a Bundle to reinflate
831      * @param bundle The Bundle passed in to onCreate
832      */
readStateFromBundle(Bundle bundle)833     public void readStateFromBundle(Bundle bundle) {
834         if (bundle == null) {
835             return;
836         }
837 
838         String subject = bundle.getString("subject");
839         setSubject(subject, false);
840 
841         Uri uri = (Uri)bundle.getParcelable("msg_uri");
842         if (uri != null) {
843             loadFromUri(uri);
844             return;
845         } else {
846             String body = bundle.getString("sms_body");
847             mText = body;
848         }
849     }
850 
851     /**
852      * Update the temporary list of recipients, used when setting up a
853      * new conversation.  Will be converted to a ContactList on any
854      * save event (send, save draft, etc.)
855      */
setWorkingRecipients(List<String> numbers)856     public void setWorkingRecipients(List<String> numbers) {
857         mWorkingRecipients = numbers;
858         Log.i(TAG, "setWorkingRecipients");
859     }
860 
dumpWorkingRecipients()861     private void dumpWorkingRecipients() {
862         Log.i(TAG, "-- mWorkingRecipients:");
863 
864         if (mWorkingRecipients != null) {
865             int count = mWorkingRecipients.size();
866             for (int i=0; i<count; i++) {
867                 Log.i(TAG, "   [" + i + "] " + mWorkingRecipients.get(i));
868             }
869             Log.i(TAG, "");
870         }
871     }
872 
dump()873     public void dump() {
874         Log.i(TAG, "WorkingMessage:");
875         dumpWorkingRecipients();
876         if (mConversation != null) {
877             Log.i(TAG, "mConversation: " + mConversation.toString());
878         }
879     }
880 
881     /**
882      * Set the conversation associated with this message.
883      */
setConversation(Conversation conv)884     public void setConversation(Conversation conv) {
885         if (DEBUG) LogTag.debug("setConversation %s -> %s", mConversation, conv);
886 
887         mConversation = conv;
888 
889         // Convert to MMS if there are any email addresses in the recipient list.
890         setHasEmail(conv.getRecipients().containsEmail(), false);
891     }
892 
893     /**
894      * Hint whether or not this message will be delivered to an
895      * an email address.
896      */
setHasEmail(boolean hasEmail, boolean notify)897     public void setHasEmail(boolean hasEmail, boolean notify) {
898         if (MmsConfig.getEmailGateway() != null) {
899             updateState(RECIPIENTS_REQUIRE_MMS, false, notify);
900         } else {
901             updateState(RECIPIENTS_REQUIRE_MMS, hasEmail, notify);
902         }
903     }
904 
905     /**
906      * Returns true if this message would require MMS to send.
907      */
requiresMms()908     public boolean requiresMms() {
909         return (mMmsState > 0);
910     }
911 
912     /**
913      * Set whether or not we want to send this message via MMS in order to
914      * avoid sending an excessive number of concatenated SMS messages.
915      * @param: mmsRequired is the value for the LENGTH_REQUIRES_MMS bit.
916      * @param: notify Whether or not to notify the user.
917     */
setLengthRequiresMms(boolean mmsRequired, boolean notify)918     public void setLengthRequiresMms(boolean mmsRequired, boolean notify) {
919         updateState(LENGTH_REQUIRES_MMS, mmsRequired, notify);
920     }
921 
stateString(int state)922     private static String stateString(int state) {
923         if (state == 0)
924             return "<none>";
925 
926         StringBuilder sb = new StringBuilder();
927         if ((state & RECIPIENTS_REQUIRE_MMS) > 0)
928             sb.append("RECIPIENTS_REQUIRE_MMS | ");
929         if ((state & HAS_SUBJECT) > 0)
930             sb.append("HAS_SUBJECT | ");
931         if ((state & HAS_ATTACHMENT) > 0)
932             sb.append("HAS_ATTACHMENT | ");
933         if ((state & LENGTH_REQUIRES_MMS) > 0)
934             sb.append("LENGTH_REQUIRES_MMS | ");
935         if ((state & FORCE_MMS) > 0)
936             sb.append("FORCE_MMS | ");
937 
938         sb.delete(sb.length() - 3, sb.length());
939         return sb.toString();
940     }
941 
942     /**
943      * Sets the current state of our various "MMS required" bits.
944      *
945      * @param state The bit to change, such as {@link HAS_ATTACHMENT}
946      * @param on If true, set it; if false, clear it
947      * @param notify Whether or not to notify the user
948      */
updateState(int state, boolean on, boolean notify)949     private void updateState(int state, boolean on, boolean notify) {
950         if (!sMmsEnabled) {
951             // If Mms isn't enabled, the rest of the Messaging UI should not be using any
952             // feature that would cause us to to turn on any Mms flag and show the
953             // "Converting to multimedia..." message.
954             return;
955         }
956         int oldState = mMmsState;
957         if (on) {
958             mMmsState |= state;
959         } else {
960             mMmsState &= ~state;
961         }
962 
963         // If we are clearing the last bit that is not FORCE_MMS,
964         // expire the FORCE_MMS bit.
965         if (mMmsState == FORCE_MMS && ((oldState & ~FORCE_MMS) > 0)) {
966             mMmsState = 0;
967         }
968 
969         // Notify the listener if we are moving from SMS to MMS
970         // or vice versa.
971         if (notify) {
972             if (oldState == 0 && mMmsState != 0) {
973                 mStatusListener.onProtocolChanged(true);
974             } else if (oldState != 0 && mMmsState == 0) {
975                 mStatusListener.onProtocolChanged(false);
976             }
977         }
978 
979         if (oldState != mMmsState) {
980             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) LogTag.debug("updateState: %s%s = %s",
981                     on ? "+" : "-",
982                     stateString(state), stateString(mMmsState));
983         }
984     }
985 
986     /**
987      * Send this message over the network.  Will call back with onMessageSent() once
988      * it has been dispatched to the telephony stack.  This WorkingMessage object is
989      * no longer useful after this method has been called.
990      */
send(final String recipientsInUI)991     public void send(final String recipientsInUI) {
992         if (Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
993             LogTag.debug("send");
994         }
995 
996         // Begin -------- debug code
997         if (LogTag.VERBOSE) {
998             Log.i(TAG, "##### send #####");
999             Log.i(TAG, "   mConversation (beginning of send): " + mConversation.toString());
1000             Log.i(TAG, "   recipientsInUI: " + recipientsInUI);
1001         }
1002         // End -------- debug code
1003 
1004         // Get ready to write to disk.
1005         prepareForSave(true /* notify */);
1006 
1007         // Begin -------- debug code
1008         String newRecipients = mConversation.getRecipients().serialize();
1009         if (!TextUtils.isEmpty(recipientsInUI) && !newRecipients.equals(recipientsInUI)) {
1010             if (LogTag.SEVERE_WARNING) {
1011                 LogTag.warnPossibleRecipientMismatch("send() after newRecipients changed from "
1012                         + recipientsInUI + " to " + newRecipients, mActivity);
1013                 dumpWorkingRecipients();
1014             } else {
1015                 Log.w(TAG, "send() after newRecipients changed from "
1016                         + recipientsInUI + " to " + newRecipients);
1017             }
1018         }
1019         // End -------- debug code
1020 
1021         // We need the recipient list for both SMS and MMS.
1022         final Conversation conv = mConversation;
1023         String msgTxt = mText.toString();
1024 
1025         if (requiresMms() || addressContainsEmailToMms(conv, msgTxt)) {
1026             // Make local copies of the bits we need for sending a message,
1027             // because we will be doing it off of the main thread, which will
1028             // immediately continue on to resetting some of this state.
1029             final Uri mmsUri = mMessageUri;
1030             final PduPersister persister = PduPersister.getPduPersister(mActivity);
1031 
1032             final SlideshowModel slideshow = mSlideshow;
1033             final SendReq sendReq = makeSendReq(conv, mSubject);
1034 
1035             // Do the dirty work of sending the message off of the main UI thread.
1036             new Thread(new Runnable() {
1037                 public void run() {
1038                     // Make sure the text in slide 0 is no longer holding onto a reference to
1039                     // the text in the message text box.
1040                     slideshow.prepareForSend();
1041                     sendMmsWorker(conv, mmsUri, persister, slideshow, sendReq);
1042                 }
1043             }).start();
1044         } else {
1045             // Same rules apply as above.
1046             final String msgText = mText.toString();
1047             new Thread(new Runnable() {
1048                 public void run() {
1049                     preSendSmsWorker(conv, msgText, recipientsInUI);
1050                 }
1051             }).start();
1052         }
1053 
1054         // update the Recipient cache with the new to address, if it's different
1055         RecipientIdCache.updateNumbers(conv.getThreadId(), conv.getRecipients());
1056 
1057         // Mark the message as discarded because it is "off the market" after being sent.
1058         mDiscarded = true;
1059     }
1060 
addressContainsEmailToMms(Conversation conv, String text)1061     private boolean addressContainsEmailToMms(Conversation conv, String text) {
1062         if (MmsConfig.getEmailGateway() != null) {
1063             String[] dests = conv.getRecipients().getNumbers();
1064             int length = dests.length;
1065             for (int i = 0; i < length; i++) {
1066                 if (Mms.isEmailAddress(dests[i]) || MessageUtils.isAlias(dests[i])) {
1067                     String mtext = dests[i] + " " + text;
1068                     int[] params = SmsMessage.calculateLength(mtext, false);
1069                     if (params[0] > 1) {
1070                         updateState(RECIPIENTS_REQUIRE_MMS, true, true);
1071                         ensureSlideshow();
1072                         syncTextToSlideshow();
1073                         return true;
1074                     }
1075                 }
1076             }
1077         }
1078         return false;
1079     }
1080 
1081     // Message sending stuff
1082 
preSendSmsWorker(Conversation conv, String msgText, String recipientsInUI)1083     private void preSendSmsWorker(Conversation conv, String msgText, String recipientsInUI) {
1084         // If user tries to send the message, it's a signal the inputted text is what they wanted.
1085         UserHappinessSignals.userAcceptedImeText(mActivity);
1086 
1087         mStatusListener.onPreMessageSent();
1088 
1089         long origThreadId = conv.getThreadId();
1090 
1091         // Make sure we are still using the correct thread ID for our recipient set.
1092         long threadId = conv.ensureThreadId();
1093 
1094         final String semiSepRecipients = conv.getRecipients().serialize();
1095 
1096         // recipientsInUI can be empty when the user types in a number and hits send
1097         if (LogTag.SEVERE_WARNING && ((origThreadId != 0 && origThreadId != threadId) ||
1098                (!semiSepRecipients.equals(recipientsInUI) && !TextUtils.isEmpty(recipientsInUI)))) {
1099             String msg = origThreadId != 0 && origThreadId != threadId ?
1100                     "WorkingMessage.preSendSmsWorker threadId changed or " +
1101                     "recipients changed. origThreadId: " +
1102                     origThreadId + " new threadId: " + threadId +
1103                     " also mConversation.getThreadId(): " +
1104                     mConversation.getThreadId()
1105                 :
1106                     "Recipients in window: \"" +
1107                     recipientsInUI + "\" differ from recipients from conv: \"" +
1108                     semiSepRecipients + "\"";
1109 
1110             LogTag.warnPossibleRecipientMismatch(msg, mActivity);
1111         }
1112 
1113         // just do a regular send. We're already on a non-ui thread so no need to fire
1114         // off another thread to do this work.
1115         sendSmsWorker(msgText, semiSepRecipients, threadId);
1116 
1117         // Be paranoid and clean any draft SMS up.
1118         deleteDraftSmsMessage(threadId);
1119     }
1120 
sendSmsWorker(String msgText, String semiSepRecipients, long threadId)1121     private void sendSmsWorker(String msgText, String semiSepRecipients, long threadId) {
1122         String[] dests = TextUtils.split(semiSepRecipients, ";");
1123         if (LogTag.VERBOSE || Log.isLoggable(LogTag.TRANSACTION, Log.VERBOSE)) {
1124             LogTag.debug("sendSmsWorker sending message: recipients=" + semiSepRecipients +
1125                     ", threadId=" + threadId);
1126         }
1127         MessageSender sender = new SmsMessageSender(mActivity, dests, msgText, threadId);
1128         try {
1129             sender.sendMessage(threadId);
1130 
1131             // Make sure this thread isn't over the limits in message count
1132             Recycler.getSmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
1133         } catch (Exception e) {
1134             Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e);
1135         }
1136 
1137         mStatusListener.onMessageSent();
1138     }
1139 
sendMmsWorker(Conversation conv, Uri mmsUri, PduPersister persister, SlideshowModel slideshow, SendReq sendReq)1140     private void sendMmsWorker(Conversation conv, Uri mmsUri, PduPersister persister,
1141                                SlideshowModel slideshow, SendReq sendReq) {
1142         // If user tries to send the message, it's a signal the inputted text is what they wanted.
1143         UserHappinessSignals.userAcceptedImeText(mActivity);
1144 
1145         // First make sure we don't have too many outstanding unsent message.
1146         Cursor cursor = null;
1147         try {
1148             cursor = SqliteWrapper.query(mActivity, mContentResolver,
1149                     Mms.Outbox.CONTENT_URI, MMS_OUTBOX_PROJECTION, null, null, null);
1150             if (cursor != null) {
1151                 long maxMessageSize = MmsConfig.getMaxSizeScaleForPendingMmsAllowed() *
1152                     MmsConfig.getMaxMessageSize();
1153                 long totalPendingSize = 0;
1154                 while (cursor.moveToNext()) {
1155                     totalPendingSize += cursor.getLong(MMS_MESSAGE_SIZE_INDEX);
1156                 }
1157                 if (totalPendingSize >= maxMessageSize) {
1158                     unDiscard();    // it wasn't successfully sent. Allow it to be saved as a draft.
1159                     mStatusListener.onMaxPendingMessagesReached();
1160                     return;
1161                 }
1162             }
1163         } finally {
1164             if (cursor != null) {
1165                 cursor.close();
1166             }
1167         }
1168         mStatusListener.onPreMessageSent();
1169 
1170         // Make sure we are still using the correct thread ID for our
1171         // recipient set.
1172         long threadId = conv.ensureThreadId();
1173 
1174         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1175             LogTag.debug("sendMmsWorker: update draft MMS message " + mmsUri);
1176         }
1177 
1178         if (mmsUri == null) {
1179             // Create a new MMS message if one hasn't been made yet.
1180             mmsUri = createDraftMmsMessage(persister, sendReq, slideshow);
1181         } else {
1182             // Otherwise, sync the MMS message in progress to disk.
1183             updateDraftMmsMessage(mmsUri, persister, slideshow, sendReq);
1184         }
1185 
1186         // Be paranoid and clean any draft SMS up.
1187         deleteDraftSmsMessage(threadId);
1188 
1189         // Resize all the resizeable attachments (e.g. pictures) to fit
1190         // in the remaining space in the slideshow.
1191         int error = 0;
1192         try {
1193             slideshow.finalResize(mmsUri);
1194         } catch (ExceedMessageSizeException e1) {
1195             error = MESSAGE_SIZE_EXCEEDED;
1196         } catch (MmsException e1) {
1197             error = UNKNOWN_ERROR;
1198         }
1199         if (error != 0) {
1200             markMmsMessageWithError(mmsUri);
1201             mStatusListener.onAttachmentError(error);
1202             return;
1203         }
1204 
1205         MessageSender sender = new MmsMessageSender(mActivity, mmsUri,
1206                 slideshow.getCurrentMessageSize());
1207         try {
1208             if (!sender.sendMessage(threadId)) {
1209                 // The message was sent through SMS protocol, we should
1210                 // delete the copy which was previously saved in MMS drafts.
1211                 SqliteWrapper.delete(mActivity, mContentResolver, mmsUri, null, null);
1212             }
1213 
1214             // Make sure this thread isn't over the limits in message count
1215             Recycler.getMmsRecycler().deleteOldMessagesByThreadId(mActivity, threadId);
1216         } catch (Exception e) {
1217             Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e);
1218         }
1219 
1220         mStatusListener.onMessageSent();
1221     }
1222 
markMmsMessageWithError(Uri mmsUri)1223     private void markMmsMessageWithError(Uri mmsUri) {
1224         try {
1225             PduPersister p = PduPersister.getPduPersister(mActivity);
1226             // Move the message into MMS Outbox. A trigger will create an entry in
1227             // the "pending_msgs" table.
1228             p.move(mmsUri, Mms.Outbox.CONTENT_URI);
1229 
1230             // Now update the pending_msgs table with an error for that new item.
1231             ContentValues values = new ContentValues(1);
1232             values.put(PendingMessages.ERROR_TYPE, MmsSms.ERR_TYPE_GENERIC_PERMANENT);
1233             long msgId = ContentUris.parseId(mmsUri);
1234             SqliteWrapper.update(mActivity, mContentResolver,
1235                     PendingMessages.CONTENT_URI,
1236                     values, PendingMessages._ID + "=" + msgId, null);
1237         } catch (MmsException e) {
1238             // Not much we can do here. If the p.move throws an exception, we'll just
1239             // leave the message in the draft box.
1240             Log.e(TAG, "Failed to move message to outbox and mark as error: " + mmsUri, e);
1241         }
1242     }
1243 
1244     // Draft message stuff
1245 
1246     private static final String[] MMS_DRAFT_PROJECTION = {
1247         Mms._ID,                // 0
1248         Mms.SUBJECT,            // 1
1249         Mms.SUBJECT_CHARSET     // 2
1250     };
1251 
1252     private static final int MMS_ID_INDEX         = 0;
1253     private static final int MMS_SUBJECT_INDEX    = 1;
1254     private static final int MMS_SUBJECT_CS_INDEX = 2;
1255 
readDraftMmsMessage(Context context, long threadId, StringBuilder sb)1256     private static Uri readDraftMmsMessage(Context context, long threadId, StringBuilder sb) {
1257         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1258             LogTag.debug("readDraftMmsMessage tid=%d", threadId);
1259         }
1260         Cursor cursor;
1261         ContentResolver cr = context.getContentResolver();
1262 
1263         final String selection = Mms.THREAD_ID + " = " + threadId;
1264         cursor = SqliteWrapper.query(context, cr,
1265                 Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION,
1266                 selection, null, null);
1267 
1268         Uri uri;
1269         try {
1270             if (cursor.moveToFirst()) {
1271                 uri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI,
1272                         cursor.getLong(MMS_ID_INDEX));
1273                 String subject = MessageUtils.extractEncStrFromCursor(cursor, MMS_SUBJECT_INDEX,
1274                         MMS_SUBJECT_CS_INDEX);
1275                 if (subject != null) {
1276                     sb.append(subject);
1277                 }
1278                 return uri;
1279             }
1280         } finally {
1281             cursor.close();
1282         }
1283 
1284         return null;
1285     }
1286 
1287     /**
1288      * makeSendReq should always return a non-null SendReq, whether the dest addresses are
1289      * valid or not.
1290      */
makeSendReq(Conversation conv, CharSequence subject)1291     private static SendReq makeSendReq(Conversation conv, CharSequence subject) {
1292         String[] dests = conv.getRecipients().getNumbers(true /* scrub for MMS address */);
1293 
1294         SendReq req = new SendReq();
1295         EncodedStringValue[] encodedNumbers = EncodedStringValue.encodeStrings(dests);
1296         if (encodedNumbers != null) {
1297             req.setTo(encodedNumbers);
1298         }
1299 
1300         if (!TextUtils.isEmpty(subject)) {
1301             req.setSubject(new EncodedStringValue(subject.toString()));
1302         }
1303 
1304         req.setDate(System.currentTimeMillis() / 1000L);
1305 
1306         return req;
1307     }
1308 
createDraftMmsMessage(PduPersister persister, SendReq sendReq, SlideshowModel slideshow)1309     private static Uri createDraftMmsMessage(PduPersister persister, SendReq sendReq,
1310             SlideshowModel slideshow) {
1311         try {
1312             PduBody pb = slideshow.toPduBody();
1313             sendReq.setBody(pb);
1314             Uri res = persister.persist(sendReq, Mms.Draft.CONTENT_URI);
1315             slideshow.sync(pb);
1316             return res;
1317         } catch (MmsException e) {
1318             return null;
1319         }
1320     }
1321 
asyncUpdateDraftMmsMessage(final Conversation conv)1322     private void asyncUpdateDraftMmsMessage(final Conversation conv) {
1323         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1324             LogTag.debug("asyncUpdateDraftMmsMessage conv=%s mMessageUri=%s", conv, mMessageUri);
1325         }
1326 
1327         final PduPersister persister = PduPersister.getPduPersister(mActivity);
1328         final SendReq sendReq = makeSendReq(conv, mSubject);
1329 
1330         new Thread(new Runnable() {
1331             public void run() {
1332                 conv.ensureThreadId();
1333                 conv.setDraftState(true);
1334                 if (mMessageUri == null) {
1335                     mMessageUri = createDraftMmsMessage(persister, sendReq, mSlideshow);
1336                 } else {
1337                     updateDraftMmsMessage(mMessageUri, persister, mSlideshow, sendReq);
1338                 }
1339 
1340                 // Be paranoid and delete any SMS drafts that might be lying around. Must do
1341                 // this after ensureThreadId so conv has the correct thread id.
1342                 asyncDeleteDraftSmsMessage(conv);
1343             }
1344         }).start();
1345     }
1346 
updateDraftMmsMessage(Uri uri, PduPersister persister, SlideshowModel slideshow, SendReq sendReq)1347     private static void updateDraftMmsMessage(Uri uri, PduPersister persister,
1348             SlideshowModel slideshow, SendReq sendReq) {
1349         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1350             LogTag.debug("updateDraftMmsMessage uri=%s", uri);
1351         }
1352         if (uri == null) {
1353             Log.e(TAG, "updateDraftMmsMessage null uri");
1354             return;
1355         }
1356         persister.updateHeaders(uri, sendReq);
1357         final PduBody pb = slideshow.toPduBody();
1358 
1359         try {
1360             persister.updateParts(uri, pb);
1361         } catch (MmsException e) {
1362             Log.e(TAG, "updateDraftMmsMessage: cannot update message " + uri);
1363         }
1364 
1365         slideshow.sync(pb);
1366     }
1367 
1368     private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT;
1369     private static final String[] SMS_BODY_PROJECTION = { Sms.BODY };
1370     private static final int SMS_BODY_INDEX = 0;
1371 
1372     /**
1373      * Reads a draft message for the given thread ID from the database,
1374      * if there is one, deletes it from the database, and returns it.
1375      * @return The draft message or an empty string.
1376      */
readDraftSmsMessage(Conversation conv)1377     private String readDraftSmsMessage(Conversation conv) {
1378         long thread_id = conv.getThreadId();
1379         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1380             LogTag.debug("readDraftSmsMessage tid=%d", thread_id);
1381         }
1382         // If it's an invalid thread or we know there's no draft, don't bother.
1383         if (thread_id <= 0 || !conv.hasDraft()) {
1384             return "";
1385         }
1386 
1387         Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id);
1388         String body = "";
1389 
1390         Cursor c = SqliteWrapper.query(mActivity, mContentResolver,
1391                         thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null);
1392         boolean haveDraft = false;
1393         if (c != null) {
1394             try {
1395                 if (c.moveToFirst()) {
1396                     body = c.getString(SMS_BODY_INDEX);
1397                     haveDraft = true;
1398                 }
1399             } finally {
1400                 c.close();
1401             }
1402         }
1403 
1404         // We found a draft, and if there are no messages in the conversation,
1405         // that means we deleted the thread, too. Must reset the thread id
1406         // so we'll eventually create a new thread.
1407         if (haveDraft && conv.getMessageCount() == 0) {
1408             // Clean out drafts for this thread -- if the recipient set changes,
1409             // we will lose track of the original draft and be unable to delete
1410             // it later.  The message will be re-saved if necessary upon exit of
1411             // the activity.
1412             clearConversation(conv);
1413         }
1414 
1415         return body;
1416     }
1417 
clearConversation(final Conversation conv)1418     private void clearConversation(final Conversation conv) {
1419         asyncDeleteDraftSmsMessage(conv);
1420 
1421         if (conv.getMessageCount() == 0) {
1422             if (DEBUG) LogTag.debug("clearConversation calling clearThreadId");
1423             conv.clearThreadId();
1424         }
1425 
1426         conv.setDraftState(false);
1427     }
1428 
asyncUpdateDraftSmsMessage(final Conversation conv, final String contents)1429     private void asyncUpdateDraftSmsMessage(final Conversation conv, final String contents) {
1430         new Thread(new Runnable() {
1431             public void run() {
1432                 long threadId = conv.ensureThreadId();
1433                 conv.setDraftState(true);
1434                 updateDraftSmsMessage(threadId, contents);
1435             }
1436         }).start();
1437     }
1438 
updateDraftSmsMessage(long thread_id, String contents)1439     private void updateDraftSmsMessage(long thread_id, String contents) {
1440         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1441             LogTag.debug("updateDraftSmsMessage tid=%d, contents=\"%s\"", thread_id, contents);
1442         }
1443 
1444         // If we don't have a valid thread, there's nothing to do.
1445         if (thread_id <= 0) {
1446             return;
1447         }
1448 
1449         ContentValues values = new ContentValues(3);
1450         values.put(Sms.THREAD_ID, thread_id);
1451         values.put(Sms.BODY, contents);
1452         values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT);
1453         SqliteWrapper.insert(mActivity, mContentResolver, Sms.CONTENT_URI, values);
1454         asyncDeleteDraftMmsMessage(thread_id);
1455     }
1456 
asyncDelete(final Uri uri, final String selection, final String[] selectionArgs)1457     private void asyncDelete(final Uri uri, final String selection, final String[] selectionArgs) {
1458         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1459             LogTag.debug("asyncDelete %s where %s", uri, selection);
1460         }
1461         new Thread(new Runnable() {
1462             public void run() {
1463                 SqliteWrapper.delete(mActivity, mContentResolver, uri, selection, selectionArgs);
1464             }
1465         }).start();
1466     }
1467 
asyncDeleteDraftSmsMessage(Conversation conv)1468     private void asyncDeleteDraftSmsMessage(Conversation conv) {
1469         long threadId = conv.getThreadId();
1470         if (threadId > 0) {
1471             asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
1472                 SMS_DRAFT_WHERE, null);
1473         }
1474     }
1475 
deleteDraftSmsMessage(long threadId)1476     private void deleteDraftSmsMessage(long threadId) {
1477         SqliteWrapper.delete(mActivity, mContentResolver,
1478                 ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
1479                 SMS_DRAFT_WHERE, null);
1480     }
1481 
asyncDeleteDraftMmsMessage(long threadId)1482     private void asyncDeleteDraftMmsMessage(long threadId) {
1483         final String where = Mms.THREAD_ID + " = " + threadId;
1484         asyncDelete(Mms.Draft.CONTENT_URI, where, null);
1485     }
1486 }
1487