blob: 3ee79f7a666cf72d16cf26a2f60fd14b023e225d [file] [log] [blame]
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001/**
2 * Copyright (c) 2011, Google Inc.
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
Andy Huang30e2c242012-01-06 18:14:30 -080017package com.android.mail.compose;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080018
Mindy Pereira326c6602012-01-04 15:32:42 -080019import android.app.ActionBar;
Andy Huang5c5fd572012-04-08 18:19:29 -070020import android.app.ActionBar.OnNavigationListener;
21import android.app.Activity;
Mindy Pereira82cc5662012-01-09 17:29:30 -080022import android.app.ActivityManager;
23import android.app.AlertDialog;
24import android.app.Dialog;
Mindy Pereirab199d172012-08-13 11:04:03 -070025import android.app.Fragment;
Mindy Pereirab199d172012-08-13 11:04:03 -070026import android.app.FragmentTransaction;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070027import android.app.LoaderManager;
Mindy Pereira6349a042012-01-04 11:25:01 -080028import android.content.ContentResolver;
Mindy Pereira82cc5662012-01-09 17:29:30 -080029import android.content.ContentValues;
Mindy Pereira6349a042012-01-04 11:25:01 -080030import android.content.Context;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070031import android.content.CursorLoader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080032import android.content.DialogInterface;
Mindy Pereira6349a042012-01-04 11:25:01 -080033import android.content.Intent;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -070034import android.content.Loader;
Mindy Pereira82cc5662012-01-09 17:29:30 -080035import android.content.pm.ActivityInfo;
Mindy Pereira7ed1c112012-01-18 10:59:25 -080036import android.database.Cursor;
Mindy Pereira6349a042012-01-04 11:25:01 -080037import android.net.Uri;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080038import android.os.Bundle;
Mindy Pereira82cc5662012-01-09 17:29:30 -080039import android.os.Handler;
40import android.os.HandlerThread;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -070041import android.os.ParcelFileDescriptor;
Alice Yang1ebc2db2013-03-14 21:21:44 -070042import android.os.Parcelable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080043import android.provider.BaseColumns;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080044import android.text.Editable;
Mindy Pereira82cc5662012-01-09 17:29:30 -080045import android.text.Html;
mindyped9c2f02012-10-12 10:02:08 -070046import android.text.SpannableString;
Mindy Pereira82cc5662012-01-09 17:29:30 -080047import android.text.Spanned;
Paul Westbrookc1827622012-01-06 11:27:12 -080048import android.text.TextUtils;
Mindy Pereira82cc5662012-01-09 17:29:30 -080049import android.text.TextWatcher;
Mindy Pereira46ce0b12012-01-05 10:32:15 -080050import android.text.util.Rfc822Token;
Mindy Pereirac17d0732011-12-29 10:46:19 -080051import android.text.util.Rfc822Tokenizer;
Mindy Pereira3cd4f402012-07-17 11:16:18 -070052import android.view.Gravity;
mindyp62d3ec72012-08-24 13:04:09 -070053import android.view.KeyEvent;
Mindy Pereira326c6602012-01-04 15:32:42 -080054import android.view.LayoutInflater;
Mindy Pereirab47f3e22011-12-13 14:25:04 -080055import android.view.Menu;
56import android.view.MenuInflater;
57import android.view.MenuItem;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080058import android.view.View;
59import android.view.View.OnClickListener;
Andy Huang5c5fd572012-04-08 18:19:29 -070060import android.view.ViewGroup;
Paul Westbrookb4931c62013-01-14 17:51:18 -080061import android.view.inputmethod.BaseInputConnection;
mindyp62d3ec72012-08-24 13:04:09 -070062import android.view.inputmethod.EditorInfo;
Mindy Pereira326c6602012-01-04 15:32:42 -080063import android.widget.ArrayAdapter;
Mindy Pereira8e9305e2011-12-13 14:25:04 -080064import android.widget.Button;
Mindy Pereira433b1982012-04-03 11:53:07 -070065import android.widget.EditText;
Mindy Pereira6349a042012-01-04 11:25:01 -080066import android.widget.TextView;
Mindy Pereira013194c2012-01-06 15:09:33 -080067import android.widget.Toast;
Mindy Pereira7b56a612011-12-14 12:32:28 -080068
Mindy Pereirac17d0732011-12-29 10:46:19 -080069import com.android.common.Rfc822Validator;
Andy Huang5c5fd572012-04-08 18:19:29 -070070import com.android.ex.chips.RecipientEditTextView;
Scott Kennedy5680ec22013-01-07 13:15:20 -080071import com.android.mail.MailIntentService;
Andy Huang5c5fd572012-04-08 18:19:29 -070072import com.android.mail.R;
Alice Yang1ebc2db2013-03-14 21:21:44 -070073import com.android.mail.browse.MessageHeaderView;
mindyp40882432012-09-06 11:07:40 -070074import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
Mindy Pereira9932dee2012-01-10 16:09:50 -080075import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
Mindy Pereira5a85e2b2012-01-11 09:53:32 -080076import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
Andy Huang30e2c242012-01-06 18:14:30 -080077import com.android.mail.compose.QuotedTextView.RespondInlineListener;
Mindy Pereira33fe9082012-01-09 16:24:30 -080078import com.android.mail.providers.Account;
Andy Huang30e2c242012-01-06 18:14:30 -080079import com.android.mail.providers.Address;
80import com.android.mail.providers.Attachment;
Scott Kennedy5680ec22013-01-07 13:15:20 -080081import com.android.mail.providers.Folder;
Mindy Pereira47d0e652012-07-23 09:45:07 -070082import com.android.mail.providers.MailAppProvider;
Mindy Pereira3ce64e72012-01-13 14:29:45 -080083import com.android.mail.providers.Message;
Mindy Pereira82cc5662012-01-09 17:29:30 -080084import com.android.mail.providers.MessageModification;
Mindy Pereira92551d02012-04-05 11:31:12 -070085import com.android.mail.providers.ReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -080086import com.android.mail.providers.Settings;
Andy Huang30e2c242012-01-06 18:14:30 -080087import com.android.mail.providers.UIProvider;
Mindy Pereira3ca5bad2012-04-16 11:02:42 -070088import com.android.mail.providers.UIProvider.AccountCapabilities;
Mindy Pereira12575862012-03-21 16:30:54 -070089import com.android.mail.providers.UIProvider.DraftType;
Alice Yang1ebc2db2013-03-14 21:21:44 -070090import com.android.mail.ui.AttachmentTile.AttachmentPreview;
Paul Westbrook83e6b572013-02-05 16:22:42 -080091import com.android.mail.ui.FeedbackEnabledActivity;
Mindy Pereirafa20c1a2012-07-23 13:00:02 -070092import com.android.mail.ui.MailActivity;
Mindy Pereirab199d172012-08-13 11:04:03 -070093import com.android.mail.ui.WaitFragment;
Paul Westbrook92227f62012-03-20 10:32:51 -070094import com.android.mail.utils.AccountUtils;
Mark Wei434f2942012-08-24 11:54:02 -070095import com.android.mail.utils.AttachmentUtils;
mindypfebd2262012-11-13 17:45:09 -080096import com.android.mail.utils.ContentProviderTask;
Paul Westbrookb334c902012-06-25 11:42:46 -070097import com.android.mail.utils.LogTag;
Andy Huang30e2c242012-01-06 18:14:30 -080098import com.android.mail.utils.LogUtils;
Andy Huang30e2c242012-01-06 18:14:30 -080099import com.android.mail.utils.Utils;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800100import com.google.common.annotations.VisibleForTesting;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800101import com.google.common.collect.Lists;
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800102import com.google.common.collect.Sets;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800103
Paul Westbrook3c7f94d2012-10-23 14:13:00 -0700104import java.io.FileNotFoundException;
105import java.io.IOException;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700106import java.io.UnsupportedEncodingException;
107import java.net.URLDecoder;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800108import java.util.ArrayList;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700109import java.util.Arrays;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800110import java.util.Collection;
Mindy Pereira75f66632012-01-11 11:42:02 -0800111import java.util.HashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800112import java.util.HashSet;
113import java.util.List;
Paul Westbrook1c078cf2012-03-20 16:18:51 -0700114import java.util.Map.Entry;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700115import java.util.Set;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800116import java.util.concurrent.ConcurrentHashMap;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800117
118public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
Mindy Pereira5a85e2b2012-01-11 09:53:32 -0800119 RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
Alice Yanga990a712013-03-13 18:37:00 -0700120 AttachmentAddedOrDeletedListener, OnAccountChangedListener,
121 LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
122 FeedbackEnabledActivity {
Mindy Pereira6349a042012-01-04 11:25:01 -0800123 // Identifiers for which type of composition this is
mindyp15f505b2013-01-09 09:33:13 -0800124 protected static final int COMPOSE = -1;
125 protected static final int REPLY = 0;
126 protected static final int REPLY_ALL = 1;
127 protected static final int FORWARD = 2;
128 protected static final int EDIT_DRAFT = 3;
Mindy Pereira6349a042012-01-04 11:25:01 -0800129
130 // Integer extra holding one of the above compose action
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700131 protected static final String EXTRA_ACTION = "action";
Mindy Pereira6349a042012-01-04 11:25:01 -0800132
Mindy Pereira326689d2012-05-17 10:14:14 -0700133 private static final String EXTRA_SHOW_CC = "showCc";
134 private static final String EXTRA_SHOW_BCC = "showBcc";
mindyp1623f9b2012-11-21 12:41:16 -0800135 private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
mindyp1d7e9142012-11-21 13:54:30 -0800136 private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700137
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700138 private static final String UTF8_ENCODING_NAME = "UTF-8";
139
140 private static final String MAIL_TO = "mailto";
141
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700142 private static final String EXTRA_SUBJECT = "subject";
143
144 private static final String EXTRA_BODY = "body";
145
mindypd27b6ea2012-10-05 09:43:49 -0700146 protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700147
Mark Wei62066e42012-09-13 12:07:02 -0700148 private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
149
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700150 // Extra that we can get passed from other activities
151 private static final String EXTRA_TO = "to";
152 private static final String EXTRA_CC = "cc";
153 private static final String EXTRA_BCC = "bcc";
154
155 // List of all the fields
156 static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC };
157
Mindy Pereira82cc5662012-01-09 17:29:30 -0800158 private static SendOrSaveCallback sTestSendOrSaveCallback = null;
159 // Map containing information about requests to create new messages, and the id of the
160 // messages that were the result of those requests.
161 //
162 // This map is used when the activity that initiated the save a of a new message, is killed
163 // before the save has completed (and when we know the id of the newly created message). When
164 // a save is completed, the service that is running in the background, will update the map
165 //
166 // When a new ComposeActivity instance is created, it will attempt to use the information in
167 // the previously instantiated map. If ComposeActivity.onCreate() is called, with a bundle
168 // (restoring data from a previous instance), and the map hasn't been created, we will attempt
169 // to populate the map with data stored in shared preferences.
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700170 // FIXME: values in this map are never read.
Mindy Pereira82cc5662012-01-09 17:29:30 -0800171 private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
Mindy Pereira6349a042012-01-04 11:25:01 -0800172 /**
173 * Notifies the {@code Activity} that the caller is an Email
174 * {@code Activity}, so that the back behavior may be modified accordingly.
175 *
176 * @see #onAppUpPressed
177 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700178 public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
Mindy Pereira6349a042012-01-04 11:25:01 -0800179
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700180 public static final String EXTRA_ATTACHMENTS = "attachments";
Paul Westbrookf97588b2012-03-20 11:11:37 -0700181
Scott Kennedy5680ec22013-01-07 13:15:20 -0800182 /** If set, we will clear notifications for this folder. */
183 public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
184
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800185 // If this is a reply/forward then this extra will hold the original message
Mindy Pereira36bbcae2012-04-25 09:27:04 -0700186 private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700187 // If this is a reply/forward then this extra will hold a uri we must query
188 // to get the original message.
189 protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
Mark Wei434f2942012-08-24 11:54:02 -0700190 // If this is an action to edit an existing draft message, this extra will hold the
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700191 // draft message
192 private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
Mindy Pereira4a27ea92012-01-05 15:55:25 -0800193 private static final String END_TOKEN = ", ";
Paul Westbrookb334c902012-06-25 11:42:46 -0700194 private static final String LOG_TAG = LogTag.getLogTag();
Mindy Pereira013194c2012-01-06 15:09:33 -0800195 // Request numbers for activities we start
196 private static final int RESULT_PICK_ATTACHMENT = 1;
197 private static final int RESULT_CREATE_ACCOUNT = 2;
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700198 // TODO(mindyp) set mime-type for auto send?
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700199 public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700200
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700201 private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
202 private static final String EXTRA_REQUEST_ID = "requestId";
203 private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
204 private static final String EXTRA_FOCUS_SELECTION_END = null;
205 private static final String EXTRA_MESSAGE = "extraMessage";
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700206 private static final int REFERENCE_MESSAGE_LOADER = 0;
Mindy Pereirab199d172012-08-13 11:04:03 -0700207 private static final int LOADER_ACCOUNT_CURSOR = 1;
Alice Yanga990a712013-03-13 18:37:00 -0700208 private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700209 private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
Mindy Pereirab199d172012-08-13 11:04:03 -0700210 private static final String TAG_WAIT = "wait-fragment";
Mindy Pereira2db7d4a2012-08-15 11:00:02 -0700211 private static final String MIME_TYPE_PHOTO = "image/*";
212 private static final String MIME_TYPE_VIDEO = "video/*";
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800213
Mindy Pereira82cc5662012-01-09 17:29:30 -0800214 /**
215 * A single thread for running tasks in the background.
216 */
217 private Handler mSendSaveTaskHandler = null;
Mindy Pereirac17d0732011-12-29 10:46:19 -0800218 private RecipientEditTextView mTo;
219 private RecipientEditTextView mCc;
220 private RecipientEditTextView mBcc;
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800221 private Button mCcBccButton;
222 private CcBccView mCcBccView;
Mindy Pereira7b56a612011-12-14 12:32:28 -0800223 private AttachmentsView mAttachmentsView;
Mindy Pereira33fe9082012-01-09 16:24:30 -0800224 private Account mAccount;
Mindy Pereira92551d02012-04-05 11:31:12 -0700225 private ReplyFromAccount mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -0800226 private Settings mCachedSettings;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800227 private Rfc822Validator mValidator;
Mindy Pereira6349a042012-01-04 11:25:01 -0800228 private TextView mSubject;
229
Mindy Pereira326c6602012-01-04 15:32:42 -0800230 private ComposeModeAdapter mComposeModeAdapter;
231 private int mComposeMode = -1;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800232 private boolean mForward;
233 private String mRecipient;
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800234 private QuotedTextView mQuotedTextView;
Mindy Pereira433b1982012-04-03 11:53:07 -0700235 private EditText mBodyView;
Mindy Pereira1a95a572012-01-05 12:21:29 -0800236 private View mFromStatic;
Mindy Pereira2eb17322012-03-07 10:07:34 -0800237 private TextView mFromStaticText;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800238 private View mFromSpinnerWrapper;
Mindy Pereira1883b342012-06-20 08:34:56 -0700239 @VisibleForTesting
240 protected FromAddressSpinner mFromSpinner;
Mindy Pereira013194c2012-01-06 15:09:33 -0800241 private boolean mAddingAttachment;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800242 private boolean mAttachmentsChanged;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800243 private boolean mTextChanged;
244 private boolean mReplyFromChanged;
245 private MenuItem mSave;
246 private MenuItem mSend;
Mindy Pereira82cc5662012-01-09 17:29:30 -0800247 private AlertDialog mRecipientErrorDialog;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800248 private AlertDialog mSendConfirmDialog;
Mindy Pereirab3112a22012-06-20 12:10:03 -0700249 @VisibleForTesting
250 protected Message mRefMessage;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800251 private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
252 private Message mDraft;
mindyp44a63392012-11-05 12:05:16 -0800253 private ReplyFromAccount mDraftAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -0800254 private Object mDraftLock = new Object();
mindyp93b079b2012-08-29 16:32:15 -0700255 private View mPhotoAttachmentsButton;
256 private View mVideoAttachmentsButton;
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800257
Mindy Pereira326c6602012-01-04 15:32:42 -0800258 /**
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700259 * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
260 */
261 private boolean mLaunchedFromEmail = false;
Mindy Pereiracbfb75a2012-06-25 14:52:23 -0700262 private RecipientTextWatcher mToListener;
263 private RecipientTextWatcher mCcListener;
264 private RecipientTextWatcher mBccListener;
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700265 private Uri mRefMessageUri;
Alice Yanga990a712013-03-13 18:37:00 -0700266 private boolean mShowQuotedText = false;
Mindy Pereirab199d172012-08-13 11:04:03 -0700267 private Bundle mSavedInstanceState;
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700268
269
mindyp1623f9b2012-11-21 12:41:16 -0800270 // Array of the outstanding send or save tasks. Access is synchronized
271 // with the object itself
272 /* package for testing */
273 @VisibleForTesting
274 public ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
275 // FIXME: this variable is never read. related to sRequestMessageIdMap.
276 private int mRequestId;
277 private String mSignature;
278 private Account[] mAccounts;
279 private boolean mRespondedInline;
280
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700281 /**
Mindy Pereira326c6602012-01-04 15:32:42 -0800282 * Can be called from a non-UI thread.
283 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800284 public static void editDraft(Context launcher, Account account, Message message) {
Alice Yang1ebc2db2013-03-14 21:21:44 -0700285 launch(launcher, account, message, EDIT_DRAFT, null, null);
Mindy Pereira326c6602012-01-04 15:32:42 -0800286 }
287
Mindy Pereira6349a042012-01-04 11:25:01 -0800288 /**
289 * Can be called from a non-UI thread.
290 */
Mindy Pereira33fe9082012-01-09 16:24:30 -0800291 public static void compose(Context launcher, Account account) {
Alice Yang1ebc2db2013-03-14 21:21:44 -0700292 launch(launcher, account, null, COMPOSE, null, null);
Mindy Pereira6349a042012-01-04 11:25:01 -0800293 }
294
295 /**
296 * Can be called from a non-UI thread.
297 */
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800298 public static Intent createReplyIntent(final Context launcher, final Account account,
299 final Uri messageUri, final boolean isReplyAll) {
300 return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
301 }
302
303 /**
304 * Can be called from a non-UI thread.
305 */
306 public static Intent createForwardIntent(final Context launcher, final Account account,
307 final Uri messageUri) {
308 return createActionIntent(launcher, account, messageUri, FORWARD);
309 }
310
311 private static Intent createActionIntent(final Context launcher, final Account account,
312 final Uri messageUri, final int action) {
313 final Intent intent = new Intent(launcher, ComposeActivity.class);
314
315 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
316 intent.putExtra(EXTRA_ACTION, action);
317 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
318 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
319
320 return intent;
321 }
322
323 /**
324 * Can be called from a non-UI thread.
325 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800326 public static void reply(Context launcher, Account account, Message message) {
Alice Yang1ebc2db2013-03-14 21:21:44 -0700327 launch(launcher, account, message, REPLY, null, null);
Mindy Pereira6349a042012-01-04 11:25:01 -0800328 }
329
330 /**
331 * Can be called from a non-UI thread.
332 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800333 public static void replyAll(Context launcher, Account account, Message message) {
Alice Yang1ebc2db2013-03-14 21:21:44 -0700334 launch(launcher, account, message, REPLY_ALL, null, null);
Mindy Pereira6349a042012-01-04 11:25:01 -0800335 }
336
337 /**
338 * Can be called from a non-UI thread.
339 */
Mindy Pereira3ce64e72012-01-13 14:29:45 -0800340 public static void forward(Context launcher, Account account, Message message) {
Alice Yang1ebc2db2013-03-14 21:21:44 -0700341 launch(launcher, account, message, FORWARD, null, null);
Mindy Pereira6349a042012-01-04 11:25:01 -0800342 }
343
Alice Yang1ebc2db2013-03-14 21:21:44 -0700344 public static void reportRenderingFeedback(Context launcher, Account account, Message message,
345 String body) {
346 launch(launcher, account, message, FORWARD, "android-gmail-readability@google.com", body);
347 }
348
349 private static void launch(Context launcher, Account account, Message message, int action,
350 String toAddress, String body) {
Mindy Pereira6349a042012-01-04 11:25:01 -0800351 Intent intent = new Intent(launcher, ComposeActivity.class);
352 intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
353 intent.putExtra(EXTRA_ACTION, action);
354 intent.putExtra(Utils.EXTRA_ACCOUNT, account);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700355 if (action == EDIT_DRAFT) {
356 intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
357 } else {
358 intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
359 }
Alice Yang1ebc2db2013-03-14 21:21:44 -0700360 if (toAddress != null) {
361 intent.putExtra(EXTRA_TO, toAddress);
362 }
363 if (body != null) {
364 intent.putExtra(EXTRA_BODY, body);
365 }
Mindy Pereira6349a042012-01-04 11:25:01 -0800366 launcher.startActivity(intent);
367 }
Mindy Pereira8e9305e2011-12-13 14:25:04 -0800368
369 @Override
370 public void onCreate(Bundle savedInstanceState) {
371 super.onCreate(savedInstanceState);
Mindy Pereira3528d362012-01-05 14:39:44 -0800372 setContentView(R.layout.compose);
Mindy Pereirab199d172012-08-13 11:04:03 -0700373 mSavedInstanceState = savedInstanceState;
374 checkValidAccounts();
375 }
376
377 private void finishCreate() {
378 Bundle savedInstanceState = mSavedInstanceState;
Mindy Pereira3528d362012-01-05 14:39:44 -0800379 findViews();
Mindy Pereira818143e2012-01-11 13:59:49 -0800380 Intent intent = getIntent();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700381 Message message;
Mark Wei62066e42012-09-13 12:07:02 -0700382 ArrayList<AttachmentPreview> previews;
Alice Yanga990a712013-03-13 18:37:00 -0700383 mShowQuotedText = false;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700384 int action;
Mindy Pereira47d0e652012-07-23 09:45:07 -0700385 // Check for any of the possibly supplied accounts.;
386 Account account = null;
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700387 if (hadSavedInstanceStateMessage(savedInstanceState)) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700388 action = savedInstanceState.getInt(EXTRA_ACTION, COMPOSE);
389 account = savedInstanceState.getParcelable(Utils.EXTRA_ACCOUNT);
390 message = (Message) savedInstanceState.getParcelable(EXTRA_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700391
392 previews = savedInstanceState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700393 mRefMessage = (Message) savedInstanceState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
394 } else {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700395 account = obtainAccount(intent);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700396 action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
397 // Initialize the message from the message in the intent
398 message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
Mark Wei62066e42012-09-13 12:07:02 -0700399 previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700400 mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
Mindy Pereirab18e5a92012-07-10 11:47:21 -0700401 mRefMessageUri = (Uri) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700402 }
Mark Wei62066e42012-09-13 12:07:02 -0700403 mAttachmentsView.setAttachmentPreviews(previews);
Paul Westbrook92227f62012-03-20 10:32:51 -0700404
405 setAccount(account);
Mindy Pereira818143e2012-01-11 13:59:49 -0800406 if (mAccount == null) {
407 return;
408 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700409
Scott Kennedy5680ec22013-01-07 13:15:20 -0800410 // Clear the notification and mark the conversation as seen, if necessary
411 final Folder notificationFolder =
412 intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
413 if (notificationFolder != null) {
414 final Intent clearNotifIntent =
415 new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
Scott Kennedyd5edd2d2012-12-05 11:11:32 -0800416 clearNotifIntent.setPackage(getPackageName());
Scott Kennedy48cfe462013-04-10 11:32:02 -0700417 clearNotifIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
418 clearNotifIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
Scott Kennedy5680ec22013-01-07 13:15:20 -0800419
420 startService(clearNotifIntent);
421 }
422
Paul Westbrookdaecb4b2012-05-31 10:21:26 -0700423 if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
424 mLaunchedFromEmail = true;
425 } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
426 final Uri dataUri = intent.getData();
427 if (dataUri != null) {
428 final String dataScheme = intent.getData().getScheme();
429 final String accountScheme = mAccount.composeIntentUri.getScheme();
430 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
431 }
432 }
433
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700434 if (mRefMessageUri != null) {
Alice Yanga990a712013-03-13 18:37:00 -0700435 mShowQuotedText = true;
436 mComposeMode = action;
437 getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700438 return;
439 } else if (message != null && action != EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700440 initFromDraftMessage(message);
441 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereiraa34c9a02012-04-17 14:10:53 -0700442 showCcBcc(savedInstanceState);
Alice Yanga990a712013-03-13 18:37:00 -0700443 mShowQuotedText = message.appendRefMessageContent;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700444 } else if (action == EDIT_DRAFT) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700445 initFromDraftMessage(message);
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800446 boolean showBcc = !TextUtils.isEmpty(message.getBcc());
447 boolean showCc = showBcc || !TextUtils.isEmpty(message.getCc());
Mindy Pereiraef388302012-06-18 19:07:44 -0700448 mCcBccView.show(false, showCc, showBcc);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700449 // Update the action to the draft type of the previous draft
450 switch (message.draftType) {
451 case UIProvider.DraftType.REPLY:
452 action = REPLY;
453 break;
454 case UIProvider.DraftType.REPLY_ALL:
455 action = REPLY_ALL;
456 break;
457 case UIProvider.DraftType.FORWARD:
458 action = FORWARD;
459 break;
460 case UIProvider.DraftType.COMPOSE:
461 default:
462 action = COMPOSE;
463 break;
464 }
Alice Yanga990a712013-03-13 18:37:00 -0700465 LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
466
467 mShowQuotedText = message.appendRefMessageContent;
468 if (message.refMessageUri != null) {
469 // If we're editing an existing draft that was in reference to an existing message,
470 // still need to load that original message since we might need to refer to the
471 // original sender and recipients if user switches "reply <-> reply-all".
472 mRefMessageUri = message.refMessageUri;
473 mComposeMode = action;
474 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
475 return;
476 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700477 } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
478 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -0800479 initFromRefMessage(action);
Alice Yanga990a712013-03-13 18:37:00 -0700480 mShowQuotedText = true;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700481 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -0700482 } else {
483 initFromExtras(intent);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700484 }
Alice Yanga990a712013-03-13 18:37:00 -0700485
486 mComposeMode = action;
487 finishSetup(action, intent, savedInstanceState);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -0700488 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700489
Mindy Pereirab199d172012-08-13 11:04:03 -0700490 private void checkValidAccounts() {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700491 final Account[] allAccounts = AccountUtils.getAccounts(this);
492 if (allAccounts == null || allAccounts.length == 0) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700493 final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
494 if (noAccountIntent != null) {
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700495 mAccounts = null;
Mindy Pereirab199d172012-08-13 11:04:03 -0700496 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
497 }
498 } else {
mindyp26d4d2d2012-09-18 17:30:32 -0700499 // If none of the accounts are syncing, setup a watcher.
Mindy Pereirab199d172012-08-13 11:04:03 -0700500 boolean anySyncing = false;
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700501 for (Account a : allAccounts) {
Paul Westbrookdfa1dec2012-09-26 16:27:28 -0700502 if (a.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700503 anySyncing = true;
504 break;
505 }
506 }
507 if (!anySyncing) {
508 // There are accounts, but none are sync'd, which is just like having no accounts.
509 mAccounts = null;
510 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
511 return;
512 }
Paul Westbrookfaa742f2012-11-01 09:50:16 -0700513 mAccounts = AccountUtils.getSyncingAccounts(this);
Mindy Pereirab199d172012-08-13 11:04:03 -0700514 finishCreate();
515 }
516 }
517
Mindy Pereira47d0e652012-07-23 09:45:07 -0700518 private Account obtainAccount(Intent intent) {
519 Account account = null;
520 Object accountExtra = null;
521 if (intent != null && intent.getExtras() != null) {
522 accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
523 if (accountExtra instanceof Account) {
524 return (Account) accountExtra;
mindyp7ae042e2012-08-27 13:27:37 -0700525 } else if (accountExtra instanceof String) {
526 // This is the Account attached to the widget compose intent.
527 account = Account.newinstance((String)accountExtra);
528 if (account != null) {
529 return account;
530 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700531 }
mindyp5ee9dc42013-01-08 09:54:54 -0800532 accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
533 intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
534 intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
Mindy Pereira47d0e652012-07-23 09:45:07 -0700535 }
536 if (account == null) {
mindyp06174462012-10-12 09:11:27 -0700537 MailAppProvider provider = MailAppProvider.getInstance();
538 String lastAccountUri = provider.getLastSentFromAccount();
539 if (TextUtils.isEmpty(lastAccountUri)) {
540 lastAccountUri = provider.getLastViewedAccount();
541 }
Mindy Pereira47d0e652012-07-23 09:45:07 -0700542 if (!TextUtils.isEmpty(lastAccountUri)) {
543 accountExtra = Uri.parse(lastAccountUri);
544 }
545 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700546 if (mAccounts != null && mAccounts.length > 0) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700547 if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
548 // For backwards compatibility, we need to check account
549 // names.
Mindy Pereirab199d172012-08-13 11:04:03 -0700550 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700551 if (a.name.equals(accountExtra)) {
552 account = a;
553 }
554 }
555 } else if (accountExtra instanceof Uri) {
556 // The uri of the last viewed account is what is stored in
557 // the current code base.
Mindy Pereirab199d172012-08-13 11:04:03 -0700558 for (Account a : mAccounts) {
Mindy Pereira47d0e652012-07-23 09:45:07 -0700559 if (a.uri.equals(accountExtra)) {
560 account = a;
561 }
562 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700563 }
564 if (account == null) {
565 account = mAccounts[0];
Mindy Pereira47d0e652012-07-23 09:45:07 -0700566 }
567 }
568 return account;
569 }
570
Alice Yanga990a712013-03-13 18:37:00 -0700571 private void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
mindyp34a3c562012-11-06 15:12:15 -0800572 setFocus(action);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -0700573 if (action == COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800574 mQuotedTextView.setVisibility(View.GONE);
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800575 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800576 initRecipients();
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700577 // Don't bother with the intent if we have procured a message from the
578 // intent already.
579 if (!hadSavedInstanceStateMessage(savedInstanceState)) {
580 initAttachmentsFromIntent(intent);
581 }
Alice Yanga990a712013-03-13 18:37:00 -0700582 initActionBar();
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700583 initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
584 action);
mindypd4a48662012-11-08 17:13:49 -0800585
586 // If this is a draft message, the draft account is whatever account was
587 // used to open the draft message in Compose.
588 if (mDraft != null) {
589 mDraftAccount = mReplyFromAccount;
590 }
591
Mindy Pereira75f66632012-01-11 11:42:02 -0800592 initChangeListeners();
Mindy Pereira326689d2012-05-17 10:14:14 -0700593 updateHideOrShowCcBcc();
Alice Yanga990a712013-03-13 18:37:00 -0700594 updateHideOrShowQuotedText(mShowQuotedText);
mindyp1623f9b2012-11-21 12:41:16 -0800595
596 mRespondedInline = mSavedInstanceState != null ?
597 mSavedInstanceState.getBoolean(EXTRA_RESPONDED_INLINE) : false;
598 if (mRespondedInline) {
599 mQuotedTextView.setVisibility(View.GONE);
600 }
Mindy Pereira71c9e562012-05-17 11:01:02 -0700601 }
602
Scott Kennedyff8553f2013-04-05 20:57:44 -0700603 private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
Mindy Pereiraf7fc6c32012-06-19 15:18:33 -0700604 return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
605 }
606
Mindy Pereira71c9e562012-05-17 11:01:02 -0700607 private void updateHideOrShowQuotedText(boolean showQuotedText) {
608 mQuotedTextView.updateCheckedState(showQuotedText);
mindyp40882432012-09-06 11:07:40 -0700609 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereira433b1982012-04-03 11:53:07 -0700610 }
611
612 private void setFocus(int action) {
613 if (action == EDIT_DRAFT) {
614 int type = mDraft.draftType;
615 switch (type) {
616 case UIProvider.DraftType.COMPOSE:
617 case UIProvider.DraftType.FORWARD:
618 action = COMPOSE;
619 break;
620 case UIProvider.DraftType.REPLY:
621 case UIProvider.DraftType.REPLY_ALL:
622 default:
623 action = REPLY;
624 break;
625 }
626 }
627 switch (action) {
628 case FORWARD:
629 case COMPOSE:
mindyp27083062012-11-15 09:02:01 -0800630 if (TextUtils.isEmpty(mTo.getText())) {
631 mTo.requestFocus();
632 break;
633 }
Scott Kennedyff8553f2013-04-05 20:57:44 -0700634 //$FALL-THROUGH$
Mindy Pereira433b1982012-04-03 11:53:07 -0700635 case REPLY:
636 case REPLY_ALL:
637 default:
638 focusBody();
639 break;
640 }
641 }
642
643 /**
644 * Focus the body of the message.
645 */
646 public void focusBody() {
647 mBodyView.requestFocus();
648 int length = mBodyView.getText().length();
649
650 int signatureStartPos = getSignatureStartPosition(
651 mSignature, mBodyView.getText().toString());
652 if (signatureStartPos > -1) {
653 // In case the user deleted the newlines...
654 mBodyView.setSelection(signatureStartPos);
mindyp8743cfc2012-09-18 13:29:08 -0700655 } else if (length >= 0) {
Mindy Pereira433b1982012-04-03 11:53:07 -0700656 // Move cursor to the end.
657 mBodyView.setSelection(length);
658 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800659 }
660
661 @Override
662 protected void onResume() {
663 super.onResume();
664 // Update the from spinner as other accounts
665 // may now be available.
Mindy Pereira818143e2012-01-11 13:59:49 -0800666 if (mFromSpinner != null && mAccount != null) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700667 mFromSpinner.asyncInitFromSpinner(mComposeMode, mAccount, mAccounts);
Mindy Pereira818143e2012-01-11 13:59:49 -0800668 }
Mindy Pereira1a95a572012-01-05 12:21:29 -0800669 }
670
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800671 @Override
672 protected void onPause() {
673 super.onPause();
674
675 if (mSendConfirmDialog != null) {
676 mSendConfirmDialog.dismiss();
677 }
678 if (mRecipientErrorDialog != null) {
679 mRecipientErrorDialog.dismiss();
680 }
Mindy Pereiraa2148332012-07-02 13:54:14 -0700681 // When the user exits the compose view, see if this draft needs saving.
Yorke Lee3d7048e2012-09-19 14:19:25 -0700682 // Don't save unnecessary drafts if we are only changing the orientation.
683 if (!isChangingConfigurations()) {
Mindy Pereiraa2148332012-07-02 13:54:14 -0700684 saveIfNeeded();
685 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800686 }
687
688 @Override
689 protected final void onActivityResult(int request, int result, Intent data) {
Mindy Pereirab199d172012-08-13 11:04:03 -0700690 if (request == RESULT_PICK_ATTACHMENT && result == RESULT_OK) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800691 addAttachmentAndUpdateView(data);
Mindy Pereirab199d172012-08-13 11:04:03 -0700692 mAddingAttachment = false;
693 } else if (request == RESULT_CREATE_ACCOUNT) {
Alice Yanga990a712013-03-13 18:37:00 -0700694 // We were waiting for the user to create an account
Mindy Pereirab199d172012-08-13 11:04:03 -0700695 if (result != RESULT_OK) {
696 finish();
697 } else {
698 // Watch for accounts to show up!
699 // restart the loader to get the updated list of accounts
700 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
701 showWaitFragment(null);
702 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800703 }
704 }
705
706 @Override
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700707 public final void onRestoreInstanceState(Bundle savedInstanceState) {
Yorke Lee7bec2b92013-04-26 08:31:42 -0700708 final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
709 if (hasAccounts) {
710 clearChangeListeners();
711 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700712 super.onRestoreInstanceState(savedInstanceState);
713 if (savedInstanceState != null) {
714 if (savedInstanceState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
715 int selectionStart = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_START);
716 int selectionEnd = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_END);
717 // There should be a focus and it should be an EditText since we
718 // only save these extras if these conditions are true.
719 EditText focusEditText = (EditText) getCurrentFocus();
720 final int length = focusEditText.getText().length();
721 if (selectionStart < length && selectionEnd < length) {
722 focusEditText.setSelection(selectionStart, selectionEnd);
723 }
724 }
725 }
Yorke Lee7bec2b92013-04-26 08:31:42 -0700726 if (hasAccounts) {
727 initChangeListeners();
728 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700729 }
730
731 @Override
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800732 public final void onSaveInstanceState(Bundle state) {
733 super.onSaveInstanceState(state);
Mindy Pereirab199d172012-08-13 11:04:03 -0700734 // We have no accounts so there is nothing to compose, and therefore, nothing to save.
735 if (mAccounts == null || mAccounts.length == 0) {
736 return;
737 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700738 // The framework is happy to save and restore the selection but only if it also saves and
739 // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
740 // this manually.
741 View focus = getCurrentFocus();
742 if (focus != null && focus instanceof EditText) {
743 EditText focusEditText = (EditText) focus;
744 state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
745 state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
746 }
Paul Westbrook6273e962012-04-23 10:44:15 -0700747
748 final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
Paul Westbrook151f1ad2012-04-24 09:13:00 -0700749 final int selectedPos = mFromSpinner.getSelectedItemPosition();
Mindy Pereirad90f7ac2012-06-27 10:31:06 -0700750 final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
751 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
752 replyFromAccounts.get(selectedPos) : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700753 if (selectedReplyFromAccount != null) {
754 state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
755 .toString());
756 state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
757 } else {
758 state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
759 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -0800760
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700761 if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
762 // We don't have a draft id, and we have a request id,
763 // save the request id.
764 state.putInt(EXTRA_REQUEST_ID, mRequestId);
765 }
766
767 // We want to restore the current mode after a pause
768 // or rotation.
769 int mode = getMode();
770 state.putInt(EXTRA_ACTION, mode);
771
mindype7b76aa2012-11-14 16:19:13 -0800772 final Message message = createMessage(selectedReplyFromAccount, mode);
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700773 if (mDraft != null) {
mindype7b76aa2012-11-14 16:19:13 -0800774 message.id = mDraft.id;
775 message.serverId = mDraft.serverId;
776 message.uri = mDraft.uri;
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700777 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700778 state.putParcelable(EXTRA_MESSAGE, message);
779
780 if (mRefMessage != null) {
781 state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
782 }
Mindy Pereira326689d2012-05-17 10:14:14 -0700783 state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
784 state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
mindyp1623f9b2012-11-21 12:41:16 -0800785 state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
mindyp816b3f02012-12-11 08:25:04 -0800786 state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
Mark Wei62066e42012-09-13 12:07:02 -0700787 state.putParcelableArrayList(
788 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700789 }
790
791 private int getMode() {
792 int mode = ComposeActivity.COMPOSE;
793 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -0700794 if (actionBar != null
795 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700796 mode = actionBar.getSelectedNavigationIndex();
797 }
798 return mode;
799 }
800
801 private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
802 Message message = new Message();
803 message.id = UIProvider.INVALID_MESSAGE_ID;
Andy Huangd47877e2012-08-09 19:31:24 -0700804 message.serverId = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700805 message.uri = null;
806 message.conversationUri = null;
807 message.subject = mSubject.getText().toString();
808 message.snippet = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800809 message.setTo(formatSenders(mTo.getText().toString()));
810 message.setCc(formatSenders(mCc.getText().toString()));
811 message.setBcc(formatSenders(mBcc.getText().toString()));
812 message.setReplyTo(null);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700813 message.dateReceivedMs = 0;
Paul Westbrookb4931c62013-01-14 17:51:18 -0800814 final String htmlBody = Html.toHtml(removeComposingSpans(mBodyView.getText()));
815 final StringBuilder fullBody = new StringBuilder(htmlBody);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700816 message.bodyHtml = fullBody.toString();
817 message.bodyText = mBodyView.getText().toString();
818 message.embedsExternalResources = false;
Alice Yanga990a712013-03-13 18:37:00 -0700819 message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700820 message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
821 ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
822 message.hasAttachments = attachments != null && attachments.size() > 0;
823 message.attachmentListUri = null;
824 message.messageFlags = 0;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700825 message.alwaysShowImages = false;
826 message.attachmentsJson = Attachment.toJSONArray(attachments);
827 CharSequence quotedText = mQuotedTextView.getQuotedText();
828 message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
829 .getQuotedTextOffset(quotedText.toString()) : -1;
830 message.accountUri = null;
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800831 message.setFrom(selectedReplyFromAccount != null ? selectedReplyFromAccount.address
832 : mAccount != null ? mAccount.name : null);
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700833 message.draftType = getDraftType(mode);
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800834 message.setTo(formatSenders(mTo.getText().toString()));
835 message.setCc(formatSenders(mCc.getText().toString()));
836 message.setBcc(formatSenders(mBcc.getText().toString()));
mindype7b76aa2012-11-14 16:19:13 -0800837 return message;
Andy Huang1f8f4dd2012-10-25 21:35:35 -0700838 }
839
Scott Kennedyff8553f2013-04-05 20:57:44 -0700840 private static String formatSenders(final String string) {
Mindy Pereira3c911582012-08-09 16:59:09 -0700841 if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
842 return string.substring(0, string.length() - 1);
843 }
844 return string;
845 }
846
Mindy Pereira818143e2012-01-11 13:59:49 -0800847 @VisibleForTesting
848 void setAccount(Account account) {
Mindy Pereirabb5217e2012-04-17 11:08:29 -0700849 if (account == null) {
850 return;
851 }
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700852 if (!account.equals(mAccount)) {
853 mAccount = account;
Paul Westbrookb1f573c2012-04-06 11:38:28 -0700854 mCachedSettings = mAccount.settings;
855 appendSignature();
Mindy Pereira23e9fde2012-03-20 15:08:24 -0700856 }
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700857 if (mAccount != null) {
Vikram Aggarwalf6c00b82013-01-03 10:02:50 -0800858 MailActivity.setNfcMessage(mAccount.name);
Mindy Pereirafa20c1a2012-07-23 13:00:02 -0700859 }
Mindy Pereira818143e2012-01-11 13:59:49 -0800860 }
861
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700862 private void initFromSpinner(Bundle bundle, int action) {
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700863 String accountString = null;
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700864 if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700865 action = COMPOSE;
866 }
Mindy Pereirab199d172012-08-13 11:04:03 -0700867 mFromSpinner.asyncInitFromSpinner(action, mAccount, mAccounts);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700868 if (bundle != null) {
869 if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
870 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
871 bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
872 } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
873 accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
874 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
875 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700876 }
877 if (mReplyFromAccount == null) {
878 if (mDraft != null) {
879 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
880 } else if (mRefMessage != null) {
881 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
882 }
Mindy Pereira62de1b12012-04-06 12:17:56 -0700883 }
884 if (mReplyFromAccount == null) {
Andy Huang238aa472012-10-30 17:45:17 -0700885 mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700886 }
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700887
Mindy Pereira62de1b12012-04-06 12:17:56 -0700888 mFromSpinner.setCurrentAccount(mReplyFromAccount);
Mindy Pereira9a42bb42012-04-18 15:21:33 -0700889
Mindy Pereira62de1b12012-04-06 12:17:56 -0700890 if (mFromSpinner.getCount() > 1) {
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700891 // If there is only 1 account, just show that account.
892 // Otherwise, give the user the ability to choose which account to
Mindy Pereira62de1b12012-04-06 12:17:56 -0700893 // send mail from / save drafts to.
894 mFromStatic.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700895 mFromStaticText.setText(mAccount.name);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700896 mFromSpinnerWrapper.setVisibility(View.VISIBLE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700897 } else {
898 mFromStatic.setVisibility(View.VISIBLE);
899 mFromStaticText.setText(mAccount.name);
900 mFromSpinnerWrapper.setVisibility(View.GONE);
Mindy Pereiraa83e7082012-03-30 08:53:11 -0700901 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -0800902 }
903
Mindy Pereira62de1b12012-04-06 12:17:56 -0700904 private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
905 if (refMessage.accountUri != null) {
906 // This must be from combined inbox.
907 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
908 for (ReplyFromAccount from : replyFromAccounts) {
909 if (from.account.uri.equals(refMessage.accountUri)) {
910 return from;
911 }
912 }
913 return null;
914 } else {
915 return getReplyFromAccount(account, refMessage);
916 }
917 }
918
919 /**
920 * Given an account and which email address the message was sent to,
921 * return who the message should be sent from.
922 * @param account Account in which the message arrived.
923 * @param sentTo Email address to which the message was sent.
924 * @return the address from which to reply.
925 */
926 public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
927 // First see if we are supposed to use the default address or
928 // the address it was sentTo.
Mindy Pereira326689d2012-05-17 10:14:14 -0700929 if (mCachedSettings.forceReplyFromDefault) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700930 return getDefaultReplyFromAccount(account);
931 } else {
Mindy Pereira89bae572012-06-18 11:34:36 -0700932 // If we aren't explicitly told which account to look for, look at
Mindy Pereira62de1b12012-04-06 12:17:56 -0700933 // all the message recipients and find one that matches
934 // a custom from or account.
935 List<String> allRecipients = new ArrayList<String>();
mindyp5ed63112012-09-17 17:31:45 -0700936 allRecipients.addAll(Arrays.asList(refMessage.getToAddresses()));
937 allRecipients.addAll(Arrays.asList(refMessage.getCcAddresses()));
Mindy Pereira62de1b12012-04-06 12:17:56 -0700938 return getMatchingRecipient(account, allRecipients);
939 }
940 }
941
942 /**
943 * Compare all the recipients of an email to the current account and all
944 * custom addresses associated with that account. Return the match if there
945 * is one, or the default account if there isn't.
946 */
947 protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
948 // Tokenize the list and place in a hashmap.
949 ReplyFromAccount matchingReplyFrom = null;
950 Rfc822Token[] tokens;
951 HashSet<String> recipientsMap = new HashSet<String>();
952 for (String address : sentTo) {
953 tokens = Rfc822Tokenizer.tokenize(address);
954 for (int i = 0; i < tokens.length; i++) {
955 recipientsMap.add(tokens[i].getAddress());
956 }
957 }
958
959 int matchingAddressCount = 0;
960 List<ReplyFromAccount> customFroms;
Andy Huang16174812012-08-16 16:40:35 -0700961 customFroms = account.getReplyFroms();
962 if (customFroms != null) {
963 for (ReplyFromAccount entry : customFroms) {
964 if (recipientsMap.contains(entry.address)) {
965 matchingReplyFrom = entry;
966 matchingAddressCount++;
Mindy Pereira62de1b12012-04-06 12:17:56 -0700967 }
968 }
Mindy Pereira62de1b12012-04-06 12:17:56 -0700969 }
970 if (matchingAddressCount > 1) {
971 matchingReplyFrom = getDefaultReplyFromAccount(account);
972 }
973 return matchingReplyFrom;
974 }
975
Scott Kennedyff8553f2013-04-05 20:57:44 -0700976 private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
977 for (final ReplyFromAccount from : account.getReplyFroms()) {
Mindy Pereira62de1b12012-04-06 12:17:56 -0700978 if (from.isDefault) {
979 return from;
980 }
981 }
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700982 return new ReplyFromAccount(account, account.uri, account.name, account.name, account.name,
983 true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700984 }
985
Mindy Pereirae8f94dc2012-04-16 11:56:21 -0700986 private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) {
Scott Kennedy8960f0a2012-11-07 15:35:50 -0800987 String sender = msg.getFrom();
Mindy Pereira62de1b12012-04-06 12:17:56 -0700988 ReplyFromAccount replyFromAccount = null;
989 List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
990 if (TextUtils.equals(account.name, sender)) {
991 replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
Mindy Pereiracd970dd2012-05-31 10:07:47 -0700992 mAccount.name, mAccount.name, true, false);
Mindy Pereira62de1b12012-04-06 12:17:56 -0700993 } else {
994 for (ReplyFromAccount fromAccount : replyFromAccounts) {
995 if (TextUtils.equals(fromAccount.name, sender)) {
996 replyFromAccount = fromAccount;
997 break;
998 }
999 }
1000 }
1001 return replyFromAccount;
1002 }
1003
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001004 private void findViews() {
Mindy Pereirab199d172012-08-13 11:04:03 -07001005 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001006 mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001007 if (mCcBccButton != null) {
1008 mCcBccButton.setOnClickListener(this);
1009 }
1010 mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001011 mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
mindyp93b079b2012-08-29 16:32:15 -07001012 mPhotoAttachmentsButton = findViewById(R.id.add_photo_attachment);
mindypcd0b0b92012-08-23 14:33:17 -07001013 if (mPhotoAttachmentsButton != null) {
1014 mPhotoAttachmentsButton.setOnClickListener(this);
1015 }
mindyp93b079b2012-08-29 16:32:15 -07001016 mVideoAttachmentsButton = findViewById(R.id.add_video_attachment);
mindypcd0b0b92012-08-23 14:33:17 -07001017 if (mVideoAttachmentsButton != null) {
1018 mVideoAttachmentsButton.setOnClickListener(this);
1019 }
Mindy Pereira818143e2012-01-11 13:59:49 -08001020 mTo = (RecipientEditTextView) findViewById(R.id.to);
Scott Kennedy41500392013-04-24 18:46:36 -07001021 mTo.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira818143e2012-01-11 13:59:49 -08001022 mCc = (RecipientEditTextView) findViewById(R.id.cc);
Scott Kennedy41500392013-04-24 18:46:36 -07001023 mCc.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira818143e2012-01-11 13:59:49 -08001024 mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
Scott Kennedy41500392013-04-24 18:46:36 -07001025 mBcc.setTokenizer(new Rfc822Tokenizer());
Mindy Pereira82cc5662012-01-09 17:29:30 -08001026 // TODO: add special chips text change watchers before adding
1027 // this as a text changed watcher to the to, cc, bcc fields.
Mindy Pereira6349a042012-01-04 11:25:01 -08001028 mSubject = (TextView) findViewById(R.id.subject);
mindyp62d3ec72012-08-24 13:04:09 -07001029 mSubject.setOnEditorActionListener(this);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001030 mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1031 mQuotedTextView.setRespondInlineListener(this);
Mindy Pereira433b1982012-04-03 11:53:07 -07001032 mBodyView = (EditText) findViewById(R.id.body);
Mindy Pereira1a95a572012-01-05 12:21:29 -08001033 mFromStatic = findViewById(R.id.static_from_content);
Mindy Pereira2eb17322012-03-07 10:07:34 -08001034 mFromStaticText = (TextView) findViewById(R.id.from_account_name);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001035 mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08001036 mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
Mindy Pereira6349a042012-01-04 11:25:01 -08001037 }
1038
mindyp62d3ec72012-08-24 13:04:09 -07001039 @Override
1040 public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1041 if (action == EditorInfo.IME_ACTION_DONE) {
1042 focusBody();
1043 return true;
1044 }
1045 return false;
1046 }
1047
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001048 protected TextView getBody() {
1049 return mBodyView;
1050 }
1051
1052 @VisibleForTesting
1053 public Account getFromAccount() {
1054 return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1055 mReplyFromAccount.account : mAccount;
1056 }
1057
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001058 private void clearChangeListeners() {
1059 mSubject.removeTextChangedListener(this);
1060 mBodyView.removeTextChangedListener(this);
1061 mTo.removeTextChangedListener(mToListener);
1062 mCc.removeTextChangedListener(mCcListener);
1063 mBcc.removeTextChangedListener(mBccListener);
1064 mFromSpinner.setOnAccountChangedListener(null);
1065 mAttachmentsView.setAttachmentChangesListener(null);
1066 }
1067
Mindy Pereira75f66632012-01-11 11:42:02 -08001068 // Now that the message has been initialized from any existing draft or
1069 // ref message data, set up listeners for any changes that occur to the
1070 // message.
1071 private void initChangeListeners() {
mindyp1d7e9142012-11-21 13:54:30 -08001072 // Make sure we only add text changed listeners once!
1073 clearChangeListeners();
Mindy Pereira75f66632012-01-11 11:42:02 -08001074 mSubject.addTextChangedListener(this);
1075 mBodyView.addTextChangedListener(this);
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07001076 if (mToListener == null) {
1077 mToListener = new RecipientTextWatcher(mTo, this);
1078 }
1079 mTo.addTextChangedListener(mToListener);
1080 if (mCcListener == null) {
1081 mCcListener = new RecipientTextWatcher(mCc, this);
1082 }
1083 mCc.addTextChangedListener(mCcListener);
1084 if (mBccListener == null) {
1085 mBccListener = new RecipientTextWatcher(mBcc, this);
1086 }
1087 mBcc.addTextChangedListener(mBccListener);
Mindy Pereira75f66632012-01-11 11:42:02 -08001088 mFromSpinner.setOnAccountChangedListener(this);
Mindy Pereira818143e2012-01-11 13:59:49 -08001089 mAttachmentsView.setAttachmentChangesListener(this);
Mindy Pereira75f66632012-01-11 11:42:02 -08001090 }
1091
Alice Yanga990a712013-03-13 18:37:00 -07001092 private void initActionBar() {
1093 LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001094 ActionBar actionBar = getActionBar();
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001095 if (actionBar == null) {
1096 return;
1097 }
Alice Yanga990a712013-03-13 18:37:00 -07001098 if (mComposeMode == ComposeActivity.COMPOSE) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001099 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
1100 actionBar.setTitle(R.string.compose);
Mindy Pereira326c6602012-01-04 15:32:42 -08001101 } else {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001102 actionBar.setTitle(null);
Mindy Pereira326c6602012-01-04 15:32:42 -08001103 if (mComposeModeAdapter == null) {
1104 mComposeModeAdapter = new ComposeModeAdapter(this);
1105 }
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001106 actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1107 actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
Alice Yanga990a712013-03-13 18:37:00 -07001108 switch (mComposeMode) {
Mindy Pereira326c6602012-01-04 15:32:42 -08001109 case ComposeActivity.REPLY:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001110 actionBar.setSelectedNavigationItem(0);
Mindy Pereira326c6602012-01-04 15:32:42 -08001111 break;
1112 case ComposeActivity.REPLY_ALL:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001113 actionBar.setSelectedNavigationItem(1);
Mindy Pereira326c6602012-01-04 15:32:42 -08001114 break;
1115 case ComposeActivity.FORWARD:
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001116 actionBar.setSelectedNavigationItem(2);
Mindy Pereira326c6602012-01-04 15:32:42 -08001117 break;
1118 }
1119 }
Mindy Pereirafbe40192012-03-20 10:40:45 -07001120 actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
1121 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
1122 actionBar.setHomeButtonEnabled(true);
Mindy Pereira326c6602012-01-04 15:32:42 -08001123 }
1124
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001125 private void initFromRefMessage(int action) {
1126 setFieldsFromRefMessage(action);
Alice Yang1ebc2db2013-03-14 21:21:44 -07001127
1128 // Check if To: address and email body needs to be prefilled based on extras.
1129 // This is used for reporting rendering feedback.
1130 if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1131 Intent intent = getIntent();
1132 if (intent.getExtras() != null) {
1133 String toAddresses = intent.getStringExtra(EXTRA_TO);
1134 if (toAddresses != null) {
1135 addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1136 }
1137 String body = intent.getStringExtra(EXTRA_BODY);
1138 if (body != null) {
1139 setBody(body, false /* withSignature */);
1140 }
1141 }
1142 }
1143
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001144 if (mRefMessage != null) {
1145 // CC field only gets populated when doing REPLY_ALL.
1146 // BCC never gets auto-populated, unless the user is editing
1147 // a draft with one.
Mindy Pereira29a717e2012-07-25 18:05:48 -07001148 if (!TextUtils.isEmpty(mCc.getText()) && action == REPLY_ALL) {
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07001149 mCcBccView.show(false, true, false);
1150 }
1151 }
1152 updateHideOrShowCcBcc();
1153 }
1154
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001155 private void setFieldsFromRefMessage(int action) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001156 setSubject(mRefMessage, action);
1157 // Setup recipients
1158 if (action == FORWARD) {
1159 mForward = true;
Mindy Pereira6349a042012-01-04 11:25:01 -08001160 }
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001161 initRecipientsFromRefMessage(mRefMessage, action);
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001162 initQuotedTextFromRefMessage(mRefMessage, action);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001163 if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1164 initAttachments(mRefMessage);
1165 }
Mindy Pereirac17d0732011-12-29 10:46:19 -08001166 }
1167
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001168 private void initFromDraftMessage(Message message) {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07001169 LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001170
1171 mDraft = message;
1172 mDraftId = message.id;
1173 mSubject.setText(message.subject);
1174 mForward = message.draftType == UIProvider.DraftType.FORWARD;
1175 final List<String> toAddresses = Arrays.asList(message.getToAddresses());
1176 addToAddresses(toAddresses);
1177 addCcAddresses(Arrays.asList(message.getCcAddresses()), toAddresses);
1178 addBccAddresses(Arrays.asList(message.getBccAddresses()));
Mindy Pereira2421dc82012-03-27 13:32:31 -07001179 if (message.hasAttachments) {
1180 List<Attachment> attachments = message.getAttachments();
1181 for (Attachment a : attachments) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001182 addAttachmentAndUpdateView(a);
Mindy Pereira2421dc82012-03-27 13:32:31 -07001183 }
1184 }
Mindy Pereiracc8e7db2012-05-30 12:57:42 -07001185 int quotedTextIndex = message.appendRefMessageContent ?
Mindy Pereira002ff522012-05-30 10:31:26 -07001186 message.quotedTextOffset : -1;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001187 // Set the body
Mindy Pereira002ff522012-05-30 10:31:26 -07001188 CharSequence quotedText = null;
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001189 if (!TextUtils.isEmpty(message.bodyHtml)) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001190 CharSequence htmlText = "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001191 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001192 // Find the offset in the htmltext of the actual quoted text and strip it out.
1193 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1194 if (quotedTextIndex > -1) {
mindypc59dd822012-11-13 10:56:21 -08001195 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml.substring(0,
1196 quotedTextIndex));
Mindy Pereira752222d2012-07-19 09:58:53 -07001197 quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1198 message.bodyHtml.length());
1199 }
Mindy Pereira1a6e9382012-08-14 15:51:22 -07001200 } else {
mindypc59dd822012-11-13 10:56:21 -08001201 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml);
Mindy Pereira002ff522012-05-30 10:31:26 -07001202 }
1203 mBodyView.setText(htmlText);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001204 } else {
Mindy Pereira752222d2012-07-19 09:58:53 -07001205 final String body = message.bodyText;
1206 final CharSequence bodyText = !TextUtils.isEmpty(body) ?
1207 (quotedTextIndex > -1 ?
1208 message.bodyText.substring(0, quotedTextIndex) : message.bodyText)
1209 : "";
Mindy Pereira002ff522012-05-30 10:31:26 -07001210 if (quotedTextIndex > -1) {
Mindy Pereira752222d2012-07-19 09:58:53 -07001211 quotedText = !TextUtils.isEmpty(body) ? message.bodyText.substring(quotedTextIndex)
1212 : null;
Mindy Pereira002ff522012-05-30 10:31:26 -07001213 }
1214 mBodyView.setText(bodyText);
1215 }
1216 if (quotedTextIndex > -1 && quotedText != null) {
Mindy Pereira39713232012-05-30 11:48:41 -07001217 mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001218 }
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001219 }
1220
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001221 /**
1222 * Fill all the widgets with the content found in the Intent Extra, if any.
1223 * Also apply the same style to all widgets. Note: if initFromExtras is
1224 * called as a result of switching between reply, reply all, and forward per
1225 * the latest revision of Gmail, and the user has already made changes to
1226 * attachments on a previous incarnation of the message (as a reply, reply
1227 * all, or forward), the original attachments from the message will not be
1228 * re-instantiated. The user's changes will be respected. This follows the
1229 * web gmail interaction.
1230 */
1231 public void initFromExtras(Intent intent) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001232 // If we were invoked with a SENDTO intent, the value
1233 // should take precedence
1234 final Uri dataUri = intent.getData();
1235 if (dataUri != null) {
1236 if (MAIL_TO.equals(dataUri.getScheme())) {
1237 initFromMailTo(dataUri.toString());
1238 } else {
Mindy Pereira0b4f28e2012-03-28 14:12:21 -07001239 if (!mAccount.composeIntentUri.equals(dataUri)) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001240 String toText = dataUri.getSchemeSpecificPart();
1241 if (toText != null) {
1242 mTo.setText("");
Mindy Pereiradbe89962012-04-13 09:42:38 -07001243 addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001244 }
1245 }
1246 }
1247 }
1248
1249 String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1250 if (extraStrings != null) {
1251 addToAddresses(Arrays.asList(extraStrings));
1252 }
1253 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1254 if (extraStrings != null) {
1255 addCcAddresses(Arrays.asList(extraStrings), null);
1256 }
1257 extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1258 if (extraStrings != null) {
1259 addBccAddresses(Arrays.asList(extraStrings));
1260 }
1261
1262 String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1263 if (extraString != null) {
1264 mSubject.setText(extraString);
1265 }
1266
1267 for (String extra : ALL_EXTRAS) {
1268 if (intent.hasExtra(extra)) {
1269 String value = intent.getStringExtra(extra);
1270 if (EXTRA_TO.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001271 addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001272 } else if (EXTRA_CC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001273 addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001274 } else if (EXTRA_BCC.equals(extra)) {
Mindy Pereiradbe89962012-04-13 09:42:38 -07001275 addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001276 } else if (EXTRA_SUBJECT.equals(extra)) {
1277 mSubject.setText(value);
1278 } else if (EXTRA_BODY.equals(extra)) {
1279 setBody(value, true /* with signature */);
1280 }
1281 }
1282 }
1283
1284 Bundle extras = intent.getExtras();
1285 if (extras != null) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001286 CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
1287 if (text != null) {
1288 setBody(text, true /* with signature */);
1289 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001290 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001291 }
1292
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001293 @VisibleForTesting
1294 protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001295 // TODO: handle the case where there are spaces in the display name as
1296 // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1297 // as they could be encoded ambiguously.
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001298 // Since URLDecode.decode changes + into ' ', and + is a valid
1299 // email character, we need to find/ replace these ourselves before
1300 // decoding.
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001301 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001302 return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001303 } catch (IllegalArgumentException e) {
1304 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1305 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1306 } else {
1307 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1308 }
1309 return null;
1310 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001311 }
1312
1313 /**
Yorke Lee7dd05b12013-04-25 10:04:43 -07001314 * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1315 * changing '+' into ' '
1316 *
1317 * @param toReplace Input string
1318 * @return The string with all "+" characters replaced with "%2B"
1319 */
1320 private String replacePlus(String toReplace) {
1321 return toReplace.replace("+", "%2B");
1322 }
1323
1324 /**
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001325 * Initialize the compose view from a String representing a mailTo uri.
1326 * @param mailToString The uri as a string.
1327 */
1328 public void initFromMailTo(String mailToString) {
1329 // We need to disguise this string as a URI in order to parse it
1330 // TODO: Remove this hack when http://b/issue?id=1445295 gets fixed
1331 Uri uri = Uri.parse("foo://" + mailToString);
1332 int index = mailToString.indexOf("?");
1333 int length = "mailto".length() + 1;
1334 String to;
1335 try {
1336 // Extract the recipient after mailto:
1337 if (index == -1) {
1338 to = decodeEmailInUri(mailToString.substring(length));
1339 } else {
1340 to = decodeEmailInUri(mailToString.substring(length, index));
1341 }
Mindy Pereiraa4069f22012-05-30 15:31:45 -07001342 if (!TextUtils.isEmpty(to)) {
1343 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1344 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001345 } catch (UnsupportedEncodingException e) {
1346 if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1347 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1348 } else {
1349 LogUtils.e(LOG_TAG, e, "Exception while decoding mailto address");
1350 }
1351 }
1352
1353 List<String> cc = uri.getQueryParameters("cc");
1354 addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1355
1356 List<String> otherTo = uri.getQueryParameters("to");
1357 addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1358
1359 List<String> bcc = uri.getQueryParameters("bcc");
1360 addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1361
1362 List<String> subject = uri.getQueryParameters("subject");
1363 if (subject.size() > 0) {
1364 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001365 mSubject.setText(URLDecoder.decode(replacePlus(subject.get(0)),
1366 UTF8_ENCODING_NAME));
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001367 } catch (UnsupportedEncodingException e) {
1368 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
1369 e.getMessage(), subject);
1370 }
1371 }
1372
1373 List<String> body = uri.getQueryParameters("body");
1374 if (body.size() > 0) {
1375 try {
Yorke Lee7dd05b12013-04-25 10:04:43 -07001376 setBody(URLDecoder.decode(replacePlus(body.get(0)), UTF8_ENCODING_NAME),
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001377 true /* with signature */);
1378 } catch (UnsupportedEncodingException e) {
1379 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
1380 }
1381 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001382 }
1383
Mindy Pereirabddd6f32012-06-20 12:10:03 -07001384 @VisibleForTesting
1385 protected void initAttachments(Message refMessage) {
Mark Wei434f2942012-08-24 11:54:02 -07001386 addAttachments(refMessage.getAttachments());
1387 }
1388
1389 public long addAttachments(List<Attachment> attachments) {
1390 long size = 0;
1391 AttachmentFailureException error = null;
1392 for (Attachment a : attachments) {
1393 try {
1394 size += mAttachmentsView.addAttachment(mAccount, a);
1395 } catch (AttachmentFailureException e) {
1396 error = e;
1397 }
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001398 }
Mark Wei434f2942012-08-24 11:54:02 -07001399 if (error != null) {
1400 LogUtils.e(LOG_TAG, error, "Error adding attachment");
1401 if (attachments.size() > 1) {
1402 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1403 } else {
1404 showAttachmentTooBigToast(error.getErrorRes());
1405 }
1406 }
1407 return size;
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001408 }
1409
1410 /**
1411 * When an attachment is too large to be added to a message, show a toast.
1412 * This method also updates the position of the toast so that it is shown
1413 * clearly above they keyboard if it happens to be open.
1414 */
Mark Wei434f2942012-08-24 11:54:02 -07001415 private void showAttachmentTooBigToast(int errorRes) {
1416 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1417 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1418 showErrorToast(getString(errorRes, maxSize));
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001419 }
1420
Mark Wei434f2942012-08-24 11:54:02 -07001421 private void showErrorToast(String message) {
1422 Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1423 t.setText(message);
Mindy Pereira3cd4f402012-07-17 11:16:18 -07001424 t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1425 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1426 t.show();
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001427 }
1428
Paul Westbrookf97588b2012-03-20 11:11:37 -07001429 private void initAttachmentsFromIntent(Intent intent) {
Paul Westbrook03ee9712012-04-02 09:51:51 -07001430 Bundle extras = intent.getExtras();
1431 if (extras == null) {
1432 extras = Bundle.EMPTY;
1433 }
Paul Westbrookf97588b2012-03-20 11:11:37 -07001434 final String action = intent.getAction();
1435 if (!mAttachmentsChanged) {
1436 long totalSize = 0;
1437 if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1438 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1439 for (String uriString : uris) {
1440 final Uri uri = Uri.parse(uriString);
1441 long size = 0;
1442 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001443 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001444 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001445 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001446 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001447 }
1448 totalSize += size;
1449 }
1450 }
mindyp9a9e8d62012-10-03 12:24:07 -07001451 if (extras.containsKey(Intent.EXTRA_STREAM)) {
1452 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
1453 ArrayList<Parcelable> uris = extras
1454 .getParcelableArrayList(Intent.EXTRA_STREAM);
1455 ArrayList<Attachment> attachments = new ArrayList<Attachment>();
1456 for (Parcelable uri : uris) {
1457 try {
1458 attachments.add(mAttachmentsView.generateLocalAttachment((Uri) uri));
1459 } catch (AttachmentFailureException e) {
1460 LogUtils.e(LOG_TAG, e, "Error adding attachment");
1461 String maxSize = AttachmentUtils.convertToHumanReadableSize(
1462 getApplicationContext(),
1463 mAccount.settings.getMaxAttachmentSize());
1464 showErrorToast(getString
1465 (R.string.generic_attachment_problem, maxSize));
1466 }
1467 }
1468 totalSize += addAttachments(attachments);
1469 } else {
1470 final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
1471 long size = 0;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001472 try {
mindyp9a9e8d62012-10-03 12:24:07 -07001473 size = mAttachmentsView.addAttachment(mAccount, uri);
Paul Westbrookf97588b2012-03-20 11:11:37 -07001474 } catch (AttachmentFailureException e) {
Paul Westbrookf97588b2012-03-20 11:11:37 -07001475 LogUtils.e(LOG_TAG, e, "Error adding attachment");
mindyp9a9e8d62012-10-03 12:24:07 -07001476 showAttachmentTooBigToast(e.getErrorRes());
Paul Westbrookf97588b2012-03-20 11:11:37 -07001477 }
mindyp9a9e8d62012-10-03 12:24:07 -07001478 totalSize += size;
Paul Westbrookf97588b2012-03-20 11:11:37 -07001479 }
1480 }
1481
1482 if (totalSize > 0) {
1483 mAttachmentsChanged = true;
1484 updateSaveUi();
1485 }
1486 }
1487 }
1488
1489
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07001490 private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1491 if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001492 mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1493 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001494 }
1495
1496 private void updateHideOrShowCcBcc() {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001497 // Its possible there is a menu item OR a button.
Mindy Pereira326689d2012-05-17 10:14:14 -07001498 boolean ccVisible = mCcBccView.isCcVisible();
1499 boolean bccVisible = mCcBccView.isBccVisible();
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001500 if (mCcBccButton != null) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001501 if (!ccVisible || !bccVisible) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001502 mCcBccButton.setVisibility(View.VISIBLE);
Mindy Pereira326689d2012-05-17 10:14:14 -07001503 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001504 : R.string.add_bcc_label));
1505 } else {
mindypcd0b0b92012-08-23 14:33:17 -07001506 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001507 }
1508 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001509 }
1510
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001511 private void showCcBcc(Bundle state) {
Mindy Pereira326689d2012-05-17 10:14:14 -07001512 if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
1513 boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
1514 boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
1515 if (showCc || showBcc) {
1516 mCcBccView.show(false, showCc, showBcc);
Mindy Pereira6faeedf2012-04-18 16:11:39 -07001517 }
Mindy Pereiraa34c9a02012-04-17 14:10:53 -07001518 }
1519 }
1520
Mindy Pereira013194c2012-01-06 15:09:33 -08001521 /**
1522 * Add attachment and update the compose area appropriately.
1523 * @param data
1524 */
1525 public void addAttachmentAndUpdateView(Intent data) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001526 addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
1527 }
1528
Andy Huang5c5fd572012-04-08 18:19:29 -07001529 public void addAttachmentAndUpdateView(Uri contentUri) {
1530 if (contentUri == null) {
Mindy Pereira2421dc82012-03-27 13:32:31 -07001531 return;
1532 }
Mindy Pereira013194c2012-01-06 15:09:33 -08001533 try {
Andy Huang5c5fd572012-04-08 18:19:29 -07001534 addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1535 } catch (AttachmentFailureException e) {
Andy Huang5c5fd572012-04-08 18:19:29 -07001536 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001537 showErrorToast(getResources().getString(
1538 e.getErrorRes(),
1539 AttachmentUtils.convertToHumanReadableSize(
1540 getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
Andy Huang5c5fd572012-04-08 18:19:29 -07001541 }
1542 }
1543
1544 public void addAttachmentAndUpdateView(Attachment attachment) {
1545 try {
Mark Wei434f2942012-08-24 11:54:02 -07001546 long size = mAttachmentsView.addAttachment(mAccount, attachment);
Mindy Pereira9932dee2012-01-10 16:09:50 -08001547 if (size > 0) {
1548 mAttachmentsChanged = true;
1549 updateSaveUi();
Mindy Pereira013194c2012-01-06 15:09:33 -08001550 }
Mindy Pereira9932dee2012-01-10 16:09:50 -08001551 } catch (AttachmentFailureException e) {
Mindy Pereira9932dee2012-01-10 16:09:50 -08001552 LogUtils.e(LOG_TAG, e, "Error adding attachment");
Mark Wei434f2942012-08-24 11:54:02 -07001553 showAttachmentTooBigToast(e.getErrorRes());
Mindy Pereira013194c2012-01-06 15:09:33 -08001554 }
1555 }
1556
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08001557 void initRecipientsFromRefMessage(Message refMessage, int action) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001558 // Don't populate the address if this is a forward.
1559 if (action == ComposeActivity.FORWARD) {
1560 return;
1561 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07001562 initReplyRecipients(refMessage, action);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001563 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001564
Mindy Pereira818143e2012-01-11 13:59:49 -08001565 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001566 void initReplyRecipients(final Message refMessage, final int action) {
mindyp5ed63112012-09-17 17:31:45 -07001567 String[] sentToAddresses = refMessage.getToAddresses();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001568 final Collection<String> toAddresses;
mindyp65b06f52012-11-21 10:35:08 -08001569 String replytoAddress = refMessage.getReplyTo();
1570 // If there is no reply to address, the reply to address is the sender.
1571 if (TextUtils.isEmpty(replytoAddress)) {
1572 replytoAddress = refMessage.getFrom();
1573 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001574
1575 // If this is a reply, the Cc list is empty. If this is a reply-all, the
1576 // Cc list is the union of the To and Cc recipients of the original
1577 // message, excluding the current user's email address and any addresses
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001578 // already on the To list.
1579 if (action == ComposeActivity.REPLY) {
Scott Kennedyff8553f2013-04-05 20:57:44 -07001580 toAddresses = initToRecipients(refMessage.getFrom(), replytoAddress, sentToAddresses);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001581 addToAddresses(toAddresses);
1582 } else if (action == ComposeActivity.REPLY_ALL) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001583 final Set<String> ccAddresses = Sets.newHashSet();
Scott Kennedyff8553f2013-04-05 20:57:44 -07001584 toAddresses = initToRecipients(refMessage.getFrom(), replytoAddress, sentToAddresses);
Mindy Pereira154386a2012-01-11 13:02:33 -08001585 addToAddresses(toAddresses);
Scott Kennedyff8553f2013-04-05 20:57:44 -07001586 addRecipients(ccAddresses, sentToAddresses);
1587 addRecipients(ccAddresses, refMessage.getCcAddresses());
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001588 addCcAddresses(ccAddresses, toAddresses);
1589 }
1590 }
1591
1592 private void addToAddresses(Collection<String> addresses) {
1593 addAddressesToList(addresses, mTo);
1594 }
1595
1596 private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001597 addCcAddressesToList(tokenizeAddressList(addresses),
1598 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001599 }
1600
Paul Westbrookbb87b7f2012-03-20 16:20:07 -07001601 private void addBccAddresses(Collection<String> addresses) {
1602 addAddressesToList(addresses, mBcc);
1603 }
1604
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001605 @VisibleForTesting
1606 protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1607 List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1608 String address;
1609
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001610 if (compareToList == null) {
1611 for (Rfc822Token[] tokens : addresses) {
1612 for (int i = 0; i < tokens.length; i++) {
1613 address = tokens[i].toString();
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001614 list.append(address + END_TOKEN);
1615 }
1616 }
Mindy Pereira8eca57a2012-03-20 16:42:34 -07001617 } else {
1618 HashSet<String> compareTo = convertToHashSet(compareToList);
1619 for (Rfc822Token[] tokens : addresses) {
1620 for (int i = 0; i < tokens.length; i++) {
1621 address = tokens[i].toString();
1622 // Check if this is a duplicate:
1623 if (!compareTo.contains(tokens[i].getAddress())) {
1624 // Get the address here
1625 list.append(address + END_TOKEN);
1626 }
1627 }
1628 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001629 }
1630 }
1631
Scott Kennedyff8553f2013-04-05 20:57:44 -07001632 private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
1633 final HashSet<String> hash = new HashSet<String>();
1634 for (final Rfc822Token[] tokens : list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001635 for (int i = 0; i < tokens.length; i++) {
1636 hash.add(tokens[i].getAddress());
1637 }
1638 }
1639 return hash;
1640 }
1641
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001642 protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1643 @VisibleForTesting
1644 List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1645
1646 for (String address: addresses) {
1647 tokenized.add(Rfc822Tokenizer.tokenize(address));
1648 }
1649 return tokenized;
1650 }
1651
1652 @VisibleForTesting
1653 void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1654 for (String address : addresses) {
1655 addAddressToList(address, list);
1656 }
1657 }
1658
Scott Kennedyff8553f2013-04-05 20:57:44 -07001659 private static void addAddressToList(final String address, final RecipientEditTextView list) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001660 if (address == null || list == null)
1661 return;
1662
Scott Kennedyff8553f2013-04-05 20:57:44 -07001663 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001664
1665 for (int i = 0; i < tokens.length; i++) {
1666 list.append(tokens[i] + END_TOKEN);
1667 }
1668 }
1669
1670 @VisibleForTesting
Scott Kennedyff8553f2013-04-05 20:57:44 -07001671 protected Collection<String> initToRecipients(final String fullSenderAddress,
1672 final String replyToAddress, final String[] inToAddresses) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001673 // The To recipient is the reply-to address specified in the original
1674 // message, unless it is:
1675 // the current user OR a custom from of the current user, in which case
1676 // it's the To recipient list of the original message.
1677 // OR missing, in which case use the sender of the original message
1678 Set<String> toAddresses = Sets.newHashSet();
mindyp65b06f52012-11-21 10:35:08 -08001679 if (!TextUtils.isEmpty(replyToAddress) && !recipientMatchesThisAccount(replyToAddress)) {
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001680 toAddresses.add(replyToAddress);
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001681 } else {
mindyp65b06f52012-11-21 10:35:08 -08001682 // In this case, the user is replying to a message in which their
1683 // current account or one of their custom from addresses is the only
1684 // recipient and they sent the original message.
1685 if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
1686 && recipientMatchesThisAccount(inToAddresses[0])) {
1687 toAddresses.add(inToAddresses[0]);
1688 return toAddresses;
1689 }
1690 // This happens if the user replies to a message they originally
1691 // wrote. In this case, "reply" really means "re-send," so we
1692 // target the original recipients. This works as expected even
1693 // if the user sent the original message to themselves.
1694 for (String address : inToAddresses) {
1695 if (!recipientMatchesThisAccount(address)) {
1696 toAddresses.add(address);
mindypfe8557b2012-11-05 12:05:16 -08001697 }
Mindy Pereira1469b4e2012-06-19 19:18:54 -07001698 }
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001699 }
1700 return toAddresses;
1701 }
1702
Scott Kennedyff8553f2013-04-05 20:57:44 -07001703 private void addRecipients(final Set<String> recipients, final String[] addresses) {
1704 for (final String email : addresses) {
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001705 // Do not add this account, or any of its custom from addresses, to
1706 // the list of recipients.
Mindy Pereira4a20b702012-01-05 16:24:24 -08001707 final String recipientAddress = Address.getEmailAddress(email).getAddress();
mindyp5ee5d692012-11-19 16:02:16 -08001708 if (!recipientMatchesThisAccount(recipientAddress)) {
Mindy Pereira4a27ea92012-01-05 15:55:25 -08001709 recipients.add(email.replace("\"\"", ""));
1710 }
1711 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001712 }
1713
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001714 /**
1715 * A recipient matches this account if it has the same address as the
1716 * currently selected account OR one of the custom from addresses associated
1717 * with the currently selected account.
1718 * @param accountAddress currently selected account
1719 * @param recipientAddress address we are comparing with the currently selected account
1720 * @return
1721 */
mindyp5ee5d692012-11-19 16:02:16 -08001722 protected boolean recipientMatchesThisAccount(String recipientAddress) {
1723 return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
mindypfe8557b2012-11-05 12:05:16 -08001724 mAccount.getReplyFroms());
Mindy Pereiracecc54a2012-07-31 09:38:11 -07001725 }
1726
Mindy Pereira3ce64e72012-01-13 14:29:45 -08001727 private void setSubject(Message refMessage, int action) {
1728 String subject = refMessage.subject;
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001729 String prefix;
1730 String correctedSubject = null;
1731 if (action == ComposeActivity.COMPOSE) {
1732 prefix = "";
1733 } else if (action == ComposeActivity.FORWARD) {
1734 prefix = getString(R.string.forward_subject_label);
1735 } else {
1736 prefix = getString(R.string.reply_subject_label);
1737 }
1738
1739 // Don't duplicate the prefix
Mindy Pereirac7a36992012-07-30 14:00:37 -07001740 if (!TextUtils.isEmpty(subject)
1741 && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
Mindy Pereira46ce0b12012-01-05 10:32:15 -08001742 correctedSubject = subject;
1743 } else {
1744 correctedSubject = String
1745 .format(getString(R.string.formatted_subject), prefix, subject);
1746 }
1747 mSubject.setText(correctedSubject);
1748 }
1749
Mindy Pereira818143e2012-01-11 13:59:49 -08001750 private void initRecipients() {
1751 setupRecipients(mTo);
1752 setupRecipients(mCc);
1753 setupRecipients(mBcc);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001754 }
1755
Mindy Pereira818143e2012-01-11 13:59:49 -08001756 private void setupRecipients(RecipientEditTextView view) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001757 view.setAdapter(new RecipientAdapter(this, mAccount));
Mindy Pereira82cc5662012-01-09 17:29:30 -08001758 if (mValidator == null) {
Paul Westbrook679a8cc2012-02-21 16:37:58 -08001759 final String accountName = mAccount.name;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001760 int offset = accountName.indexOf("@") + 1;
1761 String account = accountName;
Mindy Pereirac17d0732011-12-29 10:46:19 -08001762 if (offset > -1) {
Mindy Pereira33fe9082012-01-09 16:24:30 -08001763 account = account.substring(accountName.indexOf("@") + 1);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001764 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001765 mValidator = new Rfc822Validator(account);
Mindy Pereirac17d0732011-12-29 10:46:19 -08001766 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001767 view.setValidator(mValidator);
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001768 }
1769
1770 @Override
1771 public void onClick(View v) {
1772 int id = v.getId();
1773 switch (id) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001774 case R.id.add_cc_bcc:
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001775 // Verify that cc/ bcc aren't showing.
1776 // Animate in cc/bcc.
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08001777 showCcBccViews();
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001778 break;
mindypcd0b0b92012-08-23 14:33:17 -07001779 case R.id.add_photo_attachment:
1780 doAttach(MIME_TYPE_PHOTO);
1781 break;
1782 case R.id.add_video_attachment:
1783 doAttach(MIME_TYPE_VIDEO);
1784 break;
Mindy Pereira8e9305e2011-12-13 14:25:04 -08001785 }
1786 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001787
1788 @Override
1789 public boolean onCreateOptionsMenu(Menu menu) {
1790 super.onCreateOptionsMenu(menu);
Mindy Pereirab199d172012-08-13 11:04:03 -07001791 // Don't render any menu items when there are no accounts.
1792 if (mAccounts == null || mAccounts.length == 0) {
1793 return true;
1794 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001795 MenuInflater inflater = getMenuInflater();
1796 inflater.inflate(R.menu.compose_menu, menu);
mindyp1d7e9142012-11-21 13:54:30 -08001797
1798 /*
1799 * Start save in the correct enabled state.
1800 * 1) If a user launches compose from within gmail, save is disabled
1801 * until they add something, at which point, save is enabled, auto save
1802 * on exit; if the user empties everything, save is disabled, exiting does not
1803 * auto-save
1804 * 2) if a user replies/ reply all/ forwards from within gmail, save is
1805 * disabled until they change something, at which point, save is
1806 * enabled, auto save on exit; if the user empties everything, save is
1807 * disabled, exiting does not auto-save.
1808 * 3) If a user launches compose from another application and something
1809 * gets populated (attachments, recipients, body, subject, etc), save is
1810 * enabled, auto save on exit; if the user empties everything, save is
1811 * disabled, exiting does not auto-save
1812 */
Mindy Pereira82cc5662012-01-09 17:29:30 -08001813 mSave = menu.findItem(R.id.save);
mindyp1d7e9142012-11-21 13:54:30 -08001814 String action = getIntent() != null ? getIntent().getAction() : null;
1815 enableSave(mSavedInstanceState != null ?
1816 mSavedInstanceState.getBoolean(EXTRA_SAVE_ENABLED)
1817 : (Intent.ACTION_SEND.equals(action)
1818 || Intent.ACTION_SEND_MULTIPLE.equals(action)
1819 || Intent.ACTION_SENDTO.equals(action)
1820 || shouldSave()));
1821
Mindy Pereira82cc5662012-01-09 17:29:30 -08001822 mSend = menu.findItem(R.id.send);
Mindy Pereira3ca5bad2012-04-16 11:02:42 -07001823 MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
1824 MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
1825 if (helpItem != null) {
1826 helpItem.setVisible(mAccount != null
1827 && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
1828 }
1829 if (sendFeedbackItem != null) {
1830 sendFeedbackItem.setVisible(mAccount != null
1831 && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
1832 }
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001833 return true;
1834 }
1835
1836 @Override
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001837 public boolean onPrepareOptionsMenu(Menu menu) {
1838 MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
Mindy Pereira818143e2012-01-11 13:59:49 -08001839 if (ccBcc != null && mCc != null) {
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001840 // Its possible there is a menu item OR a button.
1841 boolean ccFieldVisible = mCc.isShown();
1842 boolean bccFieldVisible = mBcc.isShown();
1843 if (!ccFieldVisible || !bccFieldVisible) {
1844 ccBcc.setVisible(true);
1845 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
1846 : R.string.add_bcc_label));
1847 } else {
1848 ccBcc.setVisible(false);
1849 }
1850 }
1851 return true;
1852 }
1853
1854 @Override
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001855 public boolean onOptionsItemSelected(MenuItem item) {
1856 int id = item.getItemId();
Mindy Pereira75f66632012-01-11 11:42:02 -08001857 boolean handled = true;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001858 switch (id) {
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07001859 case R.id.add_photo_attachment:
1860 doAttach(MIME_TYPE_PHOTO);
1861 break;
1862 case R.id.add_video_attachment:
1863 doAttach(MIME_TYPE_VIDEO);
Mindy Pereira7b56a612011-12-14 12:32:28 -08001864 break;
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08001865 case R.id.add_cc_bcc:
1866 showCcBccViews();
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001867 break;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001868 case R.id.save:
Mindy Pereira48e31b02012-05-30 13:12:24 -07001869 doSave(true);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001870 break;
1871 case R.id.send:
1872 doSend();
Mindy Pereira75f66632012-01-11 11:42:02 -08001873 break;
Mindy Pereiraefe3d252012-03-01 14:20:44 -08001874 case R.id.discard:
1875 doDiscard();
1876 break;
Mindy Pereira1f936682012-03-02 11:30:33 -08001877 case R.id.settings:
1878 Utils.showSettings(this, mAccount);
1879 break;
Mindy Pereirafbe40192012-03-20 10:40:45 -07001880 case android.R.id.home:
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001881 onAppUpPressed();
Mindy Pereirafbe40192012-03-20 10:40:45 -07001882 break;
1883 case R.id.help_info_menu_item:
Paul Westbrook30745b62012-08-19 14:10:32 -07001884 Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
Mindy Pereirafbe40192012-03-20 10:40:45 -07001885 break;
1886 case R.id.feedback_menu_item:
Paul Westbrook17beb0b2012-08-20 13:34:37 -07001887 Utils.sendFeedback(this, mAccount, false);
Mindy Pereirafbe40192012-03-20 10:40:45 -07001888 break;
Mindy Pereira75f66632012-01-11 11:42:02 -08001889 default:
1890 handled = false;
Mindy Pereira33fe9082012-01-09 16:24:30 -08001891 break;
Mindy Pereirab47f3e22011-12-13 14:25:04 -08001892 }
1893 return !handled ? super.onOptionsItemSelected(item) : handled;
1894 }
Mindy Pereira326c6602012-01-04 15:32:42 -08001895
Mindy Pereirab199d172012-08-13 11:04:03 -07001896 @Override
1897 public void onBackPressed() {
1898 // If we are showing the wait fragment, just exit.
1899 if (getWaitFragment() != null) {
1900 finish();
1901 } else {
1902 super.onBackPressed();
1903 }
1904 }
1905
Vikram Aggarwal1672ff82012-09-21 10:15:22 -07001906 /**
1907 * Carries out the "up" action in the action bar.
1908 */
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001909 private void onAppUpPressed() {
1910 if (mLaunchedFromEmail) {
1911 // If this was started from Gmail, simply treat app up as the system back button, so
1912 // that the last view is restored.
1913 onBackPressed();
1914 return;
1915 }
1916
1917 // Fire the main activity to ensure it launches the "top" screen of mail.
1918 // Since the main Activity is singleTask, it should revive that task if it was already
1919 // started.
Vikram Aggarwal0c3c2052012-09-21 11:06:28 -07001920 final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
Paul Westbrookdaecb4b2012-05-31 10:21:26 -07001921 mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
1922 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
1923 startActivity(mailIntent);
1924 finish();
1925 }
1926
Mindy Pereira33fe9082012-01-09 16:24:30 -08001927 private void doSend() {
Mark Weidd19b632012-10-19 13:59:28 -07001928 sendOrSaveWithSanityChecks(false, true, false, false);
Mindy Pereira33fe9082012-01-09 16:24:30 -08001929 }
1930
Mindy Pereira48e31b02012-05-30 13:12:24 -07001931 private void doSave(boolean showToast) {
Mark Weidd19b632012-10-19 13:59:28 -07001932 sendOrSaveWithSanityChecks(true, showToast, false, false);
Mindy Pereira48e31b02012-05-30 13:12:24 -07001933 }
1934
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001935 @VisibleForTesting
1936 public interface SendOrSaveCallback {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001937 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001938 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
1939 public Message getMessage();
Mindy Pereira82cc5662012-01-09 17:29:30 -08001940 public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
1941 }
1942
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001943 @VisibleForTesting
1944 public static class SendOrSaveTask implements Runnable {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001945 private final Context mContext;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001946 @VisibleForTesting
Mindy Pereirae011b1d2012-06-18 13:45:26 -07001947 public final SendOrSaveCallback mSendOrSaveCallback;
1948 @VisibleForTesting
1949 public final SendOrSaveMessage mSendOrSaveMessage;
mindyp44a63392012-11-05 12:05:16 -08001950 private ReplyFromAccount mExistingDraftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001951
1952 public SendOrSaveTask(Context context, SendOrSaveMessage message,
mindyp44a63392012-11-05 12:05:16 -08001953 SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001954 mContext = context;
1955 mSendOrSaveCallback = callback;
1956 mSendOrSaveMessage = message;
mindyp44a63392012-11-05 12:05:16 -08001957 mExistingDraftAccount = draftAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001958 }
1959
1960 @Override
1961 public void run() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001962 final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001963
Mindy Pereira92551d02012-04-05 11:31:12 -07001964 final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08001965 Message message = mSendOrSaveCallback.getMessage();
1966 long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
Mindy Pereira82cc5662012-01-09 17:29:30 -08001967 // If a previous draft has been saved, in an account that is different
1968 // than what the user wants to send from, remove the old draft, and treat this
1969 // as a new message
mindyp44a63392012-11-05 12:05:16 -08001970 if (mExistingDraftAccount != null
1971 && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08001972 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
1973 ContentResolver resolver = mContext.getContentResolver();
1974 ContentValues values = new ContentValues();
1975 values.put(BaseColumns._ID, messageId);
mindypfebd2262012-11-13 17:45:09 -08001976 if (mExistingDraftAccount.account.expungeMessageUri != null) {
1977 new ContentProviderTask.UpdateTask()
1978 .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
1979 values, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08001980 } else {
1981 // TODO(mindyp) delete the conversation.
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08001982 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08001983 // reset messageId to 0, so a new message will be created
1984 messageId = UIProvider.INVALID_MESSAGE_ID;
1985 }
1986 }
1987
1988 final long messageIdToSave = messageId;
Scott Kennedyff8553f2013-04-05 20:57:44 -07001989 sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07001990
1991 if (!sendOrSaveMessage.mSave) {
1992 UIProvider.incrementRecipientsTimesContacted(mContext,
1993 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
1994 UIProvider.incrementRecipientsTimesContacted(mContext,
1995 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
1996 UIProvider.incrementRecipientsTimesContacted(mContext,
1997 (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
1998 }
1999 mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
2000 }
2001
2002 /**
2003 * Send or Save a message.
2004 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002005 private void sendOrSaveMessage(final long messageIdToSave,
2006 final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002007 final ContentResolver resolver = mContext.getContentResolver();
2008 final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2009
2010 final String accountMethod = sendOrSaveMessage.mSave ?
2011 UIProvider.AccountCallMethods.SAVE_MESSAGE :
2012 UIProvider.AccountCallMethods.SEND_MESSAGE;
2013
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002014 try {
2015 if (updateExistingMessage) {
2016 sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002017
Paul Westbrook013a23c2013-02-22 10:37:41 -08002018 callAccountSendSaveMethod(resolver,
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002019 selectedAccount.account, accountMethod, sendOrSaveMessage);
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002020 } else {
Paul Westbrook013a23c2013-02-22 10:37:41 -08002021 Uri messageUri = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002022 final Bundle result = callAccountSendSaveMethod(resolver,
2023 selectedAccount.account, accountMethod, sendOrSaveMessage);
2024 if (result != null) {
2025 // If a non-null value was returned, then the provider handled the call
2026 // method
2027 messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002028 }
2029 if (sendOrSaveMessage.mSave && messageUri != null) {
2030 final Cursor messageCursor = resolver.query(messageUri,
2031 UIProvider.MESSAGE_PROJECTION, null, null, null);
2032 if (messageCursor != null) {
2033 try {
2034 if (messageCursor.moveToFirst()) {
2035 // Broadcast notification that a new message has
2036 // been allocated
2037 mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
2038 new Message(messageCursor));
2039 }
2040 } finally {
2041 messageCursor.close();
Paul Westbrookba558482012-03-19 11:00:24 -07002042 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002043 }
2044 }
2045 }
2046 } finally {
2047 // Close any opened file descriptors
2048 closeOpenedAttachmentFds(sendOrSaveMessage);
2049 }
2050 }
2051
Scott Kennedyff8553f2013-04-05 20:57:44 -07002052 private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002053 final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2054 if (openedFds != null) {
2055 final Set<String> keys = openedFds.keySet();
Scott Kennedyff8553f2013-04-05 20:57:44 -07002056 for (final String key : keys) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002057 final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2058 if (fd != null) {
2059 try {
2060 fd.close();
2061 } catch (IOException e) {
2062 // Do nothing
Paul Westbrookba558482012-03-19 11:00:24 -07002063 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002064 }
2065 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002066 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002067 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002068
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002069 /**
2070 * Use the {@link ContentResolver#call()} method to send or save the message.
2071 *
2072 * If this was successful, this method will return an non-null Bundle instance
2073 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002074 private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2075 final Account account, final String method,
2076 final SendOrSaveMessage sendOrSaveMessage) {
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002077 // Copy all of the values from the content values to the bundle
2078 final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2079 final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2080
2081 for (Entry<String, Object> entry : valueSet) {
2082 final Object entryValue = entry.getValue();
2083 final String key = entry.getKey();
2084 if (entryValue instanceof String) {
2085 methodExtras.putString(key, (String)entryValue);
2086 } else if (entryValue instanceof Boolean) {
2087 methodExtras.putBoolean(key, (Boolean)entryValue);
2088 } else if (entryValue instanceof Integer) {
2089 methodExtras.putInt(key, (Integer)entryValue);
2090 } else if (entryValue instanceof Long) {
2091 methodExtras.putLong(key, (Long)entryValue);
2092 } else {
2093 LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2094 entryValue.getClass().getName());
2095 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002096 }
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002097
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002098 // If the SendOrSaveMessage has some opened fds, add them to the bundle
2099 final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2100 if (fdMap != null) {
2101 methodExtras.putParcelable(
2102 UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2103 }
2104
Paul Westbrook72e2ea82012-10-22 16:25:22 -07002105 return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002106 }
2107 }
2108
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002109 @VisibleForTesting
2110 public static class SendOrSaveMessage {
Mindy Pereira92551d02012-04-05 11:31:12 -07002111 final ReplyFromAccount mAccount;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002112 final ContentValues mValues;
Mindy Pereira3ce64e72012-01-13 14:29:45 -08002113 final String mRefMessageId;
Mindy Pereirae011b1d2012-06-18 13:45:26 -07002114 @VisibleForTesting
2115 public final boolean mSave;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002116 final int mRequestId;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002117 private final Bundle mAttachmentFds;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002118
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002119 public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
2120 String refMessageId, List<Attachment> attachments, boolean save) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002121 mAccount = account;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002122 mValues = values;
2123 mRefMessageId = refMessageId;
2124 mSave = save;
2125 mRequestId = mValues.hashCode() ^ hashCode();
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002126
2127 mAttachmentFds = initializeAttachmentFds(context, attachments);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002128 }
2129
2130 int requestId() {
2131 return mRequestId;
2132 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002133
2134 Bundle attachmentFds() {
2135 return mAttachmentFds;
2136 }
2137
2138 /**
2139 * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
2140 * called before the ComposeActivity finishes.
2141 * Note: The caller is responsible for closing these file descriptors.
2142 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002143 private static Bundle initializeAttachmentFds(final Context context,
2144 final List<Attachment> attachments) {
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002145 if (attachments == null || attachments.size() == 0) {
2146 return null;
2147 }
2148
2149 final Bundle result = new Bundle(attachments.size());
2150 final ContentResolver resolver = context.getContentResolver();
2151
2152 for (Attachment attachment : attachments) {
2153 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2154 continue;
2155 }
2156
2157 ParcelFileDescriptor fileDescriptor;
2158 try {
2159 fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2160 } catch (FileNotFoundException e) {
2161 LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2162 fileDescriptor = null;
Paul Westbrookc537fd42013-02-20 11:10:03 -08002163 } catch (SecurityException e) {
2164 // We have encountered a security exception when attempting to open the file
2165 // specified by the content uri. If the attachment has been cached, this
2166 // isn't a problem, as even through the original permission may have been
2167 // revoked, we have cached the file. This will happen when saving/sending
2168 // a previously saved draft.
2169 // TODO(markwei): Expose whether the attachment has been cached through the
2170 // attachment object. This would allow us to limit when the log is made, as
2171 // if the attachment has been cached, this really isn't an error
2172 LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2173 // Just set the file descriptor to null, as the underlying provider needs
2174 // to handle the file descriptor not being set.
2175 fileDescriptor = null;
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002176 }
2177
2178 if (fileDescriptor != null) {
2179 result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2180 }
2181 }
2182
2183 return result;
2184 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002185 }
2186
2187 /**
2188 * Get the to recipients.
2189 */
2190 public String[] getToAddresses() {
2191 return getAddressesFromList(mTo);
2192 }
2193
2194 /**
2195 * Get the cc recipients.
2196 */
2197 public String[] getCcAddresses() {
2198 return getAddressesFromList(mCc);
2199 }
2200
2201 /**
2202 * Get the bcc recipients.
2203 */
2204 public String[] getBccAddresses() {
2205 return getAddressesFromList(mBcc);
2206 }
2207
2208 public String[] getAddressesFromList(RecipientEditTextView list) {
2209 if (list == null) {
2210 return new String[0];
2211 }
2212 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2213 int count = tokens.length;
2214 String[] result = new String[count];
2215 for (int i = 0; i < count; i++) {
2216 result[i] = tokens[i].toString();
2217 }
2218 return result;
2219 }
2220
2221 /**
2222 * Check for invalid email addresses.
2223 * @param to String array of email addresses to check.
2224 * @param wrongEmailsOut Emails addresses that were invalid.
2225 */
Scott Kennedyff8553f2013-04-05 20:57:44 -07002226 public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
Mindy Pereirae5f20bf2012-06-25 14:20:40 -07002227 if (mValidator == null) {
2228 return;
2229 }
Scott Kennedyff8553f2013-04-05 20:57:44 -07002230 for (final String email : to) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002231 if (!mValidator.isValid(email)) {
2232 wrongEmailsOut.add(email);
2233 }
2234 }
2235 }
2236
2237 /**
2238 * Show an error because the user has entered an invalid recipient.
2239 * @param message
2240 */
2241 public void showRecipientErrorDialog(String message) {
2242 // Only 1 invalid recipients error dialog should be allowed up at a
2243 // time.
2244 if (mRecipientErrorDialog != null) {
2245 mRecipientErrorDialog.dismiss();
2246 }
2247 mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
2248 R.string.recipient_error_dialog_title)
2249 .setIconAttribute(android.R.attr.alertDialogIcon)
Mindy Pereira82cc5662012-01-09 17:29:30 -08002250 .setPositiveButton(
2251 R.string.ok, new Dialog.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07002252 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08002253 public void onClick(DialogInterface dialog, int which) {
2254 // after the user dismisses the recipient error
2255 // dialog we want to make sure to refocus the
2256 // recipient to field so they can fix the issue
2257 // easily
2258 if (mTo != null) {
2259 mTo.requestFocus();
2260 }
2261 mRecipientErrorDialog = null;
2262 }
2263 }).show();
2264 }
2265
2266 /**
2267 * Update the state of the UI based on whether or not the current draft
2268 * needs to be saved and the message is not empty.
2269 */
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002270 public void updateSaveUi() {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002271 if (mSave != null) {
2272 mSave.setEnabled((shouldSave() && !isBlank()));
2273 }
2274 }
2275
2276 /**
2277 * Returns true if we need to save the current draft.
2278 */
2279 private boolean shouldSave() {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002280 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002281 // The message should only be saved if:
2282 // It hasn't been sent AND
2283 // Some text has been added to the message OR
2284 // an attachment has been added or removed
Mindy Pereiraa2148332012-07-02 13:54:14 -07002285 // AND there is actually something in the draft to save.
Andy Huangd47877e2012-08-09 19:31:24 -07002286 return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
Mindy Pereiraa2148332012-07-02 13:54:14 -07002287 && !isBlank();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002288 }
2289 }
2290
2291 /**
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002292 * Check if all fields are blank.
Mindy Pereira82cc5662012-01-09 17:29:30 -08002293 * @return boolean
2294 */
2295 public boolean isBlank() {
2296 return mSubject.getText().length() == 0
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002297 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2298 mBodyView.getText().toString()) == 0)
2299 && mTo.length() == 0
2300 && mCc.length() == 0 && mBcc.length() == 0
2301 && mAttachmentsView.getAttachments().size() == 0;
2302 }
2303
2304 @VisibleForTesting
2305 protected int getSignatureStartPosition(String signature, String bodyText) {
2306 int startPos = -1;
2307
2308 if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2309 return startPos;
2310 }
2311
2312 int bodyLength = bodyText.length();
2313 int signatureLength = signature.length();
2314 String printableVersion = convertToPrintableSignature(signature);
2315 int printableLength = printableVersion.length();
2316
2317 if (bodyLength >= printableLength
2318 && bodyText.substring(bodyLength - printableLength)
2319 .equals(printableVersion)) {
2320 startPos = bodyLength - printableLength;
2321 } else if (bodyLength >= signatureLength
2322 && bodyText.substring(bodyLength - signatureLength)
2323 .equals(signature)) {
2324 startPos = bodyLength - signatureLength;
2325 }
2326 return startPos;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002327 }
2328
2329 /**
2330 * Allows any changes made by the user to be ignored. Called when the user
2331 * decides to discard a draft.
2332 */
2333 private void discardChanges() {
2334 mTextChanged = false;
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002335 mAttachmentsChanged = false;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002336 mReplyFromChanged = false;
2337 }
2338
2339 /**
Mindy Pereira181df782012-03-01 13:32:44 -08002340 * @param body
2341 * @param save
2342 * @param showToast
2343 * @return Whether the send or save succeeded.
2344 */
2345 protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
Mark Weidd19b632012-10-19 13:59:28 -07002346 final boolean orientationChanged, final boolean autoSend) {
Mark Wei009b3712012-10-18 18:07:50 -07002347 if (mAccounts == null || mAccount == null) {
2348 Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
Mark Weidd19b632012-10-19 13:59:28 -07002349 if (autoSend) {
2350 finish();
2351 }
Mark Wei009b3712012-10-18 18:07:50 -07002352 return false;
2353 }
2354
Scott Kennedyff8553f2013-04-05 20:57:44 -07002355 final String[] to, cc, bcc;
2356 final Editable body = mBodyView.getEditableText();
Mindy Pereira181df782012-03-01 13:32:44 -08002357 if (orientationChanged) {
2358 to = cc = bcc = new String[0];
2359 } else {
2360 to = getToAddresses();
2361 cc = getCcAddresses();
2362 bcc = getBccAddresses();
2363 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002364
Mindy Pereira181df782012-03-01 13:32:44 -08002365 // Don't let the user send to nobody (but it's okay to save a message
2366 // with no recipients)
2367 if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2368 showRecipientErrorDialog(getString(R.string.recipient_needed));
2369 return false;
2370 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002371
Mindy Pereira181df782012-03-01 13:32:44 -08002372 List<String> wrongEmails = new ArrayList<String>();
2373 if (!save) {
2374 checkInvalidEmails(to, wrongEmails);
2375 checkInvalidEmails(cc, wrongEmails);
2376 checkInvalidEmails(bcc, wrongEmails);
2377 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002378
Mindy Pereira181df782012-03-01 13:32:44 -08002379 // Don't let the user send an email with invalid recipients
2380 if (wrongEmails.size() > 0) {
2381 String errorText = String.format(getString(R.string.invalid_recipient),
2382 wrongEmails.get(0));
2383 showRecipientErrorDialog(errorText);
2384 return false;
2385 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002386
Mindy Pereira181df782012-03-01 13:32:44 -08002387 DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
Marc Blank0bbc8582012-04-23 15:07:57 -07002388 @Override
Mindy Pereira181df782012-03-01 13:32:44 -08002389 public void onClick(DialogInterface dialog, int which) {
Scott Kennedyff8553f2013-04-05 20:57:44 -07002390 sendOrSave(mBodyView.getEditableText(), save, showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002391 }
2392 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08002393
Mindy Pereira181df782012-03-01 13:32:44 -08002394 // Show a warning before sending only if there are no attachments.
2395 if (!save) {
2396 if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2397 boolean warnAboutEmptySubject = isSubjectEmpty();
2398 boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
Mindy Pereira82cc5662012-01-09 17:29:30 -08002399
Mindy Pereira181df782012-03-01 13:32:44 -08002400 // A warning about an empty body may not be warranted when
2401 // forwarding mails, since a common use case is to forward
2402 // quoted text and not append any more text.
2403 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002404
Mindy Pereira181df782012-03-01 13:32:44 -08002405 // When we bring up a dialog warning the user about a send,
2406 // assume that they accept sending the message. If they do not,
2407 // the dialog listener is required to enable sending again.
2408 if (warnAboutEmptySubject) {
2409 showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
2410 return true;
2411 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002412
Mindy Pereira181df782012-03-01 13:32:44 -08002413 if (warnAboutEmptyBody) {
2414 showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
2415 return true;
2416 }
2417 }
2418 // Ask for confirmation to send (if always required)
2419 if (showSendConfirmation()) {
2420 showSendConfirmDialog(R.string.confirm_send_message, listener);
2421 return true;
2422 }
2423 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002424
Scott Kennedyff8553f2013-04-05 20:57:44 -07002425 sendOrSave(body, save, showToast);
Mindy Pereira181df782012-03-01 13:32:44 -08002426 return true;
2427 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002428
Mindy Pereira181df782012-03-01 13:32:44 -08002429 /**
2430 * Returns a boolean indicating whether warnings should be shown for empty
2431 * subject and body fields
Andy Huang5c5fd572012-04-08 18:19:29 -07002432 *
Mindy Pereira181df782012-03-01 13:32:44 -08002433 * @return True if a warning should be shown for empty text fields
2434 */
2435 protected boolean showEmptyTextWarnings() {
2436 return mAttachmentsView.getAttachments().size() == 0;
2437 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002438
Mindy Pereira181df782012-03-01 13:32:44 -08002439 /**
2440 * Returns a boolean indicating whether the user should confirm each send
2441 *
2442 * @return True if a warning should be on each send
2443 */
2444 protected boolean showSendConfirmation() {
2445 return mCachedSettings != null ? mCachedSettings.confirmSend : false;
2446 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002447
Mindy Pereira181df782012-03-01 13:32:44 -08002448 private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
2449 if (mSendConfirmDialog != null) {
2450 mSendConfirmDialog.dismiss();
2451 mSendConfirmDialog = null;
2452 }
2453 mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
2454 .setTitle(R.string.confirm_send_title)
2455 .setIconAttribute(android.R.attr.alertDialogIcon)
2456 .setPositiveButton(R.string.send, listener)
Mindy Pereira6edd5972012-06-19 10:22:36 -07002457 .setNegativeButton(R.string.cancel, this)
2458 .show();
Mindy Pereira181df782012-03-01 13:32:44 -08002459 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002460
Mindy Pereira181df782012-03-01 13:32:44 -08002461 /**
2462 * Returns whether the ComposeArea believes there is any text in the body of
2463 * the composition. TODO: When ComposeArea controls the Body as well, add
2464 * that here.
2465 */
2466 public boolean isBodyEmpty() {
2467 return !mQuotedTextView.isTextIncluded();
2468 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002469
Mindy Pereira181df782012-03-01 13:32:44 -08002470 /**
2471 * Test to see if the subject is empty.
2472 *
2473 * @return boolean.
2474 */
2475 // TODO: this will likely go away when composeArea.focus() is implemented
2476 // after all the widget control is moved over.
2477 public boolean isSubjectEmpty() {
2478 return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
2479 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002480
Mindy Pereira181df782012-03-01 13:32:44 -08002481 /* package */
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002482 static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Paul Westbrook05b92b82012-04-20 13:29:37 -07002483 Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
mindyp44a63392012-11-05 12:05:16 -08002484 SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
2485 ReplyFromAccount draftAccount) {
Paul Westbrookb4931c62013-01-14 17:51:18 -08002486 final ContentValues values = new ContentValues();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002487
Paul Westbrookb4931c62013-01-14 17:51:18 -08002488 final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
Mindy Pereirac2031972012-04-03 09:38:35 -07002489
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002490 MessageModification.putToAddresses(values, message.getToAddresses());
2491 MessageModification.putCcAddresses(values, message.getCcAddresses());
2492 MessageModification.putBccAddresses(values, message.getBccAddresses());
Mindy Pereira82cc5662012-01-09 17:29:30 -08002493
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002494 MessageModification.putCustomFromAddress(values, message.getFrom());
Mindy Pereira92551d02012-04-05 11:31:12 -07002495
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002496 MessageModification.putSubject(values, message.subject);
Paul Westbrookb4931c62013-01-14 17:51:18 -08002497 // Make sure to remove only the composing spans from the Spannable before saving.
2498 final String htmlBody = Html.toHtml(removeComposingSpans(body));
Paul Westbrook05b92b82012-04-20 13:29:37 -07002499
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002500 boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
2501 StringBuilder fullBody = new StringBuilder(htmlBody);
2502 if (includeQuotedText) {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002503 // HTML gets converted to text for now
2504 final String text = quotedText.toString();
2505 if (QuotedTextView.containsQuotedText(text)) {
2506 int pos = QuotedTextView.getQuotedTextOffset(text);
Paul Westbrook55271cf2012-04-20 16:25:02 -07002507 final int quoteStartPos = fullBody.length() + pos;
2508 fullBody.append(text);
2509 MessageModification.putQuoteStartPos(values, quoteStartPos);
Mindy Pereira12575862012-03-21 16:30:54 -07002510 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
Mindy Pereirae8caf122012-03-20 15:23:31 -07002511 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002512 } else {
Mindy Pereirae8caf122012-03-20 15:23:31 -07002513 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
2514 // This shouldn't happen, but just use what we have,
2515 // and don't do server-side expansion
2516 fullBody.append(text);
Mindy Pereira29ef1b82012-01-13 11:26:21 -08002517 }
2518 }
Mindy Pereira002ff522012-05-30 10:31:26 -07002519 int draftType = getDraftType(composeMode);
Mindy Pereira12575862012-03-21 16:30:54 -07002520 MessageModification.putDraftType(values, draftType);
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002521 if (refMessage != null) {
2522 if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
2523 MessageModification.putBodyHtml(values, fullBody.toString());
2524 }
2525 if (!TextUtils.isEmpty(refMessage.bodyText)) {
mindypc59dd822012-11-13 10:56:21 -08002526 MessageModification.putBody(values,
2527 Utils.convertHtmlToPlainText(fullBody.toString()).toString());
Mindy Pereirac6f1e2a2012-04-04 10:33:45 -07002528 }
2529 } else {
Mindy Pereirac2031972012-04-03 09:38:35 -07002530 MessageModification.putBodyHtml(values, fullBody.toString());
mindypc59dd822012-11-13 10:56:21 -08002531 MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString())
2532 .toString());
Mindy Pereirac2031972012-04-03 09:38:35 -07002533 }
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002534 MessageModification.putAttachments(values, message.getAttachments());
Mindy Pereira12575862012-03-21 16:30:54 -07002535 if (!TextUtils.isEmpty(refMessageId)) {
2536 MessageModification.putRefMessageId(values, refMessageId);
2537 }
Paul Westbrook3c7f94d2012-10-23 14:13:00 -07002538 SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
2539 values, refMessageId, message.getAttachments(), save);
mindyp44a63392012-11-05 12:05:16 -08002540 SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
2541 draftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002542
Mindy Pereira181df782012-03-01 13:32:44 -08002543 callback.initializeSendOrSave(sendOrSaveTask);
Mindy Pereira181df782012-03-01 13:32:44 -08002544 // Do the send/save action on the specified handler to avoid possible
2545 // ANRs
2546 handler.post(sendOrSaveTask);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002547
Mindy Pereira181df782012-03-01 13:32:44 -08002548 return sendOrSaveMessage.requestId();
2549 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002550
Paul Westbrookb4931c62013-01-14 17:51:18 -08002551 /**
2552 * Removes any composing spans from the specified string. This will create a new
2553 * SpannableString instance, as to not modify the behavior of the EditText view.
2554 */
2555 private static SpannableString removeComposingSpans(Spanned body) {
2556 final SpannableString messageBody = new SpannableString(body);
2557 BaseInputConnection.removeComposingSpans(messageBody);
2558 return messageBody;
2559 }
2560
Mindy Pereira002ff522012-05-30 10:31:26 -07002561 private static int getDraftType(int mode) {
2562 int draftType = -1;
2563 switch (mode) {
2564 case ComposeActivity.COMPOSE:
2565 draftType = DraftType.COMPOSE;
2566 break;
2567 case ComposeActivity.REPLY:
2568 draftType = DraftType.REPLY;
2569 break;
2570 case ComposeActivity.REPLY_ALL:
2571 draftType = DraftType.REPLY_ALL;
2572 break;
2573 case ComposeActivity.FORWARD:
2574 draftType = DraftType.FORWARD;
2575 break;
2576 }
2577 return draftType;
2578 }
2579
Scott Kennedyff8553f2013-04-05 20:57:44 -07002580 private void sendOrSave(final Spanned body, final boolean save, final boolean showToast) {
Mindy Pereira181df782012-03-01 13:32:44 -08002581 // Check if user is a monkey. Monkeys can compose and hit send
2582 // button but are not allowed to send anything off the device.
Paul Westbrook3ae824c2012-04-06 13:29:39 -07002583 if (ActivityManager.isUserAMonkey()) {
Mindy Pereira181df782012-03-01 13:32:44 -08002584 return;
2585 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002586
Mindy Pereira181df782012-03-01 13:32:44 -08002587 SendOrSaveCallback callback = new SendOrSaveCallback() {
Andy Huang1f8f4dd2012-10-25 21:35:35 -07002588 // FIXME: unused
Mindy Pereira82cc5662012-01-09 17:29:30 -08002589 private int mRestoredRequestId;
2590
Marc Blank0bbc8582012-04-23 15:07:57 -07002591 @Override
Mindy Pereira82cc5662012-01-09 17:29:30 -08002592 public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
Mindy Pereira181df782012-03-01 13:32:44 -08002593 synchronized (mActiveTasks) {
2594 int numTasks = mActiveTasks.size();
2595 if (numTasks == 0) {
2596 // Start service so we won't be killed if this app is
2597 // put in the background.
2598 startService(new Intent(ComposeActivity.this, EmptyService.class));
2599 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002600
Mindy Pereira181df782012-03-01 13:32:44 -08002601 mActiveTasks.add(sendOrSaveTask);
2602 }
2603 if (sTestSendOrSaveCallback != null) {
2604 sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
2605 }
2606 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002607
Marc Blank0bbc8582012-04-23 15:07:57 -07002608 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002609 public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
2610 Message message) {
Mindy Pereira181df782012-03-01 13:32:44 -08002611 synchronized (mDraftLock) {
mindyp44a63392012-11-05 12:05:16 -08002612 mDraftAccount = sendOrSaveMessage.mAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002613 mDraftId = message.id;
2614 mDraft = message;
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002615 if (sRequestMessageIdMap != null) {
2616 sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
2617 }
Mindy Pereira181df782012-03-01 13:32:44 -08002618 // Cache request message map, in case the process is killed
2619 saveRequestMap();
2620 }
2621 if (sTestSendOrSaveCallback != null) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002622 sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
Mindy Pereira181df782012-03-01 13:32:44 -08002623 }
2624 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002625
Marc Blank0bbc8582012-04-23 15:07:57 -07002626 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002627 public Message getMessage() {
2628 synchronized (mDraftLock) {
2629 return mDraft;
2630 }
2631 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002632
Marc Blank0bbc8582012-04-23 15:07:57 -07002633 @Override
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002634 public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
Mindy Pereira47d0e652012-07-23 09:45:07 -07002635 // Update the last sent from account.
2636 if (mAccount != null) {
2637 MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
2638 }
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002639 if (success) {
2640 // Successfully sent or saved so reset change markers
2641 discardChanges();
2642 } else {
2643 // A failure happened with saving/sending the draft
2644 // TODO(pwestbro): add a better string that should be used
2645 // when failing to send or save
2646 Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
2647 .show();
2648 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002649
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002650 int numTasks;
2651 synchronized (mActiveTasks) {
2652 // Remove the task from the list of active tasks
2653 mActiveTasks.remove(task);
2654 numTasks = mActiveTasks.size();
2655 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002656
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002657 if (numTasks == 0) {
2658 // Stop service so we can be killed.
2659 stopService(new Intent(ComposeActivity.this, EmptyService.class));
2660 }
2661 if (sTestSendOrSaveCallback != null) {
2662 sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
2663 }
2664 }
Mindy Pereira181df782012-03-01 13:32:44 -08002665 };
Mindy Pereira82cc5662012-01-09 17:29:30 -08002666
Mindy Pereira181df782012-03-01 13:32:44 -08002667 // Get the selected account if the from spinner has been setup.
Mindy Pereira92551d02012-04-05 11:31:12 -07002668 ReplyFromAccount selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002669 String fromAddress = selectedAccount.name;
2670 if (selectedAccount == null || fromAddress == null) {
2671 // We don't have either the selected account or from address,
2672 // use mAccount.
Mindy Pereira92551d02012-04-05 11:31:12 -07002673 selectedAccount = mReplyFromAccount;
Mindy Pereira181df782012-03-01 13:32:44 -08002674 fromAddress = mAccount.name;
2675 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002676
Mindy Pereira181df782012-03-01 13:32:44 -08002677 if (mSendSaveTaskHandler == null) {
2678 HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
2679 handlerThread.start();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002680
Mindy Pereira181df782012-03-01 13:32:44 -08002681 mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
2682 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002683
Mindy Pereirae8f94dc2012-04-16 11:56:21 -07002684 Message msg = createMessage(mReplyFromAccount, getMode());
Paul Westbrook05b92b82012-04-20 13:29:37 -07002685 mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
2686 mQuotedTextView.getQuotedTextIfIncluded(), callback,
mindyp44a63392012-11-05 12:05:16 -08002687 mSendSaveTaskHandler, save, mComposeMode, mDraftAccount);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002688
Mindy Pereira181df782012-03-01 13:32:44 -08002689 if (mRecipient != null && mRecipient.equals(mAccount.name)) {
2690 mRecipient = selectedAccount.name;
2691 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002692 setAccount(selectedAccount.account);
Mindy Pereira82cc5662012-01-09 17:29:30 -08002693
Mindy Pereira181df782012-03-01 13:32:44 -08002694 // Don't display the toast if the user is just changing the orientation,
2695 // but we still need to save the draft to the cursor because this is how we restore
2696 // the attachments when the configuration change completes.
2697 if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
2698 Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
2699 Toast.LENGTH_LONG).show();
2700 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002701
Mindy Pereira181df782012-03-01 13:32:44 -08002702 // Need to update variables here because the send or save completes
2703 // asynchronously even though the toast shows right away.
2704 discardChanges();
2705 updateSaveUi();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002706
Mindy Pereira181df782012-03-01 13:32:44 -08002707 // If we are sending, finish the activity
2708 if (!save) {
2709 finish();
2710 }
2711 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002712
Mindy Pereira181df782012-03-01 13:32:44 -08002713 /**
2714 * Save the state of the request messageid map. This allows for the Gmail
2715 * process to be killed, but and still allow for ComposeActivity instances
2716 * to be recreated correctly.
2717 */
2718 private void saveRequestMap() {
2719 // TODO: store the request map in user preferences.
2720 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002721
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07002722 private void doAttach(String type) {
Mindy Pereira013194c2012-01-06 15:09:33 -08002723 Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2724 i.addCategory(Intent.CATEGORY_OPENABLE);
Paul Westbrookd6a9a3f2012-04-26 18:47:23 -07002725 i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
Mindy Pereira2db7d4a2012-08-15 11:00:02 -07002726 i.setType(type);
Mindy Pereira013194c2012-01-06 15:09:33 -08002727 mAddingAttachment = true;
Mindy Pereira181df782012-03-01 13:32:44 -08002728 startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2729 RESULT_PICK_ATTACHMENT);
Mindy Pereira013194c2012-01-06 15:09:33 -08002730 }
2731
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002732 private void showCcBccViews() {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002733 mCcBccView.show(true, true, true);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002734 if (mCcBccButton != null) {
mindypcd0b0b92012-08-23 14:33:17 -07002735 mCcBccButton.setVisibility(View.INVISIBLE);
Mindy Pereiraec8b0ed2012-01-06 10:35:59 -08002736 }
2737 }
2738
Mindy Pereira326c6602012-01-04 15:32:42 -08002739 @Override
2740 public boolean onNavigationItemSelected(int position, long itemId) {
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002741 int initialComposeMode = mComposeMode;
Mindy Pereira326c6602012-01-04 15:32:42 -08002742 if (position == ComposeActivity.REPLY) {
2743 mComposeMode = ComposeActivity.REPLY;
2744 } else if (position == ComposeActivity.REPLY_ALL) {
2745 mComposeMode = ComposeActivity.REPLY_ALL;
2746 } else if (position == ComposeActivity.FORWARD) {
2747 mComposeMode = ComposeActivity.FORWARD;
2748 }
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07002749 clearChangeListeners();
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002750 if (initialComposeMode != mComposeMode) {
Mindy Pereira154386a2012-01-11 13:02:33 -08002751 resetMessageForModeChange();
mindyp68c0bfc2012-12-04 10:29:48 -08002752 if (mRefMessage != null) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08002753 setFieldsFromRefMessage(mComposeMode);
Mindy Pereira8eca57a2012-03-20 16:42:34 -07002754 }
Mindy Pereiraef388302012-06-18 19:07:44 -07002755 boolean showCc = false;
2756 boolean showBcc = false;
2757 if (mDraft != null) {
2758 // Following desktop behavior, if the user has added a BCC
2759 // field to a draft, we show it regardless of compose mode.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002760 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
Mindy Pereiraef388302012-06-18 19:07:44 -07002761 // Use the draft to determine what to populate.
2762 // If the Bcc field is showing, show the Cc field whether it is populated or not.
Scott Kennedy8960f0a2012-11-07 15:35:50 -08002763 showCc = showBcc
2764 || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
mindyp68c0bfc2012-12-04 10:29:48 -08002765 }
2766 if (mRefMessage != null) {
mindyp9b1ac572012-09-27 14:12:00 -07002767 showCc = !TextUtils.isEmpty(mCc.getText());
mindyp68c0bfc2012-12-04 10:29:48 -08002768 showBcc = !TextUtils.isEmpty(mBcc.getText());
Mindy Pereiraef388302012-06-18 19:07:44 -07002769 }
2770 mCcBccView.show(false, showCc, showBcc);
Mindy Pereiraa26b54e2012-01-06 12:54:33 -08002771 }
Mindy Pereiraef388302012-06-18 19:07:44 -07002772 updateHideOrShowCcBcc();
Mindy Pereiracbfb75a2012-06-25 14:52:23 -07002773 initChangeListeners();
Mindy Pereira326c6602012-01-04 15:32:42 -08002774 return true;
2775 }
2776
Mindy Pereirab3112a22012-06-20 12:10:03 -07002777 @VisibleForTesting
2778 protected void resetMessageForModeChange() {
Mindy Pereira154386a2012-01-11 13:02:33 -08002779 // When switching between reply, reply all, forward,
2780 // follow the behavior of webview.
2781 // The contents of the following fields are cleared
2782 // so that they can be populated directly from the
2783 // ref message:
2784 // 1) Any recipient fields
2785 // 2) The subject
2786 mTo.setText("");
2787 mCc.setText("");
2788 mBcc.setText("");
2789 // Any edits to the subject are replaced with the original subject.
2790 mSubject.setText("");
2791
2792 // Any changes to the contents of the following fields are kept:
2793 // 1) Body
2794 // 2) Attachments
2795 // If the user made changes to attachments, keep their changes.
2796 if (!mAttachmentsChanged) {
2797 mAttachmentsView.deleteAllAttachments();
2798 }
2799 }
2800
Mindy Pereira326c6602012-01-04 15:32:42 -08002801 private class ComposeModeAdapter extends ArrayAdapter<String> {
2802
2803 private LayoutInflater mInflater;
2804
2805 public ComposeModeAdapter(Context context) {
2806 super(context, R.layout.compose_mode_item, R.id.mode, getResources()
2807 .getStringArray(R.array.compose_modes));
2808 }
2809
2810 private LayoutInflater getInflater() {
2811 if (mInflater == null) {
2812 mInflater = LayoutInflater.from(getContext());
2813 }
2814 return mInflater;
2815 }
2816
2817 @Override
2818 public View getView(int position, View convertView, ViewGroup parent) {
2819 if (convertView == null) {
2820 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
2821 }
2822 ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
2823 return super.getView(position, convertView, parent);
2824 }
2825 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002826
2827 @Override
2828 public void onRespondInline(String text) {
2829 appendToBody(text, false);
mindyp40882432012-09-06 11:07:40 -07002830 mQuotedTextView.setUpperDividerVisible(false);
mindyp1623f9b2012-11-21 12:41:16 -08002831 mRespondedInline = true;
mindyp09dd3732012-12-17 08:37:52 -08002832 if (!mBodyView.hasFocus()) {
mindyp8654d4f2012-12-17 09:01:37 -08002833 mBodyView.requestFocus();
mindyp09dd3732012-12-17 08:37:52 -08002834 }
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002835 }
2836
2837 /**
2838 * Append text to the body of the message. If there is no existing body
2839 * text, just sets the body to text.
2840 *
2841 * @param text
2842 * @param withSignature True to append a signature.
2843 */
2844 public void appendToBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002845 Editable bodyText = mBodyView.getEditableText();
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002846 if (bodyText != null && bodyText.length() > 0) {
2847 bodyText.append(text);
2848 } else {
2849 setBody(text, withSignature);
2850 }
2851 }
2852
2853 /**
2854 * Set the body of the message.
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002855 *
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002856 * @param text
2857 * @param withSignature True to append a signature.
2858 */
2859 public void setBody(CharSequence text, boolean withSignature) {
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002860 mBodyView.setText(text);
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002861 if (withSignature) {
2862 appendSignature();
2863 }
2864 }
2865
2866 private void appendSignature() {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002867 String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
Mindy Pereira433b1982012-04-03 11:53:07 -07002868 boolean hasFocus = mBodyView.hasFocus();
mindyp27083062012-11-15 09:02:01 -08002869 int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
2870 if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002871 mSignature = newSignature;
mindyp27083062012-11-15 09:02:01 -08002872 if (!TextUtils.isEmpty(mSignature)) {
Mindy Pereirab13917c2012-03-29 08:08:19 -07002873 // Appending a signature does not count as changing text.
2874 mBodyView.removeTextChangedListener(this);
2875 mBodyView.append(convertToPrintableSignature(mSignature));
2876 mBodyView.addTextChangedListener(this);
2877 }
Mindy Pereira433b1982012-04-03 11:53:07 -07002878 if (hasFocus) {
2879 focusBody();
2880 }
Mindy Pereirabdf7a402012-03-01 15:23:26 -08002881 }
2882 }
2883
2884 private String convertToPrintableSignature(String signature) {
2885 String signatureResource = getResources().getString(R.string.signature);
2886 if (signature == null) {
2887 signature = "";
2888 }
2889 return String.format(signatureResource, signature);
Mindy Pereira46ce0b12012-01-05 10:32:15 -08002890 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002891
Mindy Pereira5a85e2b2012-01-11 09:53:32 -08002892 @Override
2893 public void onAccountChanged() {
Mindy Pereira92551d02012-04-05 11:31:12 -07002894 mReplyFromAccount = mFromSpinner.getCurrentAccount();
2895 if (!mAccount.equals(mReplyFromAccount.account)) {
mindypf432dbc2012-11-12 16:00:44 -08002896 // Clear a signature, if there was one.
2897 mBodyView.removeTextChangedListener(this);
2898 String oldSignature = mSignature;
2899 String bodyText = getBody().getText().toString();
2900 if (!TextUtils.isEmpty(oldSignature)) {
2901 int pos = getSignatureStartPosition(oldSignature, bodyText);
2902 if (pos > -1) {
2903 mBodyView.setText(bodyText.substring(0, pos));
2904 }
2905 }
Paul Westbrookb1f573c2012-04-06 11:38:28 -07002906 setAccount(mReplyFromAccount.account);
mindypf432dbc2012-11-12 16:00:44 -08002907 mBodyView.addTextChangedListener(this);
Mindy Pereira181df782012-03-01 13:32:44 -08002908 // TODO: handle discarding attachments when switching accounts.
2909 // Only enable save for this draft if there is any other content
2910 // in the message.
2911 if (!isBlank()) {
2912 enableSave(true);
2913 }
2914 mReplyFromChanged = true;
2915 initRecipients();
Mindy Pereira82cc5662012-01-09 17:29:30 -08002916 }
Mindy Pereira1a95a572012-01-05 12:21:29 -08002917 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002918
2919 public void enableSave(boolean enabled) {
2920 if (mSave != null) {
2921 mSave.setEnabled(enabled);
2922 }
2923 }
2924
2925 public void enableSend(boolean enabled) {
2926 if (mSend != null) {
2927 mSend.setEnabled(enabled);
2928 }
2929 }
2930
2931 /**
2932 * Handles button clicks from any error dialogs dealing with sending
2933 * a message.
2934 */
2935 @Override
2936 public void onClick(DialogInterface dialog, int which) {
2937 switch (which) {
2938 case DialogInterface.BUTTON_POSITIVE: {
2939 doDiscardWithoutConfirmation(true /* show toast */ );
2940 break;
2941 }
2942 case DialogInterface.BUTTON_NEGATIVE: {
2943 // If the user cancels the send, re-enable the send button.
2944 enableSend(true);
2945 break;
2946 }
2947 }
2948
2949 }
2950
Mindy Pereiraefe3d252012-03-01 14:20:44 -08002951 private void doDiscard() {
2952 new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text)
2953 .setPositiveButton(R.string.ok, this)
2954 .setNegativeButton(R.string.cancel, null)
2955 .create().show();
2956 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002957 /**
2958 * Effectively discard the current message.
2959 *
2960 * This method is either invoked from the menu or from the dialog
2961 * once the user has confirmed that they want to discard the message.
2962 * @param showToast show "Message discarded" toast if true
2963 */
2964 private void doDiscardWithoutConfirmation(boolean showToast) {
Mindy Pereira7ed1c112012-01-18 10:59:25 -08002965 synchronized (mDraftLock) {
Mindy Pereira82cc5662012-01-09 17:29:30 -08002966 if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
2967 ContentValues values = new ContentValues();
Paul Westbrookb7050e62012-03-20 12:59:44 -07002968 values.put(BaseColumns._ID, mDraftId);
Marc Blank78ea8e22012-08-04 11:14:06 -07002969 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002970 getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
2971 } else {
Marc Blank0bbc8582012-04-23 15:07:57 -07002972 getContentResolver().delete(mDraft.uri, null, null);
Mindy Pereiracfb7f332012-02-28 10:23:43 -08002973 }
Mindy Pereira82cc5662012-01-09 17:29:30 -08002974 // This is not strictly necessary (since we should not try to
2975 // save the draft after calling this) but it ensures that if we
2976 // do save again for some reason we make a new draft rather than
2977 // trying to resave an expunged draft.
2978 mDraftId = UIProvider.INVALID_MESSAGE_ID;
2979 }
2980 }
2981
2982 if (showToast) {
2983 // Display a toast to let the user know
2984 Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
2985 }
2986
2987 // This prevents the draft from being saved in onPause().
2988 discardChanges();
2989 finish();
2990 }
2991
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08002992 private void saveIfNeeded() {
2993 if (mAccount == null) {
2994 // We have not chosen an account yet so there's no way that we can save. This is ok,
2995 // though, since we are saving our state before AccountsActivity is activated. Thus, the
2996 // user has not interacted with us yet and there is no real state to save.
2997 return;
2998 }
2999
3000 if (shouldSave()) {
Mindy Pereira48e31b02012-05-30 13:12:24 -07003001 doSave(!mAddingAttachment /* show toast */);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003002 }
3003 }
3004
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003005 @Override
3006 public void onAttachmentDeleted() {
3007 mAttachmentsChanged = true;
mindyp40882432012-09-06 11:07:40 -07003008 // If we are showing any attachments, make sure we have an upper
3009 // divider.
3010 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
Mindy Pereiraeaea9f12012-01-10 15:05:27 -08003011 updateSaveUi();
3012 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003013
mindyp40882432012-09-06 11:07:40 -07003014 @Override
3015 public void onAttachmentAdded() {
3016 mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3017 mAttachmentsView.focusLastAttachment();
3018 }
Mindy Pereira75f66632012-01-11 11:42:02 -08003019
3020 /**
3021 * This is called any time one of our text fields changes.
3022 */
Marc Blank0bbc8582012-04-23 15:07:57 -07003023 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003024 public void afterTextChanged(Editable s) {
3025 mTextChanged = true;
3026 updateSaveUi();
3027 }
3028
3029 @Override
3030 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3031 // Do nothing.
3032 }
3033
Marc Blank0bbc8582012-04-23 15:07:57 -07003034 @Override
Mindy Pereira75f66632012-01-11 11:42:02 -08003035 public void onTextChanged(CharSequence s, int start, int before, int count) {
3036 // Do nothing.
3037 }
3038
3039
3040 // There is a big difference between the text associated with an address changing
3041 // to add the display name or to format properly and a recipient being added or deleted.
3042 // Make sure we only notify of changes when a recipient has been added or deleted.
3043 private class RecipientTextWatcher implements TextWatcher {
3044 private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3045
3046 private RecipientEditTextView mView;
3047
3048 private TextWatcher mListener;
3049
3050 public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3051 mView = view;
3052 mListener = listener;
3053 }
3054
3055 @Override
3056 public void afterTextChanged(Editable s) {
3057 if (hasChanged()) {
3058 mListener.afterTextChanged(s);
3059 }
3060 }
3061
3062 private boolean hasChanged() {
3063 String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
3064 int totalCount = currRecips.length;
3065 int totalPrevCount = 0;
3066 for (Entry<String, Integer> entry : mContent.entrySet()) {
3067 totalPrevCount += entry.getValue();
3068 }
3069 if (totalCount != totalPrevCount) {
3070 return true;
3071 }
3072
3073 for (String recip : currRecips) {
3074 if (!mContent.containsKey(recip)) {
3075 return true;
3076 } else {
3077 int count = mContent.get(recip) - 1;
3078 if (count < 0) {
3079 return true;
3080 } else {
3081 mContent.put(recip, count);
3082 }
3083 }
3084 }
3085 return false;
3086 }
3087
3088 private String[] tokenizeRecips(String[] recips) {
3089 // Tokenize them all and put them in the list.
3090 String[] recipAddresses = new String[recips.length];
3091 for (int i = 0; i < recips.length; i++) {
3092 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
3093 }
3094 return recipAddresses;
3095 }
3096
3097 @Override
3098 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3099 String[] recips = tokenizeRecips(getAddressesFromList(mView));
3100 for (String recip : recips) {
3101 if (!mContent.containsKey(recip)) {
3102 mContent.put(recip, 1);
3103 } else {
3104 mContent.put(recip, (mContent.get(recip)) + 1);
3105 }
3106 }
3107 }
3108
3109 @Override
3110 public void onTextChanged(CharSequence s, int start, int before, int count) {
3111 // Do nothing.
3112 }
3113 }
Mindy Pereirae011b1d2012-06-18 13:45:26 -07003114
3115 public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3116 if (sTestSendOrSaveCallback != null && testCallback != null) {
3117 throw new IllegalStateException("Attempting to register more than one test callback");
3118 }
3119 sTestSendOrSaveCallback = testCallback;
3120 }
Mindy Pereirabddd6f32012-06-20 12:10:03 -07003121
3122 @VisibleForTesting
3123 protected ArrayList<Attachment> getAttachments() {
3124 return mAttachmentsView.getAttachments();
3125 }
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003126
3127 @Override
3128 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3129 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003130 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3131 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3132 null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003133 case REFERENCE_MESSAGE_LOADER:
3134 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3135 null, null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003136 case LOADER_ACCOUNT_CURSOR:
3137 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3138 UIProvider.ACCOUNTS_PROJECTION, null, null, null);
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003139 }
3140 return null;
3141 }
3142
3143 @Override
3144 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003145 int id = loader.getId();
3146 switch (id) {
Alice Yanga990a712013-03-13 18:37:00 -07003147 case INIT_DRAFT_USING_REFERENCE_MESSAGE:
Mindy Pereirab199d172012-08-13 11:04:03 -07003148 if (data != null && data.moveToFirst()) {
3149 mRefMessage = new Message(data);
Mindy Pereirab199d172012-08-13 11:04:03 -07003150 Intent intent = getIntent();
Alice Yanga990a712013-03-13 18:37:00 -07003151 initFromRefMessage(mComposeMode);
3152 finishSetup(mComposeMode, intent, null);
3153 if (mComposeMode != FORWARD) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003154 String to = intent.getStringExtra(EXTRA_TO);
3155 if (!TextUtils.isEmpty(to)) {
Scott Kennedy0aeaf7d2012-11-14 18:56:05 -08003156 mRefMessage.setTo(null);
3157 mRefMessage.setFrom(null);
Mindy Pereirab199d172012-08-13 11:04:03 -07003158 clearChangeListeners();
3159 mTo.append(to);
3160 initChangeListeners();
3161 }
3162 }
3163 } else {
3164 finish();
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003165 }
Mindy Pereirab199d172012-08-13 11:04:03 -07003166 break;
Alice Yanga990a712013-03-13 18:37:00 -07003167 case REFERENCE_MESSAGE_LOADER:
3168 // Only populate mRefMessage and leave other fields untouched.
3169 if (data != null && data.moveToFirst()) {
3170 mRefMessage = new Message(data);
3171 }
3172 finishSetup(mComposeMode, getIntent(), mSavedInstanceState);
3173 break;
Mindy Pereirab199d172012-08-13 11:04:03 -07003174 case LOADER_ACCOUNT_CURSOR:
3175 if (data != null && data.moveToFirst()) {
3176 // there are accounts now!
3177 Account account;
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003178 final ArrayList<Account> accounts = new ArrayList<Account>();
3179 final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
Mindy Pereirab199d172012-08-13 11:04:03 -07003180 do {
3181 account = new Account(data);
Paul Westbrookdfa1dec2012-09-26 16:27:28 -07003182 if (account.isAccountReady()) {
Mindy Pereirab199d172012-08-13 11:04:03 -07003183 initializedAccounts.add(account);
3184 }
3185 accounts.add(account);
3186 } while (data.moveToNext());
3187 if (initializedAccounts.size() > 0) {
3188 findViewById(R.id.wait).setVisibility(View.GONE);
3189 getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3190 findViewById(R.id.compose).setVisibility(View.VISIBLE);
Paul Westbrookfaa742f2012-11-01 09:50:16 -07003191 mAccounts = initializedAccounts.toArray(
3192 new Account[initializedAccounts.size()]);
3193
Mindy Pereirab199d172012-08-13 11:04:03 -07003194 finishCreate();
3195 invalidateOptionsMenu();
3196 } else {
3197 // Show "waiting"
3198 account = accounts.size() > 0 ? accounts.get(0) : null;
3199 showWaitFragment(account);
3200 }
3201 }
3202 break;
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003203 }
3204 }
3205
Mindy Pereirab199d172012-08-13 11:04:03 -07003206 private void showWaitFragment(Account account) {
3207 WaitFragment fragment = getWaitFragment();
3208 if (fragment != null) {
3209 fragment.updateAccount(account);
3210 } else {
3211 findViewById(R.id.wait).setVisibility(View.VISIBLE);
3212 replaceFragment(WaitFragment.newInstance(account, true),
3213 FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3214 }
3215 }
3216
3217 private WaitFragment getWaitFragment() {
3218 return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3219 }
3220
3221 private int replaceFragment(Fragment fragment, int transition, String tag) {
3222 FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
Mindy Pereirab199d172012-08-13 11:04:03 -07003223 fragmentTransaction.setTransition(transition);
3224 fragmentTransaction.replace(R.id.wait, fragment, tag);
3225 final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3226 return transactionId;
3227 }
3228
Mindy Pereira96a7f7a2012-07-09 16:51:06 -07003229 @Override
3230 public void onLoaderReset(Loader<Cursor> arg0) {
3231 // Do nothing.
3232 }
Paul Westbrook83e6b572013-02-05 16:22:42 -08003233
3234 @Override
3235 public Context getActivityContext() {
3236 return this;
3237 }
Andy Huang1f8f4dd2012-10-25 21:35:35 -07003238}