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