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