attachment UI in conversation view
Load from a message's attachment list URI to render initial
attachment views. Each attachment view then monitors its own
URI for changes when it expects them (this is quite likely
too granular!).
Change-Id: Ie559672e63910034e4dbf7766101a2b5768129aa
diff --git a/res/layout/conversation_message_attachment.xml b/res/layout/conversation_message_attachment.xml
new file mode 100644
index 0000000..db9463f
--- /dev/null
+++ b/res/layout/conversation_message_attachment.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Google Inc.
+ Licensed to The Android Open Source Project.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.mail.browse.MessageHeaderAttachment xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:orientation="vertical"
+ android:divider="?android:attr/dividerHorizontal"
+ android:showDividers="middle"
+ android:background="@drawable/attachment_bg_holo">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="48dp">
+
+ <ImageView
+ android:id="@+id/attachment_icon"
+ android:layout_width="48dp"
+ android:layout_height="match_parent"
+ android:scaleType="fitCenter"
+ android:background="#e5e5e5" />
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_toRightOf="@id/attachment_icon"
+ android:gravity="center_vertical"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp">
+
+ <TextView
+ android:id="@+id/attachment_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:singleLine="true" />
+
+ <TextView
+ android:id="@+id/attachment_subtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/attachment_title"
+ android:singleLine="true" />
+
+ <ProgressBar
+ android:id="@+id/attachment_progress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/attachment_title"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:indeterminate="false"
+ android:visibility="gone" />
+
+ </RelativeLayout>
+
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/attachment_buttons"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:divider="?android:attr/dividerVertical"
+ android:showDividers="middle"
+ android:dividerPadding="8dp"
+ android:orientation="horizontal">
+ <include layout="@layout/conversation_message_attachment_buttons" />
+ </LinearLayout>
+
+</com.android.mail.browse.MessageHeaderAttachment>
diff --git a/res/layout/conversation_message_attachment_buttons.xml b/res/layout/conversation_message_attachment_buttons.xml
new file mode 100644
index 0000000..9ef8504
--- /dev/null
+++ b/res/layout/conversation_message_attachment_buttons.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Google Inc.
+ Licensed to The Android Open Source Project.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <Button
+ android:id="@+id/preview_attachment"
+ android:text="@string/preview_attachment"
+ style="@style/message_attachment_button" />
+
+ <Button
+ android:id="@+id/view_attachment"
+ android:text="@string/view_attachment"
+ style="@style/message_attachment_button" />
+
+ <Button
+ android:id="@+id/save_attachment"
+ android:text="@string/save_attachment"
+ style="@style/message_attachment_button" />
+
+ <Button
+ android:id="@+id/info_attachment"
+ android:text="@string/info_attachment"
+ style="@style/message_attachment_button" />
+
+ <Button
+ android:id="@+id/play_attachment"
+ android:text="@string/play_attachment"
+ style="@style/message_attachment_button" />
+
+ <Button
+ android:id="@+id/install_attachment"
+ android:text="@string/install_attachment"
+ style="@style/message_attachment_button" />
+
+ <Button
+ android:id="@+id/cancel_attachment"
+ android:text="@string/cancel_attachment"
+ style="@style/message_attachment_button" />
+
+</merge>
diff --git a/res/layout/conversation_message_attachments.xml b/res/layout/conversation_message_attachments.xml
new file mode 100644
index 0000000..6bf6862
--- /dev/null
+++ b/res/layout/conversation_message_attachments.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 Google Inc.
+ Licensed to The Android Open Source Project.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/attachments"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginTop="8dp"
+ android:layout_marginLeft="16dp"
+ android:layout_marginRight="16dp">
+</LinearLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9762ae1..1a926c7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -198,6 +198,34 @@
<string name="attachment_application_pdf">PDF</string>
<!-- Attachment description for unknown files [CHAR LIMIT=30]-->
<string name="attachment_unknown"><xliff:g id="attachmentExtension">%s</xliff:g> File</string>
+ <!-- Read email screen, button name. Preview an attachment by Gview. [CHAR LIMIT=10] -->
+ <string name="preview_attachment">Preview</string>
+ <!-- Read email screen, button name. View an attachment by an application on device. [CHAR LIMIT=10] -->
+ <string name="view_attachment">View</string>
+ <!-- Read email screen, button name. Save an attachment to sd card. [CHAR LIMIT=10] -->
+ <string name="save_attachment">Save</string>
+ <!-- Read email screen, button name. Open a dialog to explain that no application found to view the attachment. [CHAR LIMIT=10] -->
+ <string name="info_attachment">Info</string>
+ <!-- Read email screen, button name. Play a video or audio attachment. [CHAR LIMIT=10] -->
+ <string name="play_attachment">Play</string>
+ <!-- Read email screen, button name. Install an apk attachment. [CHAR LIMIT=10] -->
+ <string name="install_attachment">Install</string>
+ <!-- Read email screen, button name. Cancel a downloading attachment. [CHAR LIMIT=10] -->
+ <string name="cancel_attachment">Cancel</string>
+ <!-- Dialog box title [CHAR LIMIT=30] -->
+ <string name="more_info_attachment">Info</string>
+ <!-- Dialog box message, displayed when we block downloading an attachment due to security concerns. [CHAR LIMIT=200]-->
+ <string name="attachment_type_blocked">Can\'t save or open this type of attachment because it could contain malicious software.</string>
+ <!-- Dialog box message, displayed when we could not view an attachment. [CHAR LIMIT=200]-->
+ <string name="no_application_found">No app can open this attachment for viewing.</string>
+ <!-- Dialog box title. [CHAR LIMIT=30] -->
+ <string name="fetching_attachment">Fetching attachment</string>
+ <!-- Dialog box message. [CHAR LIMIT=80] -->
+ <string name="please_wait">Please wait\u2026</string>
+ <!-- Displayed in the conversation view. Status of a saved attachment. [CHAR LIMIT=20]-->
+ <string name="saved">Saved, </string>
+ <!-- Displayed in the conversation view. Status of a failed attachment. [CHAR LIMIT=20]-->
+ <string name="download_failed">Couldn\'t download.</string>
<!-- Webview Context Menu Strings -->
<!-- Title of dialog for choosing which activity to share a link with. [CHAR LIMIT=50]-->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index aea2bf7..b923623 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -306,6 +306,20 @@
<item name="android:src">@drawable/ic_menu_expander_maximized_holo_light</item>
<item name="android:contentDescription">@string/collapse_recipient_details</item>
</style>
+ <style name="message_attachment_button_base" parent="@android:style/Widget.Holo.Button.Borderless">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:minHeight">0dip</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
+ <item name="android:textStyle">bold</item>
+ <item name="android:textColor">#777777</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textAllCaps">true</item>
+ </style>
+ <style name="message_attachment_button" parent="message_attachment_button_base">
+ <item name="android:padding">8dp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_weight">1</item>
+ </style>
<!-- End conversation view message header styles -->
<!-- Folder styles -->
diff --git a/src/com/android/mail/browse/AttachmentLoader.java b/src/com/android/mail/browse/AttachmentLoader.java
new file mode 100644
index 0000000..252c6ba
--- /dev/null
+++ b/src/com/android/mail/browse/AttachmentLoader.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.browse;
+
+import com.google.common.collect.Maps;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.net.Uri;
+
+import com.android.mail.providers.Attachment;
+import com.android.mail.providers.UIProvider;
+
+import java.util.Map;
+
+public class AttachmentLoader extends CursorLoader {
+
+ public AttachmentLoader(Context c, Uri uri) {
+ super(c, uri, UIProvider.ATTACHMENT_PROJECTION, null, null, null);
+ }
+
+ @Override
+ public Cursor loadInBackground() {
+ return new AttachmentCursor(super.loadInBackground());
+ }
+
+ public static class AttachmentCursor extends CursorWrapper {
+
+ private Map<Long, Attachment> mCache = Maps.newHashMap();
+
+ private AttachmentCursor(Cursor inner) {
+ super(inner);
+ }
+
+ public Attachment get() {
+ long id = getWrappedCursor().getLong(0);
+ Attachment m = mCache.get(id);
+ if (m == null) {
+ m = new Attachment(this);
+ mCache.put(id, m);
+ }
+ return m;
+ }
+ }
+}
diff --git a/src/com/android/mail/browse/MessageHeaderAttachment.java b/src/com/android/mail/browse/MessageHeaderAttachment.java
new file mode 100644
index 0000000..20cc845
--- /dev/null
+++ b/src/com/android/mail/browse/MessageHeaderAttachment.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2012 Google Inc.
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.browse;
+
+import android.app.AlertDialog;
+import android.app.LoaderManager;
+import android.app.ProgressDialog;
+import android.content.ActivityNotFoundException;
+import android.content.AsyncQueryHandler;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.android.mail.R;
+import com.android.mail.browse.AttachmentLoader.AttachmentCursor;
+import com.android.mail.providers.Attachment;
+import com.android.mail.providers.UIProvider.AttachmentColumns;
+import com.android.mail.providers.UIProvider.AttachmentDestination;
+import com.android.mail.providers.UIProvider.AttachmentState;
+import com.android.mail.utils.AttachmentUtils;
+import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.MimeType;
+import com.android.mail.utils.Utils;
+
+/**
+ * View for a single attachment in conversation view. Shows download status and allows launching
+ * intents to act on an attachment.
+ *
+ */
+public class MessageHeaderAttachment extends LinearLayout implements OnClickListener,
+ OnMenuItemClickListener, DialogInterface.OnCancelListener,
+ DialogInterface.OnDismissListener, LoaderManager.LoaderCallbacks<Cursor> {
+
+ private LoaderManager mLoaderManager;
+ private Attachment mAttachment;
+ private ImageView mIcon;
+ private TextView mTitle;
+ private TextView mSubTitle;
+ private String mAttachmentSizeText;
+ private String mDisplayType;
+ private ProgressDialog mViewProgressDialog;
+ private AttachmentCommandHandler mCommandHandler;
+ private ProgressBar mProgress;
+ private Button mPreviewButton;
+ private Button mViewButton;
+ private Button mSaveButton;
+ private Button mInfoButton;
+ private Button mPlayButton;
+ private Button mInstallButton;
+ private Button mCancelButton;
+
+ private static final String LOG_TAG = new LogUtils().getLogTag();
+
+ private class AttachmentCommandHandler extends AsyncQueryHandler {
+
+ public AttachmentCommandHandler() {
+ super(getContext().getContentResolver());
+ }
+
+ /**
+ * Asynchronously begin an update() on a ContentProvider and initialize a loader to watch
+ * for resulting changes on this attachment.
+ *
+ */
+ public void sendCommand(ContentValues params) {
+ startUpdate(0, null, mAttachment.uri, params, null, null);
+ mLoaderManager.initLoader(mAttachment.uri.hashCode(), Bundle.EMPTY,
+ MessageHeaderAttachment.this);
+ }
+
+ }
+
+ public MessageHeaderAttachment(Context context) {
+ super(context);
+ }
+
+ public MessageHeaderAttachment(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mCommandHandler = new AttachmentCommandHandler();
+ }
+
+ public static MessageHeaderAttachment inflate(LayoutInflater inflater, ViewGroup parent,
+ LoaderManager loaderManager) {
+ MessageHeaderAttachment view = (MessageHeaderAttachment) inflater.inflate(
+ R.layout.conversation_message_attachment, parent, false);
+
+ view.mLoaderManager = loaderManager;
+
+ return view;
+ }
+
+ /**
+ * Render most of the UI using given immutable attachment properties. This happens immediately
+ * upon instantiation.
+ *
+ */
+ public void render(Attachment attachment) {
+ mAttachment = attachment;
+
+ mTitle.setText(attachment.name);
+
+ mAttachmentSizeText = AttachmentUtils.convertToHumanReadableSize(getContext(),
+ attachment.size);
+ mDisplayType = AttachmentUtils.getDisplayType(getContext(), attachment);
+ updateSubtitleText(null);
+
+ if (mAttachment.isImage() && mAttachment.thumbnailUri != null) {
+ // FIXME: this decodes on the UI thread. Also, it doesn't handle large images, so
+ // using the full image is out of the question.
+ mIcon.setImageURI(mAttachment.thumbnailUri);
+ }
+ if (mIcon.getDrawable() == null) {
+ // not an image, or image load failed. fall back to default.
+ mIcon.setImageResource(R.drawable.ic_menu_attachment_holo_light);
+ mIcon.setScaleType(ImageView.ScaleType.CENTER);
+ }
+
+ mProgress.setMax(attachment.size);
+
+ updateActions();
+
+ if (mAttachment.isDownloading()) {
+ mLoaderManager.initLoader(mAttachment.uri.hashCode(), Bundle.EMPTY, this);
+ // TODO: clean up loader when the view is detached
+ }
+ }
+
+ private void updateStatus(Attachment newAttachment) {
+
+ LogUtils.d(LOG_TAG, "got attachment update: uri=%s dled=%d state=%d contentUri=%s MIME=%s",
+ newAttachment.uri, newAttachment.downloadedSize, newAttachment.state,
+ newAttachment.contentUri, newAttachment.mimeType);
+
+ mAttachment = newAttachment;
+
+ final boolean showProgress = newAttachment.size > 0 && newAttachment.downloadedSize > 0
+ && newAttachment.downloadedSize < newAttachment.size;
+
+ if (mViewProgressDialog != null && mViewProgressDialog.isShowing()) {
+ mViewProgressDialog.setProgress(newAttachment.downloadedSize);
+ mViewProgressDialog.setIndeterminate(showProgress);
+
+ if (!newAttachment.isDownloading()) {
+ mViewProgressDialog.dismiss();
+ }
+
+ if (newAttachment.state == AttachmentState.SAVED) {
+ sendViewIntent();
+ }
+ } else {
+
+ if (newAttachment.isDownloading()) {
+ mProgress.setProgress(newAttachment.downloadedSize);
+ setProgressVisible(true);
+ } else {
+ setProgressVisible(false);
+ }
+
+ }
+
+ if (newAttachment.state == AttachmentState.FAILED) {
+ mSubTitle.setText(getResources().getString(R.string.download_failed));
+ } else {
+ updateSubtitleText(newAttachment.isSavedToExternal() ?
+ getResources().getString(R.string.saved) : null);
+ }
+
+ updateActions();
+ }
+
+ private void setProgressVisible(boolean visible) {
+ if (visible) {
+ mProgress.setVisibility(VISIBLE);
+ mSubTitle.setVisibility(INVISIBLE);
+ } else {
+ mProgress.setVisibility(GONE);
+ mSubTitle.setVisibility(VISIBLE);
+ }
+ }
+
+ private void updateSubtitleText(String prefix) {
+ // TODO: make this a formatted resource when we have a UX design.
+ // not worth translation right now.
+ StringBuilder sb = new StringBuilder();
+ if (prefix != null) {
+ sb.append(prefix);
+ }
+ sb.append(mAttachmentSizeText);
+ sb.append(' ');
+ sb.append(mDisplayType);
+ mSubTitle.setText(sb.toString());
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mIcon = (ImageView) findViewById(R.id.attachment_icon);
+ mTitle = (TextView) findViewById(R.id.attachment_title);
+ mSubTitle = (TextView) findViewById(R.id.attachment_subtitle);
+ mProgress = (ProgressBar) findViewById(R.id.attachment_progress);
+
+ mPreviewButton = (Button) findViewById(R.id.preview_attachment);
+ mViewButton = (Button) findViewById(R.id.view_attachment);
+ mSaveButton = (Button) findViewById(R.id.save_attachment);
+ mInfoButton = (Button) findViewById(R.id.info_attachment);
+ mPlayButton = (Button) findViewById(R.id.play_attachment);
+ mInstallButton = (Button) findViewById(R.id.install_attachment);
+ mCancelButton = (Button) findViewById(R.id.cancel_attachment);
+
+ setOnClickListener(this);
+ mPreviewButton.setOnClickListener(this);
+ mViewButton.setOnClickListener(this);
+ mSaveButton.setOnClickListener(this);
+ mInfoButton.setOnClickListener(this);
+ mPlayButton.setOnClickListener(this);
+ mInstallButton.setOnClickListener(this);
+ mCancelButton.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ onClick(v.getId(), v);
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ return onClick(item.getItemId(), null);
+ }
+
+ private boolean onClick(int res, View v) {
+ switch (res) {
+ case R.id.preview_attachment:
+ getContext().startActivity(mAttachment.previewIntent);
+ break;
+ case R.id.view_attachment:
+ case R.id.play_attachment:
+ showAttachment(AttachmentDestination.CACHE);
+ break;
+ case R.id.save_attachment:
+ if (mAttachment.canSave()) {
+ startDownloadingAttachment(AttachmentDestination.EXTERNAL);
+ }
+ break;
+ case R.id.info_attachment:
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+ int dialogMessage = MimeType.isBlocked(mAttachment.mimeType)
+ ? R.string.attachment_type_blocked : R.string.no_application_found;
+ builder.setTitle(R.string.more_info_attachment).setMessage(dialogMessage).show();
+ break;
+ case R.id.install_attachment:
+ showAttachment(AttachmentDestination.EXTERNAL);
+ break;
+ case R.id.cancel_attachment:
+ cancelAttachment();
+ break;
+ default:
+ // entire attachment view is clickable.
+ // TODO: this should execute a default action
+ break;
+ }
+ return true;
+ }
+
+ private void showAttachment(int destination) {
+ if (mAttachment.isPresentLocally()) {
+ sendViewIntent();
+ } else {
+ showDownloadingDialog();
+ startDownloadingAttachment(destination);
+ }
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ return new AttachmentLoader(getContext(), mAttachment.uri);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ AttachmentCursor cursor = (AttachmentCursor) data;
+ if (cursor == null || cursor.isClosed() || cursor.getCount() == 0) {
+ return;
+ }
+ cursor.moveToFirst();
+ updateStatus(cursor.get());
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ // Do nothing.
+ }
+
+ private void startDownloadingAttachment(int destination) {
+ final ContentValues params = new ContentValues(2);
+ params.put(AttachmentColumns.STATE, AttachmentState.DOWNLOADING);
+ params.put(AttachmentColumns.DESTINATION, destination);
+
+ mCommandHandler.sendCommand(params);
+ }
+
+ private void cancelAttachment() {
+ final ContentValues params = new ContentValues(1);
+ params.put(AttachmentColumns.STATE, AttachmentState.NOT_SAVED);
+
+ mCommandHandler.sendCommand(params);
+ }
+
+ private void setButtonVisible(View button, boolean visible) {
+ button.setVisibility(visible ? VISIBLE : GONE);
+ }
+
+ /**
+ * Update all action buttons based on current downloading state.
+ */
+ private void updateActions() {
+ // To avoid visibility state transition bugs, every button's visibility should be touched
+ // once by this routine.
+
+ final boolean isDownloading = mAttachment.isDownloading();
+
+ setButtonVisible(mCancelButton, isDownloading);
+
+ final boolean canInstall = MimeType.isInstallable(mAttachment.mimeType);
+ setButtonVisible(mInstallButton, canInstall && !isDownloading);
+
+ if (!canInstall) {
+
+ final boolean canPreview = (mAttachment.previewIntent != null);
+ final boolean canView = MimeType.isViewable(getContext(), mAttachment.mimeType);
+ final boolean canPlay = MimeType.isPlayable(mAttachment.mimeType);
+
+ setButtonVisible(mPreviewButton, canPreview);
+ setButtonVisible(mPlayButton, canView && canPlay && !isDownloading);
+ setButtonVisible(mViewButton, canView && !canPlay && !isDownloading);
+ setButtonVisible(mSaveButton, canView && mAttachment.canSave() && !isDownloading);
+ setButtonVisible(mInfoButton, !(canPreview || canView));
+
+ } else {
+
+ setButtonVisible(mPreviewButton, false);
+ setButtonVisible(mPlayButton, false);
+ setButtonVisible(mViewButton, false);
+ setButtonVisible(mSaveButton, false);
+ setButtonVisible(mInfoButton, false);
+
+ }
+ }
+
+ /**
+ * View an attachment by an application on device.
+ */
+ private void sendViewIntent() {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+ Utils.setIntentDataAndTypeAndNormalize(intent, Uri.parse(mAttachment.contentUri),
+ mAttachment.mimeType);
+ try {
+ getContext().startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ // couldn't find activity for View intent
+ LogUtils.e(LOG_TAG, "Coun't find Activity for intent", e);
+ }
+ }
+
+ /**
+ * Displays a loading dialog to be used for downloading attachments.
+ * Must be called on the UI thread.
+ */
+ private void showDownloadingDialog() {
+ mViewProgressDialog = new ProgressDialog(getContext());
+ mViewProgressDialog.setTitle(R.string.fetching_attachment);
+ mViewProgressDialog.setMessage(getResources().getString(R.string.please_wait));
+ mViewProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ mViewProgressDialog.setMax(mAttachment.size);
+ mViewProgressDialog.setOnDismissListener(this);
+ mViewProgressDialog.setOnCancelListener(this);
+ mViewProgressDialog.show();
+
+ // The progress number format needs to be set after the dialog is shown. See bug: 5149918
+ mViewProgressDialog.setProgressNumberFormat(null);
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ mViewProgressDialog = null;
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ cancelAttachment();
+ }
+
+}
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index f429a97..2cbb91e 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -18,9 +18,13 @@
import com.google.common.annotations.VisibleForTesting;
+import android.app.LoaderManager;
import android.content.Context;
+import android.content.Loader;
+import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Typeface;
+import android.os.Bundle;
import android.provider.ContactsContract;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
@@ -44,6 +48,7 @@
import com.android.mail.FormattedDateBuilder;
import com.android.mail.R;
import com.android.mail.SenderInfoLoader.ContactInfo;
+import com.android.mail.browse.AttachmentLoader.AttachmentCursor;
import com.android.mail.compose.ComposeActivity;
import com.android.mail.perf.Timer;
import com.android.mail.providers.Account;
@@ -56,11 +61,9 @@
import java.io.IOException;
import java.io.StringReader;
-import java.util.List;
-// TODO: this will probably becomes the message header view?
public class MessageHeaderView extends LinearLayout implements OnClickListener,
- OnMenuItemClickListener, HeaderBlock {
+ OnMenuItemClickListener, HeaderBlock, LoaderManager.LoaderCallbacks<Cursor> {
/**
* Cap very long recipient lists during summary construction for efficiency.
@@ -86,7 +89,6 @@
private MessageHeaderViewCallbacks mCallbacks;
private long mLocalMessageId = UIProvider.INVALID_CONVERSATION_ID;
private long mServerMessageId;
- private long mConversationId;
private boolean mSizeChanged;
private TextView mSenderNameView;
@@ -97,6 +99,7 @@
private ViewGroup mCollapsedDetailsView;
private ViewGroup mExpandedDetailsView;
private ViewGroup mImagePromptView;
+ private ViewGroup mAttachmentsView;
private View mBottomBorderView;
private ImageView mPresenceView;
@@ -140,9 +143,9 @@
private int mDrawTranslateY;
/**
- * List of attachments for this message. Will not be null.
+ * List of attachments for this message, loaded asynchronously.
*/
- private List<Attachment> mAttachments;
+ private AttachmentCursor mAttachments;
private CharSequence mTimestampShort;
@@ -168,6 +171,10 @@
private boolean mCollapsedDetailsValid;
private boolean mExpandedDetailsValid;
+ private LoaderManager mLoaderManager;
+
+ private final LayoutInflater mInflater;
+
public MessageHeaderView(Context context) {
this(context, null);
}
@@ -178,6 +185,8 @@
public MessageHeaderView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+
+ mInflater = LayoutInflater.from(context);
}
@Override
@@ -315,10 +324,12 @@
updateChildVisibility();
}
- public void initialize(FormattedDateBuilder dateBuilder, Account account, boolean expanded,
- boolean showImagePrompt, boolean defaultReplyAll) {
+ public void initialize(FormattedDateBuilder dateBuilder, Account account,
+ LoaderManager loaderManager, boolean expanded, boolean showImagePrompt,
+ boolean defaultReplyAll) {
mDateBuilder = dateBuilder;
mAccount = account;
+ mLoaderManager = loaderManager;
setExpanded(expanded);
mShowImagePrompt = showImagePrompt;
mDefaultReplyAll = defaultReplyAll;
@@ -334,7 +345,6 @@
mMessage = message;
mLocalMessageId = mMessage.id;
mServerMessageId = mMessage.serverId;
- mConversationId = mMessage.conversationId;
if (mCallbacks != null) {
mCallbacks.onHeaderCreated(mLocalMessageId);
}
@@ -351,6 +361,12 @@
mBcc = getBccAddresses(mMessage);
mReplyTo = Utils.splitCommaSeparatedString(mMessage.replyTo);
+ // kick off load of Attachment objects in background thread
+ if (mMessage.hasAttachments) {
+ mLoaderManager.initLoader(mMessage.hashCode(), Bundle.EMPTY, this);
+ // TODO: clean up loader when the view is detached
+ }
+
/**
* Turns draft mode on or off. Draft mode hides message operations other
* than "edit", hides contact photo, hides presence, and changes the
@@ -409,6 +425,33 @@
return h;
}
+ // Attachment list loader methods
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ return new AttachmentLoader(getContext(), mMessage.attachmentListUri);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ mAttachments = (AttachmentCursor) data;
+
+ if (mAttachmentsView != null) {
+ mAttachmentsView.removeAllViews();
+ }
+
+ if (mAttachments == null || mAttachments.isClosed()) {
+ return;
+ }
+
+ renderAttachments(mAttachmentsView);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ // Do nothing.
+ }
+
private boolean isInOutbox() {
// TODO: what should this read? Folder info?
return false;
@@ -518,7 +561,7 @@
setChildVisibility(GONE, R.id.edit_draft, R.id.reply, R.id.reply_all, R.id.forward);
setChildVisibility(GONE, R.id.overflow);
- setChildVisibility(mAttachments == null || mAttachments.isEmpty() ? GONE : VISIBLE,
+ setChildVisibility(mMessage.hasAttachments ? VISIBLE : GONE,
R.id.attachment);
setChildVisibility(mCollapsedStarVis, R.id.star);
@@ -698,7 +741,6 @@
* Get BCC addresses attached to a recipient ONLY if this is a msg the
* current user sent.
*
- * @param messageCursor Cursor to query for folder objects with
*/
private static String[] getBccAddresses(Message m) {
return Utils.splitCommaSeparatedString(m.bcc);
@@ -874,7 +916,7 @@
if (mShowImagePrompt) {
showImagePrompt();
}
- if (mAttachments != null && !mAttachments.isEmpty()) {
+ if (mMessage.hasAttachments) {
showAttachments();
}
}
@@ -884,11 +926,34 @@
}
private void showAttachments() {
- // Do nothing. Attachments not supported yet.
+ if (mAttachmentsView == null) {
+ ViewGroup container = (ViewGroup) mInflater.inflate(
+ R.layout.conversation_message_attachments, this, false);
+
+ renderAttachments(container);
+ addView(container);
+ mAttachmentsView = container;
+ }
+ mAttachmentsView.setVisibility(VISIBLE);
+ }
+
+ private void renderAttachments(ViewGroup container) {
+ if (container != null && mAttachments != null && !mAttachments.isClosed()) {
+ int i = -1;
+ while (mAttachments.moveToPosition(++i)) {
+ final Attachment attachment = mAttachments.get();
+ MessageHeaderAttachment attachView =
+ MessageHeaderAttachment.inflate(mInflater, container, mLoaderManager);
+ attachView.render(attachment);
+ container.addView(attachView);
+ }
+ }
}
private void hideAttachments() {
- // Do nothing. Attachments not supported yet.
+ if (mAttachmentsView != null) {
+ mAttachmentsView.setVisibility(GONE);
+ }
}
public void hideMessageDetails() {
diff --git a/src/com/android/mail/providers/Attachment.java b/src/com/android/mail/providers/Attachment.java
index 59607a1..4190c3d 100644
--- a/src/com/android/mail/providers/Attachment.java
+++ b/src/com/android/mail/providers/Attachment.java
@@ -15,11 +15,18 @@
*/
package com.android.mail.providers;
+import com.google.common.collect.Lists;
+
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
-import com.google.common.collect.Lists;
+import com.android.mail.providers.UIProvider.AttachmentColumns;
+import com.android.mail.providers.UIProvider.AttachmentDestination;
+import com.android.mail.providers.UIProvider.AttachmentState;
import java.util.ArrayList;
@@ -29,10 +36,64 @@
public static final int LOCAL_FILE = 1;
/**
- * Attachment name.
+ * Attachment file name. See {@link AttachmentColumns#NAME}.
*/
public String name;
+ /**
+ * Attachment size in bytes. See {@link AttachmentColumns#SIZE}.
+ */
+ public int size;
+
+ /**
+ * See {@link AttachmentColumns#URI}.
+ */
+ public Uri uri;
+
+ /**
+ * MIME type of the file. See {@link AttachmentColumns#CONTENT_TYPE}.
+ */
+ // TODO: rename to be consistent with UIProvider name: "contentType"
+ @Deprecated
+ public String mimeType;
+
+ /**
+ * See {@link AttachmentColumns#STATE}.
+ */
+ public int state;
+
+ /**
+ * See {@link AttachmentColumns#DESTINATION}.
+ */
+ public int destination;
+
+ /**
+ * See {@link AttachmentColumns#DOWNLOADED_SIZE}.
+ */
+ public int downloadedSize;
+
+ /**
+ * See {@link AttachmentColumns#CONTENT_URI}.
+ */
+ // TODO: change this to be a Uri for consistency with other URIs in data model objects.
+ @Deprecated
+ public String contentUri;
+
+ /**
+ * See {@link AttachmentColumns#THUMBNAIL_URI}. Might be null.
+ */
+ public Uri thumbnailUri;
+
+ /**
+ * See {@link AttachmentColumns#PREVIEW_INTENT}. Might be null.
+ */
+ public Intent previewIntent;
+
+ /**
+ * Part id of the attachment.
+ */
+ public String partId;
+
public int origin;
/**
@@ -40,36 +101,49 @@
* TODO: do we want this? Or location?
*/
public String originExtras;
- /**
- * Mime type of the file.
- */
- public String mimeType;
- /**
- * Content uri location of the attachment.
- */
- public String contentUri;
- /**
- * Part id of the attachment.
- */
- public String partId;
- /**
- * Attachment size in kb.
- */
- public long size;
public Attachment(Parcel in) {
name = in.readString();
- originExtras = in.readString();
+ size = in.readInt();
+ uri = in.readParcelable(null);
mimeType = in.readString();
+ state = in.readInt();
+ destination = in.readInt();
+ downloadedSize = in.readInt();
contentUri = in.readString();
+ thumbnailUri = in.readParcelable(null);
+ previewIntent = in.readParcelable(null);
partId = in.readString();
- size = in.readLong();
origin = in.readInt();
+ originExtras = in.readString();
}
public Attachment() {
}
+ public Attachment(Cursor cursor) {
+ if (cursor == null) {
+ return;
+ }
+
+ name = cursor.getString(UIProvider.ATTACHMENT_NAME_COLUMN);
+ size = cursor.getInt(UIProvider.ATTACHMENT_SIZE_COLUMN);
+ uri = Uri.parse(cursor.getString(UIProvider.ATTACHMENT_URI_COLUMN));
+ mimeType = cursor.getString(UIProvider.ATTACHMENT_CONTENT_TYPE_COLUMN);
+ state = cursor.getInt(UIProvider.ATTACHMENT_STATE_COLUMN);
+ destination = cursor.getInt(UIProvider.ATTACHMENT_DESTINATION_COLUMN);
+ downloadedSize = cursor.getInt(UIProvider.ATTACHMENT_DOWNLOADED_SIZE_COLUMN);
+ // TODO: change to use parseUri()
+ contentUri = cursor.getString(UIProvider.ATTACHMENT_CONTENT_URI_COLUMN);
+ thumbnailUri = parseOptionalUri(
+ cursor.getString(UIProvider.ATTACHMENT_THUMBNAIL_URI_COLUMN));
+ previewIntent = getOptionalIntentFromBlob(
+ cursor.getBlob(UIProvider.ATTACHMENT_PREVIEW_INTENT_COLUMN));
+
+ // TODO: ensure that local files attached to a draft have sane values, like SAVED/EXTERNAL
+ // and that contentUri is populated
+ }
+
public Attachment(String attachmentString) {
String[] attachmentValues = attachmentString.split("\\|");
if (attachmentValues != null) {
@@ -77,7 +151,7 @@
name = attachmentValues[1];
mimeType = attachmentValues[2];
try {
- size = Long.parseLong(attachmentValues[3]);
+ size = Integer.parseInt(attachmentValues[3]);
} catch (NumberFormatException e) {
size = 0;
}
@@ -88,6 +162,29 @@
}
}
+ public String toJoinedString() {
+ // FIXME: mimeType is read/written twice
+ return TextUtils.join("|", Lists.newArrayList(partId == null ? "" : partId,
+ name == null ? "" : name.replaceAll("[|\n]", ""), mimeType, size, mimeType,
+ origin + "", contentUri, TextUtils.isEmpty(originExtras) ? contentUri
+ : originExtras, ""));
+ }
+
+ private static Intent getOptionalIntentFromBlob(byte[] blob) {
+ if (blob == null) {
+ return null;
+ }
+ final Parcel intentParcel = Parcel.obtain();
+ intentParcel.unmarshall(blob, 0, blob.length);
+ final Intent intent = new Intent();
+ intent.readFromParcel(intentParcel);
+ return intent;
+ }
+
+ private static Uri parseOptionalUri(String uriString) {
+ return uriString == null ? null : Uri.parse(uriString);
+ }
+
@Override
public int describeContents() {
return 0;
@@ -96,12 +193,18 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
- dest.writeString(originExtras);
+ dest.writeInt(size);
+ dest.writeParcelable(uri, flags);
dest.writeString(mimeType);
+ dest.writeInt(state);
+ dest.writeInt(destination);
+ dest.writeInt(downloadedSize);
dest.writeString(contentUri);
+ dest.writeParcelable(thumbnailUri, flags);
+ dest.writeParcelable(previewIntent, flags);
dest.writeString(partId);
- dest.writeLong(size);
dest.writeInt(origin);
+ dest.writeString(originExtras);
}
public static final Creator<Attachment> CREATOR = new Creator<Attachment>() {
@@ -116,18 +219,26 @@
}
};
-
- public String toJoinedString() {
- return TextUtils.join("|", Lists.newArrayList(partId == null ? "" : partId,
- name == null ? "" : name.replaceAll("[|\n]", ""), mimeType, size, mimeType,
- origin + "", contentUri, TextUtils.isEmpty(originExtras) ? contentUri
- : originExtras, ""));
- }
-
public boolean isImage() {
return mimeType.startsWith("image");
}
+ public boolean isDownloading() {
+ return state == AttachmentState.DOWNLOADING;
+ }
+
+ public boolean isPresentLocally() {
+ return state == AttachmentState.SAVED || origin == LOCAL_FILE;
+ }
+
+ public boolean isSavedToExternal() {
+ return state == AttachmentState.SAVED && destination == AttachmentDestination.EXTERNAL;
+ }
+
+ public boolean canSave() {
+ return origin == SERVER_ATTACHMENT && state != AttachmentState.DOWNLOADING
+ && state != AttachmentState.SAVED;
+ }
/**
* Translate attachment info from a message into attachment objects.
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index 31ec7ae..dd5ac85 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -37,6 +37,7 @@
import android.webkit.ConsoleMessage;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
+import android.widget.Adapter;
import android.widget.ResourceCursorAdapter;
import android.widget.TextView;
@@ -207,8 +208,9 @@
MessageCursor messageCursor = (MessageCursor) data;
mWebView.loadDataWithBaseURL(mBaseUri, renderMessageBodies(messageCursor), "text/html",
"utf-8", null);
- mConversationContainer.setOverlayAdapter(
- new MessageListAdapter(mActivity.getActivityContext(), messageCursor, mAccount));
+ final Adapter messageListAdapter = new MessageListAdapter(
+ mActivity.getActivityContext(), messageCursor, mAccount, getLoaderManager());
+ mConversationContainer.setOverlayAdapter(messageListAdapter);
}
@Override
@@ -266,18 +268,21 @@
private final FormattedDateBuilder mDateBuilder;
private final Account mAccount;
+ private final LoaderManager mLoaderManager;
- public MessageListAdapter(Context context, Cursor cursor, Account account) {
- super(context, R.layout.conversation_message_header, cursor, 0);
+ public MessageListAdapter(Context context, Cursor messageCursor, Account account,
+ LoaderManager loaderManager) {
+ super(context, R.layout.conversation_message_header, messageCursor, 0);
mDateBuilder = new FormattedDateBuilder(context);
mAccount = account;
+ mLoaderManager = loaderManager;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
Message m = ((MessageCursor) cursor).get();
MessageHeaderView header = (MessageHeaderView) view;
- header.initialize(mDateBuilder, mAccount, true, false, false);
+ header.initialize(mDateBuilder, mAccount, mLoaderManager, true, false, false);
header.bind(m);
}
}
diff --git a/src/com/android/mail/utils/AttachmentUtils.java b/src/com/android/mail/utils/AttachmentUtils.java
index ad7714e..4e4b22c 100644
--- a/src/com/android/mail/utils/AttachmentUtils.java
+++ b/src/com/android/mail/utils/AttachmentUtils.java
@@ -15,17 +15,27 @@
*/
package com.android.mail.utils;
+import com.google.common.collect.ImmutableMap;
+
import android.content.Context;
import com.android.mail.R;
+import com.android.mail.providers.Attachment;
import java.text.DecimalFormat;
+import java.util.Map;
public class AttachmentUtils {
private static final int KILO = 1024;
private static final int MEGA = KILO * KILO;
/**
+ * Singleton map of MIME->friendly description
+ * @see #getMimeTypeDisplayName(Context, String)
+ */
+ private static Map<String, String> sDisplayNameMap;
+
+ /**
* @return A string suitable for display in bytes, kilobytes or megabytes
* depending on its size.
*/
@@ -40,4 +50,77 @@
+ context.getString(R.string.megabytes);
}
}
+
+ /**
+ * Return a friendly localized file type for this attachment, or the empty string if
+ * unknown.
+ * @param context a Context to do resource lookup against
+ * @return friendly file type or empty string
+ */
+ public static String getDisplayType(final Context context, final Attachment attachment) {
+ // try to get a friendly name for the exact mime type
+ // then try to show a friendly name for the mime family
+ // finally, give up and just show the file extension
+ String displayType = getMimeTypeDisplayName(context, attachment.mimeType);
+ int index = attachment.mimeType.indexOf('/');
+ if (displayType == null && index > 0) {
+ displayType = getMimeTypeDisplayName(context,
+ attachment.mimeType.substring(0, index));
+ }
+ if (displayType == null) {
+ String extension = Utils.getFileExtension(attachment.name);
+ // show '$EXTENSION File' for unknown file types
+ if (extension != null && extension.length() > 1 && extension.indexOf('.') == 0) {
+ displayType = context.getString(R.string.attachment_unknown,
+ extension.substring(1).toUpperCase());
+ }
+ }
+ if (displayType == null) {
+ // no extension to display, but the map doesn't accept null entries
+ displayType = "";
+ }
+ return displayType;
+ }
+
+ /**
+ * Returns a user-friendly localized description of either a complete a MIME type or a
+ * MIME family.
+ * @param context used to look up localized strings
+ * @param type complete MIME type or just MIME family
+ * @return localized description text, or null if not recognized
+ */
+ public static synchronized String getMimeTypeDisplayName(final Context context,
+ String type) {
+ if (sDisplayNameMap == null) {
+ String docName = context.getString(R.string.attachment_application_msword);
+ String presoName = context.getString(R.string.attachment_application_vnd_ms_powerpoint);
+ String sheetName = context.getString(R.string.attachment_application_vnd_ms_excel);
+
+ sDisplayNameMap = new ImmutableMap.Builder<String, String>()
+ .put("image", context.getString(R.string.attachment_image))
+ .put("audio", context.getString(R.string.attachment_audio))
+ .put("video", context.getString(R.string.attachment_video))
+ .put("text", context.getString(R.string.attachment_text))
+ .put("application/pdf", context.getString(R.string.attachment_application_pdf))
+
+ // Documents
+ .put("application/msword", docName)
+ .put("application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ docName)
+
+ // Presentations
+ .put("application/vnd.ms-powerpoint",
+ presoName)
+ .put("application/vnd.openxmlformats-officedocument.presentationml.presentation",
+ presoName)
+
+ // Spreadsheets
+ .put("application/vnd.ms-excel", sheetName)
+ .put("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ sheetName)
+
+ .build();
+ }
+ return sDisplayNameMap.get(type);
+ }
}
diff --git a/src/com/android/mail/utils/MimeType.java b/src/com/android/mail/utils/MimeType.java
index f88b262..abe07e0 100644
--- a/src/com/android/mail/utils/MimeType.java
+++ b/src/com/android/mail/utils/MimeType.java
@@ -31,7 +31,7 @@
import java.util.Set;
/**
- * Utilities for working with different content types within Gmail.
+ * Utilities for working with different content types within Mail.
*/
public class MimeType {
public static final String ANDROID_ARCHIVE = "application/vnd.android.package-archive";
@@ -71,7 +71,7 @@
/**
* Returns whether or not an attachment of the specified type is viewable.
*/
- public static boolean isViewable(Context context, Uri contentUri, String contentType) {
+ public static boolean isViewable(Context context, String contentType) {
// The provider returns a contentType of "null" instead of null, when the
// content type is not known. Changing the provider to return null,
// breaks other areas that will need to be fixed in a later CL.
@@ -87,7 +87,7 @@
Intent mimetypeIntent = new Intent(Intent.ACTION_VIEW);
- mimetypeIntent.setDataAndType(contentUri, contentType);
+ Utils.setIntentTypeAndNormalize(mimetypeIntent, contentType);
PackageManager manager;
// We need to catch the exception to make CanvasConversationHeaderView
// test pass. Bug: http://b/issue?id=3470653.
diff --git a/src/com/android/mail/utils/Utils.java b/src/com/android/mail/utils/Utils.java
index c9032fd..fbd0fda 100644
--- a/src/com/android/mail/utils/Utils.java
+++ b/src/com/android/mail/utils/Utils.java
@@ -46,7 +46,6 @@
import com.android.mail.providers.Account;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
-import com.android.mail.providers.UIProvider;
import java.util.Locale;
import java.util.Map;
@@ -693,4 +692,98 @@
String query = intent.getStringExtra(SearchManager.QUERY);
return TextUtils.isEmpty(query) ? null : query.trim();
}
+
+ /**
+ * Split out a filename's extension and return it.
+ * @param filename a file name
+ * @return the file extension (max of 5 chars including period, like ".docx"), or null
+ */
+ public static String getFileExtension(String filename) {
+ String extension = null;
+ int index = filename.lastIndexOf('.');
+ // Limit the suffix to dot + four characters
+ if (index >= 0 && filename.length() - index <= FILE_EXTENSION_MAX_CHARS + 1) {
+ extension = filename.substring(index);
+ }
+ return extension;
+ }
+
+ /**
+ * (copied from {@link Intent#normalizeMimeType(String)} for pre-J)
+ *
+ * Normalize a MIME data type.
+ *
+ * <p>A normalized MIME type has white-space trimmed,
+ * content-type parameters removed, and is lower-case.
+ * This aligns the type with Android best practices for
+ * intent filtering.
+ *
+ * <p>For example, "text/plain; charset=utf-8" becomes "text/plain".
+ * "text/x-vCard" becomes "text/x-vcard".
+ *
+ * <p>All MIME types received from outside Android (such as user input,
+ * or external sources like Bluetooth, NFC, or the Internet) should
+ * be normalized before they are used to create an Intent.
+ *
+ * @param type MIME data type to normalize
+ * @return normalized MIME data type, or null if the input was null
+ * @see {@link #setType}
+ * @see {@link #setTypeAndNormalize}
+ */
+ public static String normalizeMimeType(String type) {
+ if (type == null) {
+ return null;
+ }
+
+ type = type.trim().toLowerCase(Locale.US);
+
+ final int semicolonIndex = type.indexOf(';');
+ if (semicolonIndex != -1) {
+ type = type.substring(0, semicolonIndex);
+ }
+ return type;
+ }
+
+ /**
+ * (copied from {@link Uri#normalize()} for pre-J)
+ *
+ * Return a normalized representation of this Uri.
+ *
+ * <p>A normalized Uri has a lowercase scheme component.
+ * This aligns the Uri with Android best practices for
+ * intent filtering.
+ *
+ * <p>For example, "HTTP://www.android.com" becomes
+ * "http://www.android.com"
+ *
+ * <p>All URIs received from outside Android (such as user input,
+ * or external sources like Bluetooth, NFC, or the Internet) should
+ * be normalized before they are used to create an Intent.
+ *
+ * <p class="note">This method does <em>not</em> validate bad URI's,
+ * or 'fix' poorly formatted URI's - so do not use it for input validation.
+ * A Uri will always be returned, even if the Uri is badly formatted to
+ * begin with and a scheme component cannot be found.
+ *
+ * @return normalized Uri (never null)
+ * @see {@link android.content.Intent#setData}
+ * @see {@link #setNormalizedData}
+ */
+ public static Uri normalizeUri(Uri uri) {
+ String scheme = uri.getScheme();
+ if (scheme == null) return uri; // give up
+ String lowerScheme = scheme.toLowerCase(Locale.US);
+ if (scheme.equals(lowerScheme)) return uri; // no change
+
+ return uri.buildUpon().scheme(lowerScheme).build();
+ }
+
+ public static Intent setIntentTypeAndNormalize(Intent intent, String type) {
+ return intent.setType(normalizeMimeType(type));
+ }
+
+ public static Intent setIntentDataAndTypeAndNormalize(Intent intent, Uri data, String type) {
+ return intent.setDataAndType(normalizeUri(data), normalizeMimeType(type));
+ }
+
}