Merge "Fix incorrect background for change labels" into ub-gmail-ur14-dev
diff --git a/res/values/dimen.xml b/res/values/dimen.xml
index 8f7909a..791a8ab 100644
--- a/res/values/dimen.xml
+++ b/res/values/dimen.xml
@@ -114,8 +114,9 @@
<dimen name="widget_margin_bottom">0dip</dimen>
<dimen name="wait_padding">16dp</dimen>
<integer name="chips_max_lines">2</integer>
- <dimen name="tile_letter_font_size">24dp</dimen>
- <dimen name="tile_letter_font_size_small">16dp</dimen>
+ <dimen name="tile_letter_font_size_tiny">16dp</dimen>
+ <dimen name="tile_letter_font_size_small">24dp</dimen>
+ <dimen name="tile_letter_font_size_medium">40dp</dimen>
<dimen name="tile_divider_width">1dp</dimen>
<dimen name="checked_text_padding">16dip</dimen>
<dimen name="send_mail_as_padding">8dip</dimen>
diff --git a/src/com/android/mail/bitmap/ContactDrawable.java b/src/com/android/mail/bitmap/ContactDrawable.java
index 34983bf..7acb2c9 100644
--- a/src/com/android/mail/bitmap/ContactDrawable.java
+++ b/src/com/android/mail/bitmap/ContactDrawable.java
@@ -96,7 +96,7 @@
mMatrix = new Matrix();
if (sTileLetterFontSize == 0) {
- sTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size);
+ sTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size_small);
sTileFontColor = res.getColor(R.color.letter_tile_font_color);
DEFAULT_AVATAR = BitmapFactory.decodeResource(res, R.drawable.ic_generic_man);
diff --git a/src/com/android/mail/browse/ConversationViewAdapter.java b/src/com/android/mail/browse/ConversationViewAdapter.java
index c16c64d..60abb7c 100644
--- a/src/com/android/mail/browse/ConversationViewAdapter.java
+++ b/src/com/android/mail/browse/ConversationViewAdapter.java
@@ -351,6 +351,9 @@
@Override
public void setMessage(ConversationMessage message) {
mMessage = message;
+ // setMessage signifies an in-place update to the message, so let's clear out recipient
+ // summary text so the view will refresh it on the next render.
+ recipientSummaryText = null;
}
public CharSequence getTimestampShort() {
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index 9c5e486..6570cd2 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -23,11 +23,6 @@
import android.content.res.Resources;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
import android.support.v4.text.BidiFormatter;
import android.text.Html;
import android.text.Spannable;
@@ -66,6 +61,7 @@
import com.android.mail.text.EmailAddressSpan;
import com.android.mail.ui.AbstractConversationViewFragment;
import com.android.mail.ui.ImageCanvas;
+import com.android.mail.utils.BitmapUtil;
import com.android.mail.utils.LogTag;
import com.android.mail.utils.LogUtils;
import com.android.mail.utils.StyleUtils;
@@ -861,7 +857,7 @@
}
if (info.photo != null) {
- mPhotoView.setImageBitmap(frameBitmapInCircle(info.photo));
+ mPhotoView.setImageBitmap(BitmapUtil.frameBitmapInCircle(info.photo));
photoSet = true;
}
} else {
@@ -870,14 +866,14 @@
if (!photoSet) {
mPhotoView.setImageBitmap(
- frameBitmapInCircle(makeLetterTile(mSender.getPersonal(), email)));
+ BitmapUtil.frameBitmapInCircle(makeLetterTile(mSender.getPersonal(), email)));
}
}
private Bitmap makeLetterTile(
String displayName, String senderAddress) {
if (mLetterTileProvider == null) {
- mLetterTileProvider = new LetterTileProvider(getContext());
+ mLetterTileProvider = new LetterTileProvider(getContext().getResources());
}
final ImageCanvas.Dimensions dimensions = new ImageCanvas.Dimensions(
@@ -885,46 +881,6 @@
return mLetterTileProvider.getLetterTile(dimensions, displayName, senderAddress);
}
- /**
- * Frames the input bitmap in a circle.
- */
- private static Bitmap frameBitmapInCircle(Bitmap input) {
- if (input == null) {
- return null;
- }
-
- // Crop the image if not squared.
- int inputWidth = input.getWidth();
- int inputHeight = input.getHeight();
- int targetX, targetY, targetSize;
- if (inputWidth >= inputHeight) {
- targetX = inputWidth / 2 - inputHeight / 2;
- targetY = 0;
- targetSize = inputHeight;
- } else {
- targetX = 0;
- targetY = inputHeight / 2 - inputWidth / 2;
- targetSize = inputWidth;
- }
-
- // Create an output bitmap and a canvas to draw on it.
- Bitmap output = Bitmap.createBitmap(targetSize, targetSize, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(output);
-
- // Create a black paint to draw the mask.
- Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
- paint.setColor(Color.BLACK);
-
- // Draw a circle.
- canvas.drawCircle(targetSize / 2, targetSize / 2, targetSize / 2, paint);
-
- // Replace the black parts of the mask with the input image.
- paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
- canvas.drawBitmap(input, targetX /* left */, targetY /* top */, paint);
-
- return output;
- }
-
@Override
public boolean onMenuItemClick(MenuItem item) {
mPopup.dismiss();
diff --git a/src/com/android/mail/compose/ComposeActivity.java b/src/com/android/mail/compose/ComposeActivity.java
index 82116c7..e4ef81f 100644
--- a/src/com/android/mail/compose/ComposeActivity.java
+++ b/src/com/android/mail/compose/ComposeActivity.java
@@ -27,6 +27,7 @@
import android.app.FragmentTransaction;
import android.app.LoaderManager;
import android.content.ClipData;
+import android.content.ClipDescription;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
@@ -131,6 +132,7 @@
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
public class ComposeActivity extends ActionBarActivity
implements OnClickListener, ActionBar.OnNavigationListener,
@@ -265,6 +267,9 @@
// A single thread for running tasks in the background.
private static final Handler SEND_SAVE_TASK_HANDLER;
+ @VisibleForTesting
+ public static final AtomicInteger PENDING_SEND_OR_SAVE_TASKS_NUM = new AtomicInteger(0);
+
// String representing the uri of the data directory (used for attachment uri checking).
private static final String DATA_DIRECTORY_ROOT;
private static final String ALTERNATE_DATA_DIRECTORY_ROOT;
@@ -326,11 +331,6 @@
protected Bundle mInnerSavedState;
private ContentValues mExtraValues = null;
- // Array of the outstanding send or save tasks. Access is synchronized
- // with the object itself
- /* package for testing */
- @VisibleForTesting
- public final ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
// FIXME: this variable is never read. related to sRequestMessageIdMap.
private int mRequestId;
private String mSignature;
@@ -2442,201 +2442,180 @@
@VisibleForTesting
public interface SendOrSaveCallback {
- void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
+ void initializeSendOrSave();
void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
Message getMessage();
- void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
- void incrementRecipientsTimesContacted(List<String> recipients);
+ void sendOrSaveFinished(SendOrSaveMessage message, boolean success);
}
- @VisibleForTesting
- public static class SendOrSaveTask implements Runnable {
- private final Context mContext;
- @VisibleForTesting
- public final SendOrSaveCallback mSendOrSaveCallback;
- @VisibleForTesting
- public final SendOrSaveMessage mSendOrSaveMessage;
- private ReplyFromAccount mExistingDraftAccount;
-
- public SendOrSaveTask(Context context, SendOrSaveMessage message,
- SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
- mContext = context;
- mSendOrSaveCallback = callback;
- mSendOrSaveMessage = message;
- mExistingDraftAccount = draftAccount;
- }
-
- @Override
- public void run() {
- final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
-
- final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
- Message message = mSendOrSaveCallback.getMessage();
- long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
- // If a previous draft has been saved, in an account that is different
- // than what the user wants to send from, remove the old draft, and treat this
- // as a new message
- if (mExistingDraftAccount != null
- && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
- if (messageId != UIProvider.INVALID_MESSAGE_ID) {
- ContentResolver resolver = mContext.getContentResolver();
- ContentValues values = new ContentValues();
- values.put(BaseColumns._ID, messageId);
- if (mExistingDraftAccount.account.expungeMessageUri != null) {
- new ContentProviderTask.UpdateTask()
- .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
- values, null, null);
- } else {
- // TODO(mindyp) delete the conversation.
- }
- // reset messageId to 0, so a new message will be created
- messageId = UIProvider.INVALID_MESSAGE_ID;
- }
- }
-
- final long messageIdToSave = messageId;
- sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
-
- if (!sendOrSaveMessage.mSave) {
- incrementRecipientsTimesContacted(
- (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO),
- (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC),
- (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
- }
- mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
- }
-
- private void incrementRecipientsTimesContacted(
- final String toAddresses, final String ccAddresses, final String bccAddresses) {
- final List<String> recipients = Lists.newArrayList();
- addAddressesToRecipientList(recipients, toAddresses);
- addAddressesToRecipientList(recipients, ccAddresses);
- addAddressesToRecipientList(recipients, bccAddresses);
- mSendOrSaveCallback.incrementRecipientsTimesContacted(recipients);
- }
-
- private void addAddressesToRecipientList(
- final List<String> recipients, final String addressString) {
- if (recipients == null) {
- throw new IllegalArgumentException("recipientList cannot be null");
- }
- if (TextUtils.isEmpty(addressString)) {
- return;
- }
- final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
- for (final Rfc822Token token : tokens) {
- recipients.add(token.getAddress());
- }
- }
-
- /**
- * Send or Save a message.
- */
- private void sendOrSaveMessage(final long messageIdToSave,
- final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
- final ContentResolver resolver = mContext.getContentResolver();
- final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
-
- final String accountMethod = sendOrSaveMessage.mSave ?
- UIProvider.AccountCallMethods.SAVE_MESSAGE :
- UIProvider.AccountCallMethods.SEND_MESSAGE;
-
- try {
- if (updateExistingMessage) {
- sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
-
- callAccountSendSaveMethod(resolver,
- selectedAccount.account, accountMethod, sendOrSaveMessage);
+ private void runSendOrSaveProviderCalls(SendOrSaveMessage sendOrSaveMessage,
+ SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
+ final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
+ Message message = callback.getMessage();
+ long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
+ // If a previous draft has been saved, in an account that is different
+ // than what the user wants to send from, remove the old draft, and treat this
+ // as a new message
+ if (draftAccount != null
+ && !selectedAccount.account.uri.equals(draftAccount.account.uri)) {
+ if (messageId != UIProvider.INVALID_MESSAGE_ID) {
+ ContentResolver resolver = getContentResolver();
+ ContentValues values = new ContentValues();
+ values.put(BaseColumns._ID, messageId);
+ if (draftAccount.account.expungeMessageUri != null) {
+ new ContentProviderTask.UpdateTask()
+ .run(resolver, draftAccount.account.expungeMessageUri,
+ values, null, null);
} else {
- Uri messageUri = null;
- final Bundle result = callAccountSendSaveMethod(resolver,
- selectedAccount.account, accountMethod, sendOrSaveMessage);
- if (result != null) {
- // If a non-null value was returned, then the provider handled the call
- // method
- messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
- }
- if (sendOrSaveMessage.mSave && messageUri != null) {
- final Cursor messageCursor = resolver.query(messageUri,
- UIProvider.MESSAGE_PROJECTION, null, null, null);
- if (messageCursor != null) {
- try {
- if (messageCursor.moveToFirst()) {
- // Broadcast notification that a new message has
- // been allocated
- mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
- new Message(messageCursor));
- }
- } finally {
- messageCursor.close();
- }
- }
- }
+ // TODO(mindyp) delete the conversation.
}
- } finally {
- // Close any opened file descriptors
- closeOpenedAttachmentFds(sendOrSaveMessage);
+ // reset messageId to 0, so a new message will be created
+ messageId = UIProvider.INVALID_MESSAGE_ID;
}
}
- private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
- final Bundle openedFds = sendOrSaveMessage.attachmentFds();
- if (openedFds != null) {
- final Set<String> keys = openedFds.keySet();
- for (final String key : keys) {
- final ParcelFileDescriptor fd = openedFds.getParcelable(key);
- if (fd != null) {
+ final long messageIdToSave = messageId;
+ sendOrSaveMessage(callback, messageIdToSave, sendOrSaveMessage, selectedAccount);
+
+ if (!sendOrSaveMessage.mSave) {
+ incrementRecipientsTimesContacted(
+ (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO),
+ (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC),
+ (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
+ }
+ callback.sendOrSaveFinished(sendOrSaveMessage, true);
+ }
+
+ private void incrementRecipientsTimesContacted(
+ final String toAddresses, final String ccAddresses, final String bccAddresses) {
+ final List<String> recipients = Lists.newArrayList();
+ addAddressesToRecipientList(recipients, toAddresses);
+ addAddressesToRecipientList(recipients, ccAddresses);
+ addAddressesToRecipientList(recipients, bccAddresses);
+ incrementRecipientsTimesContacted(recipients);
+ }
+
+ private void addAddressesToRecipientList(
+ final List<String> recipients, final String addressString) {
+ if (recipients == null) {
+ throw new IllegalArgumentException("recipientList cannot be null");
+ }
+ if (TextUtils.isEmpty(addressString)) {
+ return;
+ }
+ final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
+ for (final Rfc822Token token : tokens) {
+ recipients.add(token.getAddress());
+ }
+ }
+
+ /**
+ * Send or Save a message.
+ */
+ private void sendOrSaveMessage(SendOrSaveCallback callback, final long messageIdToSave,
+ final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
+ final ContentResolver resolver = getContentResolver();
+ final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
+
+ final String accountMethod = sendOrSaveMessage.mSave ?
+ UIProvider.AccountCallMethods.SAVE_MESSAGE :
+ UIProvider.AccountCallMethods.SEND_MESSAGE;
+
+ try {
+ if (updateExistingMessage) {
+ sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
+
+ callAccountSendSaveMethod(resolver,
+ selectedAccount.account, accountMethod, sendOrSaveMessage);
+ } else {
+ Uri messageUri = null;
+ final Bundle result = callAccountSendSaveMethod(resolver,
+ selectedAccount.account, accountMethod, sendOrSaveMessage);
+ if (result != null) {
+ // If a non-null value was returned, then the provider handled the call
+ // method
+ messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
+ }
+ if (sendOrSaveMessage.mSave && messageUri != null) {
+ final Cursor messageCursor = resolver.query(messageUri,
+ UIProvider.MESSAGE_PROJECTION, null, null, null);
+ if (messageCursor != null) {
try {
- fd.close();
- } catch (IOException e) {
- // Do nothing
+ if (messageCursor.moveToFirst()) {
+ // Broadcast notification that a new message has
+ // been allocated
+ callback.notifyMessageIdAllocated(sendOrSaveMessage,
+ new Message(messageCursor));
+ }
+ } finally {
+ messageCursor.close();
}
}
}
}
+ } finally {
+ // Close any opened file descriptors
+ closeOpenedAttachmentFds(sendOrSaveMessage);
}
+ }
- /**
- * Use the {@link ContentResolver#call} method to send or save the message.
- *
- * If this was successful, this method will return an non-null Bundle instance
- */
- private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
- final Account account, final String method,
- final SendOrSaveMessage sendOrSaveMessage) {
- // Copy all of the values from the content values to the bundle
- final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
- final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
-
- for (Entry<String, Object> entry : valueSet) {
- final Object entryValue = entry.getValue();
- final String key = entry.getKey();
- if (entryValue instanceof String) {
- methodExtras.putString(key, (String)entryValue);
- } else if (entryValue instanceof Boolean) {
- methodExtras.putBoolean(key, (Boolean)entryValue);
- } else if (entryValue instanceof Integer) {
- methodExtras.putInt(key, (Integer)entryValue);
- } else if (entryValue instanceof Long) {
- methodExtras.putLong(key, (Long)entryValue);
- } else {
- LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
- entryValue.getClass().getName());
+ private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
+ final Bundle openedFds = sendOrSaveMessage.attachmentFds();
+ if (openedFds != null) {
+ final Set<String> keys = openedFds.keySet();
+ for (final String key : keys) {
+ final ParcelFileDescriptor fd = openedFds.getParcelable(key);
+ if (fd != null) {
+ try {
+ fd.close();
+ } catch (IOException e) {
+ // Do nothing
+ }
}
}
-
- // If the SendOrSaveMessage has some opened fds, add them to the bundle
- final Bundle fdMap = sendOrSaveMessage.attachmentFds();
- if (fdMap != null) {
- methodExtras.putParcelable(
- UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
- }
-
- return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
}
}
/**
+ * Use the {@link ContentResolver#call} method to send or save the message.
+ *
+ * If this was successful, this method will return an non-null Bundle instance
+ */
+ private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
+ final Account account, final String method,
+ final SendOrSaveMessage sendOrSaveMessage) {
+ // Copy all of the values from the content values to the bundle
+ final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
+ final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
+
+ for (Entry<String, Object> entry : valueSet) {
+ final Object entryValue = entry.getValue();
+ final String key = entry.getKey();
+ if (entryValue instanceof String) {
+ methodExtras.putString(key, (String)entryValue);
+ } else if (entryValue instanceof Boolean) {
+ methodExtras.putBoolean(key, (Boolean)entryValue);
+ } else if (entryValue instanceof Integer) {
+ methodExtras.putInt(key, (Integer)entryValue);
+ } else if (entryValue instanceof Long) {
+ methodExtras.putLong(key, (Long)entryValue);
+ } else {
+ LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
+ entryValue.getClass().getName());
+ }
+ }
+
+ // If the SendOrSaveMessage has some opened fds, add them to the bundle
+ final Bundle fdMap = sendOrSaveMessage.attachmentFds();
+ if (fdMap != null) {
+ methodExtras.putParcelable(
+ UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
+ }
+
+ return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
+ }
+
+ /**
* Reports recipients that have been contacted in order to improve auto-complete
* suggestions. Default behavior updates usage statistics in ContactsProvider.
* @param recipients addresses
@@ -2657,14 +2636,20 @@
private final Bundle mAttachmentFds;
public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
- String refMessageId, List<Attachment> attachments, boolean save) {
+ String refMessageId, List<Attachment> attachments, Bundle optionalAttachmentFds,
+ boolean save) {
mAccount = account;
mValues = values;
mRefMessageId = refMessageId;
mSave = save;
mRequestId = mValues.hashCode() ^ hashCode();
- mAttachmentFds = initializeAttachmentFds(context, attachments);
+ // If the attachments are already open for us (pre-JB), then don't open them again
+ if (optionalAttachmentFds != null) {
+ mAttachmentFds = optionalAttachmentFds;
+ } else {
+ mAttachmentFds = initializeAttachmentFds(context, attachments);
+ }
}
int requestId() {
@@ -2674,54 +2659,54 @@
Bundle attachmentFds() {
return mAttachmentFds;
}
+ }
- /**
- * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
- * called before the ComposeActivity finishes.
- * Note: The caller is responsible for closing these file descriptors.
- */
- private static Bundle initializeAttachmentFds(final Context context,
- final List<Attachment> attachments) {
- if (attachments == null || attachments.size() == 0) {
- return null;
- }
-
- final Bundle result = new Bundle(attachments.size());
- final ContentResolver resolver = context.getContentResolver();
-
- for (Attachment attachment : attachments) {
- if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
- continue;
- }
-
- ParcelFileDescriptor fileDescriptor;
- try {
- fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
- } catch (FileNotFoundException e) {
- LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
- fileDescriptor = null;
- } catch (SecurityException e) {
- // We have encountered a security exception when attempting to open the file
- // specified by the content uri. If the attachment has been cached, this
- // isn't a problem, as even through the original permission may have been
- // revoked, we have cached the file. This will happen when saving/sending
- // a previously saved draft.
- // TODO(markwei): Expose whether the attachment has been cached through the
- // attachment object. This would allow us to limit when the log is made, as
- // if the attachment has been cached, this really isn't an error
- LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
- // Just set the file descriptor to null, as the underlying provider needs
- // to handle the file descriptor not being set.
- fileDescriptor = null;
- }
-
- if (fileDescriptor != null) {
- result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
- }
- }
-
- return result;
+ /**
+ * Opens {@link ParcelFileDescriptor} for each of the attachments. This method must be
+ * called before the ComposeActivity finishes.
+ * Note: The caller is responsible for closing these file descriptors.
+ */
+ private static Bundle initializeAttachmentFds(final Context context,
+ final List<Attachment> attachments) {
+ if (attachments == null || attachments.size() == 0) {
+ return null;
}
+
+ final Bundle result = new Bundle(attachments.size());
+ final ContentResolver resolver = context.getContentResolver();
+
+ for (Attachment attachment : attachments) {
+ if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
+ continue;
+ }
+
+ ParcelFileDescriptor fileDescriptor;
+ try {
+ fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
+ } catch (FileNotFoundException e) {
+ LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
+ fileDescriptor = null;
+ } catch (SecurityException e) {
+ // We have encountered a security exception when attempting to open the file
+ // specified by the content uri. If the attachment has been cached, this
+ // isn't a problem, as even through the original permission may have been
+ // revoked, we have cached the file. This will happen when saving/sending
+ // a previously saved draft.
+ // TODO(markwei): Expose whether the attachment has been cached through the
+ // attachment object. This would allow us to limit when the log is made, as
+ // if the attachment has been cached, this really isn't an error
+ LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
+ // Just set the file descriptor to null, as the underlying provider needs
+ // to handle the file descriptor not being set.
+ fileDescriptor = null;
+ }
+
+ if (fileDescriptor != null) {
+ result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
+ }
+ }
+
+ return result;
}
/**
@@ -3110,8 +3095,9 @@
private int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
Message message, final Message refMessage, final CharSequence quotedText,
- SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
- ReplyFromAccount draftAccount, final ContentValues extraValues) {
+ SendOrSaveCallback callback, boolean save, int composeMode,
+ ReplyFromAccount draftAccount, final ContentValues extraValues,
+ Bundle optionalAttachmentFds) {
final ContentValues values = new ContentValues();
final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
@@ -3185,15 +3171,11 @@
if (extraValues != null) {
values.putAll(extraValues);
}
- SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
- values, refMessageId, message.getAttachments(), save);
- SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
- draftAccount);
- callback.initializeSendOrSave(sendOrSaveTask);
- // Do the send/save action on the specified handler to avoid possible
- // ANRs
- handler.post(sendOrSaveTask);
+ SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
+ values, refMessageId, message.getAttachments(), optionalAttachmentFds, save);
+ runSendOrSaveProviderCalls(sendOrSaveMessage, callback, draftAccount);
+
LogUtils.i(LOG_TAG, "[compose] SendOrSaveMessage [%s] posted (isSave: %s) - " +
"body length: %d, attachment count: %d",
sendOrSaveMessage.requestId(), save, message.bodyText.length(),
@@ -3267,19 +3249,40 @@
private int mRestoredRequestId;
@Override
- public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
- synchronized (mActiveTasks) {
- int numTasks = mActiveTasks.size();
- if (numTasks == 0) {
+ public void initializeSendOrSave() {
+ final Intent i = new Intent(ComposeActivity.this, EmptyService.class);
+
+ // API 16+ allows for setClipData. For pre-16 we are going to open the fds
+ // on the main thread.
+ if (Utils.isRunningJellybeanOrLater()) {
+ // Grant the READ permission for the attachments to the service so that
+ // as long as the service stays alive we won't hit PermissionExceptions.
+ final ClipDescription desc = new ClipDescription("attachment_uris",
+ new String[]{ClipDescription.MIMETYPE_TEXT_URILIST});
+ ClipData clipData = null;
+ for (Attachment a : mAttachmentsView.getAttachments()) {
+ if (a != null && !Utils.isEmpty(a.contentUri)) {
+ final ClipData.Item uriItem = new ClipData.Item(a.contentUri);
+ if (clipData == null) {
+ clipData = new ClipData(desc, uriItem);
+ } else {
+ clipData.addItem(uriItem);
+ }
+ }
+ }
+ i.setClipData(clipData);
+ i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+
+ synchronized (PENDING_SEND_OR_SAVE_TASKS_NUM) {
+ if (PENDING_SEND_OR_SAVE_TASKS_NUM.getAndAdd(1) == 0) {
// Start service so we won't be killed if this app is
// put in the background.
- startService(new Intent(ComposeActivity.this, EmptyService.class));
+ startService(i);
}
-
- mActiveTasks.add(sendOrSaveTask);
}
if (sTestSendOrSaveCallback != null) {
- sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
+ sTestSendOrSaveCallback.initializeSendOrSave();
}
}
@@ -3309,7 +3312,7 @@
}
@Override
- public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
+ public void sendOrSaveFinished(SendOrSaveMessage message, boolean success) {
// Update the last sent from account.
if (mAccount != null) {
MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
@@ -3325,37 +3328,37 @@
.show();
}
- int numTasks;
- synchronized (mActiveTasks) {
- // Remove the task from the list of active tasks
- mActiveTasks.remove(task);
- numTasks = mActiveTasks.size();
- }
-
- if (numTasks == 0) {
- // Stop service so we can be killed.
- stopService(new Intent(ComposeActivity.this, EmptyService.class));
+ synchronized (PENDING_SEND_OR_SAVE_TASKS_NUM) {
+ if (PENDING_SEND_OR_SAVE_TASKS_NUM.addAndGet(-1) == 0) {
+ // Stop service so we can be killed.
+ stopService(new Intent(ComposeActivity.this, EmptyService.class));
+ }
}
if (sTestSendOrSaveCallback != null) {
- sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
+ sTestSendOrSaveCallback.sendOrSaveFinished(message, success);
}
}
-
- @Override
- public void incrementRecipientsTimesContacted(final List<String> recipients) {
- ComposeActivity.this.incrementRecipientsTimesContacted(recipients);
- }
};
setAccount(mReplyFromAccount.account);
final Spanned body = removeComposingSpans(mBodyView.getText());
+ callback.initializeSendOrSave();
+
+ // For pre-JB we need to open the fds on the main thread
+ final Bundle attachmentFds;
+ if (!Utils.isRunningJellybeanOrLater()) {
+ attachmentFds = initializeAttachmentFds(this, mAttachmentsView.getAttachments());
+ } else {
+ attachmentFds = null;
+ }
+
SEND_SAVE_TASK_HANDLER.post(new Runnable() {
@Override
public void run() {
final Message msg = createMessage(mReplyFromAccount, mRefMessage, getMode(), body);
mRequestId = sendOrSaveInternal(ComposeActivity.this, mReplyFromAccount, msg,
mRefMessage, mQuotedTextView.getQuotedTextIfIncluded(), callback,
- SEND_SAVE_TASK_HANDLER, save, mComposeMode, mDraftAccount, mExtraValues);
+ save, mComposeMode, mDraftAccount, mExtraValues, attachmentFds);
}
});
diff --git a/src/com/android/mail/photomanager/LetterTileProvider.java b/src/com/android/mail/photomanager/LetterTileProvider.java
index ca70e72..3aaf7b1 100644
--- a/src/com/android/mail/photomanager/LetterTileProvider.java
+++ b/src/com/android/mail/photomanager/LetterTileProvider.java
@@ -16,9 +16,7 @@
package com.android.mail.photomanager;
-import android.content.Context;
import android.content.res.Resources;
-import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
@@ -59,13 +57,15 @@
private final char[] mFirstChar = new char[1];
private static final int POSSIBLE_BITMAP_SIZES = 3;
- private ColorPicker sTileColorPicker;
+ private final ColorPicker mTileColorPicker;
- public LetterTileProvider(Context context) {
- final Resources res = context.getResources();
- mTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size);
- mTileLetterFontSizeSmall = res
- .getDimensionPixelSize(R.dimen.tile_letter_font_size_small);
+ public LetterTileProvider(Resources res) {
+ this(res, new ColorPicker.PaletteColorPicker(res));
+ }
+
+ public LetterTileProvider(Resources res, ColorPicker colorPicker) {
+ mTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size_small);
+ mTileLetterFontSizeSmall = res.getDimensionPixelSize(R.dimen.tile_letter_font_size_tiny);
mTileFontColor = res.getColor(R.color.letter_tile_font_color);
mSansSerifLight = Typeface.create("sans-serif-light", Typeface.NORMAL);
mBounds = new Rect();
@@ -78,9 +78,7 @@
mDefaultBitmap = BitmapFactory.decodeResource(res, R.drawable.ic_generic_man);
mDefaultBitmapCache = new Bitmap[POSSIBLE_BITMAP_SIZES];
- if (sTileColorPicker == null) {
- sTileColorPicker = new ColorPicker.PaletteColorPicker(res);
- }
+ mTileColorPicker = colorPicker;
}
public Bitmap getLetterTile(final Dimensions dimensions, final String displayName,
@@ -98,13 +96,14 @@
final Canvas c = mCanvas;
c.setBitmap(bitmap);
- c.drawColor(sTileColorPicker.pickColor(address));
+ c.drawColor(mTileColorPicker.pickColor(address));
// If its a valid English alphabet letter,
// draw the letter on top of the color
if (isEnglishLetterOrDigit(firstChar)) {
mFirstChar[0] = Character.toUpperCase(firstChar);
- mPaint.setTextSize(getFontSize(dimensions.scale));
+ mPaint.setTextSize(
+ dimensions.fontSize > 0 ? dimensions.fontSize : getFontSize(dimensions.scale));
mPaint.getTextBounds(mFirstChar, 0, 1, mBounds);
c.drawText(mFirstChar, 0, 1, 0 + dimensions.width / 2,
0 + dimensions.height / 2 + (mBounds.bottom - mBounds.top) / 2, mPaint);
diff --git a/src/com/android/mail/ui/ActionBarController.java b/src/com/android/mail/ui/ActionBarController.java
index 34a1083..a9dc371 100644
--- a/src/com/android/mail/ui/ActionBarController.java
+++ b/src/com/android/mail/ui/ActionBarController.java
@@ -247,6 +247,7 @@
}
public boolean onPrepareOptionsMenu(Menu menu) {
+ menu.setQwertyMode(true);
// We start out with every option enabled. Based on the current view, we disable actions
// that are possible.
LogUtils.d(LOG_TAG, "ActionBarView.onPrepareOptionsMenu().");
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index edbc6b7..a2ae0c9 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -654,10 +654,12 @@
keyCode == KeyEvent.KEYCODE_DPAD_DOWN) &&
keyEvent.getAction() == KeyEvent.ACTION_UP) {
final int position = list.getSelectedItemPosition();
- final Object item = getAnimatedAdapter().getItem(position);
- if (item != null && item instanceof ConversationCursor) {
- final Conversation conv = ((ConversationCursor) item).getConversation();
- mCallbacks.onConversationFocused(conv);
+ if (position >= 0) {
+ final Object item = getAnimatedAdapter().getItem(position);
+ if (item != null && item instanceof ConversationCursor) {
+ final Conversation conv = ((ConversationCursor) item).getConversation();
+ mCallbacks.onConversationFocused(conv);
+ }
}
}
}
diff --git a/src/com/android/mail/ui/ImageCanvas.java b/src/com/android/mail/ui/ImageCanvas.java
index 9fdf0ae..b19b3e8 100644
--- a/src/com/android/mail/ui/ImageCanvas.java
+++ b/src/com/android/mail/ui/ImageCanvas.java
@@ -31,6 +31,7 @@
public int width;
public int height;
public float scale;
+ public float fontSize;
public static final float SCALE_ONE = 1.0f;
public static final float SCALE_HALF = 0.5f;
@@ -40,9 +41,14 @@
}
public Dimensions(int w, int h, float s) {
- width = w;
- height = h;
- scale = s;
+ this(w, h, s, -1f);
+ }
+
+ public Dimensions(int width, int height, float scale, float fontSize) {
+ this.width = width;
+ this.height = height;
+ this.scale = scale;
+ this.fontSize = fontSize;
}
@Override
diff --git a/src/com/android/mail/utils/BitmapUtil.java b/src/com/android/mail/utils/BitmapUtil.java
index 6f61f56..7cde938 100644
--- a/src/com/android/mail/utils/BitmapUtil.java
+++ b/src/com/android/mail/utils/BitmapUtil.java
@@ -17,7 +17,12 @@
package com.android.mail.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
/**
* Provides static functions to decode bitmaps at the optimal size
@@ -172,4 +177,43 @@
return cropped;
}
+ /**
+ * Frames the input bitmap in a circle.
+ */
+ public static Bitmap frameBitmapInCircle(Bitmap input) {
+ if (input == null) {
+ return null;
+ }
+
+ // Crop the image if not squared.
+ int inputWidth = input.getWidth();
+ int inputHeight = input.getHeight();
+ int targetX, targetY, targetSize;
+ if (inputWidth >= inputHeight) {
+ targetX = inputWidth / 2 - inputHeight / 2;
+ targetY = 0;
+ targetSize = inputHeight;
+ } else {
+ targetX = 0;
+ targetY = inputHeight / 2 - inputWidth / 2;
+ targetSize = inputWidth;
+ }
+
+ // Create an output bitmap and a canvas to draw on it.
+ Bitmap output = Bitmap.createBitmap(targetSize, targetSize, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(output);
+
+ // Create a black paint to draw the mask.
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setColor(Color.BLACK);
+
+ // Draw a circle.
+ canvas.drawCircle(targetSize / 2, targetSize / 2, targetSize / 2, paint);
+
+ // Replace the black parts of the mask with the input image.
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+ canvas.drawBitmap(input, targetX /* left */, targetY /* top */, paint);
+
+ return output;
+ }
}
diff --git a/src/com/android/mail/utils/NotificationUtils.java b/src/com/android/mail/utils/NotificationUtils.java
index 58bf0ac..0b19005 100644
--- a/src/com/android/mail/utils/NotificationUtils.java
+++ b/src/com/android/mail/utils/NotificationUtils.java
@@ -26,11 +26,6 @@
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Rect;
import android.net.Uri;
import android.os.Looper;
import android.provider.ContactsContract;
@@ -1693,13 +1688,13 @@
final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight,
Dimensions.SCALE_ONE);
- contactIconInfo.icon = new LetterTileProvider(context).getLetterTile(dimensions,
- displayName, senderAddress);
+ contactIconInfo.icon = new LetterTileProvider(context.getResources())
+ .getLetterTile(dimensions, displayName, senderAddress);
}
// Only turn the square photo/letter tile into a circle for L and later
if (Utils.isRunningLOrLater()) {
- contactIconInfo.icon = cropSquareIconToCircle(contactIconInfo.icon);
+ contactIconInfo.icon = BitmapUtil.frameBitmapInCircle(contactIconInfo.icon);
}
}
@@ -1787,28 +1782,6 @@
return contactIconInfo;
}
- /**
- * Crop a square bitmap into a circular one. Used for both contact photos and letter tiles.
- * @param icon Square bitmap to crop
- * @return Circular bitmap
- */
- private static Bitmap cropSquareIconToCircle(Bitmap icon) {
- final int iconWidth = icon.getWidth();
- final Bitmap newIcon = Bitmap.createBitmap(iconWidth, iconWidth, Bitmap.Config.ARGB_8888);
- final Canvas canvas = new Canvas(newIcon);
- final Paint paint = new Paint();
- final Rect rect = new Rect(0, 0, icon.getWidth(),
- icon.getHeight());
-
- paint.setAntiAlias(true);
- canvas.drawARGB(0, 0, 0, 0);
- canvas.drawCircle(iconWidth/2, iconWidth/2, iconWidth/2, paint);
- paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
- canvas.drawBitmap(icon, rect, rect, paint);
-
- return newIcon;
- }
-
private static String getMessageBodyWithoutElidedText(final Message message) {
return getMessageBodyWithoutElidedText(message.getBodyAsHtml());
}