18a574694606f0e5d781334d0d426fc379c51f3edMarc Blank/*
28a574694606f0e5d781334d0d426fc379c51f3edMarc Blank * Copyright (C) 2011 The Android Open Source Project
38a574694606f0e5d781334d0d426fc379c51f3edMarc Blank *
48a574694606f0e5d781334d0d426fc379c51f3edMarc Blank * Licensed under the Apache License, Version 2.0 (the "License");
58a574694606f0e5d781334d0d426fc379c51f3edMarc Blank * you may not use this file except in compliance with the License.
68a574694606f0e5d781334d0d426fc379c51f3edMarc Blank * You may obtain a copy of the License at
78a574694606f0e5d781334d0d426fc379c51f3edMarc Blank *
88a574694606f0e5d781334d0d426fc379c51f3edMarc Blank *      http://www.apache.org/licenses/LICENSE-2.0
98a574694606f0e5d781334d0d426fc379c51f3edMarc Blank *
108a574694606f0e5d781334d0d426fc379c51f3edMarc Blank * Unless required by applicable law or agreed to in writing, software
118a574694606f0e5d781334d0d426fc379c51f3edMarc Blank * distributed under the License is distributed on an "AS IS" BASIS,
128a574694606f0e5d781334d0d426fc379c51f3edMarc Blank * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
138a574694606f0e5d781334d0d426fc379c51f3edMarc Blank * See the License for the specific language governing permissions and
148a574694606f0e5d781334d0d426fc379c51f3edMarc Blank * limitations under the License.
158a574694606f0e5d781334d0d426fc379c51f3edMarc Blank */
168a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
178a574694606f0e5d781334d0d426fc379c51f3edMarc Blankpackage com.android.emailcommon.utility;
188a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
19f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport android.app.DownloadManager;
208a574694606f0e5d781334d0d426fc379c51f3edMarc Blankimport android.content.ContentResolver;
218a574694606f0e5d781334d0d426fc379c51f3edMarc Blankimport android.content.ContentUris;
22f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport android.content.ContentValues;
238a574694606f0e5d781334d0d426fc379c51f3edMarc Blankimport android.content.Context;
248a574694606f0e5d781334d0d426fc379c51f3edMarc Blankimport android.database.Cursor;
25f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport android.media.MediaScannerConnection;
268a574694606f0e5d781334d0d426fc379c51f3edMarc Blankimport android.net.Uri;
27f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport android.os.Environment;
288a574694606f0e5d781334d0d426fc379c51f3edMarc Blankimport android.text.TextUtils;
298a574694606f0e5d781334d0d426fc379c51f3edMarc Blankimport android.webkit.MimeTypeMap;
308a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
31f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport com.android.emailcommon.Logging;
32f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport com.android.emailcommon.provider.EmailContent.Attachment;
33f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport com.android.emailcommon.provider.EmailContent.AttachmentColumns;
34f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport com.android.emailcommon.provider.EmailContent.Message;
35f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport com.android.emailcommon.provider.EmailContent.MessageColumns;
36f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport com.android.mail.providers.UIProvider;
37560bfadc3151f7a06f3b06e9a6c92cfa534c63ecScott Kennedyimport com.android.mail.utils.LogUtils;
38f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
39f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport org.apache.commons.io.IOUtils;
40f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
418a574694606f0e5d781334d0d426fc379c51f3edMarc Blankimport java.io.File;
42f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport java.io.FileOutputStream;
43f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport java.io.IOException;
44f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport java.io.InputStream;
45f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport java.io.OutputStream;
468a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
478a574694606f0e5d781334d0d426fc379c51f3edMarc Blankpublic class AttachmentUtilities {
488a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
498a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    public static final String FORMAT_RAW = "RAW";
508a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    public static final String FORMAT_THUMBNAIL = "THUMBNAIL";
518a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
528a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    public static class Columns {
538a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        public static final String _ID = "_id";
548a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        public static final String DATA = "_data";
558a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        public static final String DISPLAY_NAME = "_display_name";
568a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        public static final String SIZE = "_size";
578a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    }
588a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
599a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook    private static final String[] ATTACHMENT_CACHED_FILE_PROJECTION = new String[] {
609a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook            AttachmentColumns.CACHED_FILE
619a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook    };
629a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook
6331d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    /**
6431d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * The MIME type(s) of attachments we're willing to send via attachments.
6531d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     *
6631d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * Any attachments may be added via Intents with Intent.ACTION_SEND or ACTION_SEND_MULTIPLE.
6731d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     */
6831d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    public static final String[] ACCEPTABLE_ATTACHMENT_SEND_INTENT_TYPES = new String[] {
6931d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        "*/*",
7031d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    };
7131d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    /**
7231d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * The MIME type(s) of attachments we're willing to send from the internal UI.
7331d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     *
7431d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * NOTE:  At the moment it is not possible to open a chooser with a list of filter types, so
7531d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * the chooser is only opened with the first item in the list.
7631d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     */
7731d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    public static final String[] ACCEPTABLE_ATTACHMENT_SEND_UI_TYPES = new String[] {
7831d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        "image/*",
7931d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        "video/*",
8031d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    };
8131d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    /**
8231d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * The MIME type(s) of attachments we're willing to view.
8331d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     */
8431d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    public static final String[] ACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
8531d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        "*/*",
8631d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    };
8731d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    /**
8831d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * The MIME type(s) of attachments we're not willing to view.
8931d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     */
9031d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    public static final String[] UNACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
9131d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    };
9231d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    /**
9331d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * The MIME type(s) of attachments we're willing to download to SD.
9431d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     */
9531d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    public static final String[] ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
9631d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        "*/*",
9731d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    };
9831d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    /**
9931d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * The MIME type(s) of attachments we're not willing to download to SD.
10031d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     */
10131d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    public static final String[] UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
10231d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    };
10331d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    /**
10431d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * Filename extensions of attachments we're never willing to download (potential malware).
10531d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * Entries in this list are compared to the end of the lower-cased filename, so they must
10631d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * be lower case, and should not include a "."
10731d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     */
10831d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    public static final String[] UNACCEPTABLE_ATTACHMENT_EXTENSIONS = new String[] {
10931d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        // File types that contain malware
11031d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        "ade", "adp", "bat", "chm", "cmd", "com", "cpl", "dll", "exe",
11131d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        "hta", "ins", "isp", "jse", "lib", "mde", "msc", "msp",
11231d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        "mst", "pif", "scr", "sct", "shb", "sys", "vb", "vbe",
11331d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        "vbs", "vxd", "wsc", "wsf", "wsh",
11431d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        // File types of common compression/container formats (again, to avoid malware)
11531d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        "zip", "gz", "z", "tar", "tgz", "bz2",
11631d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    };
11731d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    /**
11831d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * Filename extensions of attachments that can be installed.
11931d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * Entries in this list are compared to the end of the lower-cased filename, so they must
12031d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * be lower case, and should not include a "."
12131d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     */
12231d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    public static final String[] INSTALLABLE_ATTACHMENT_EXTENSIONS = new String[] {
12331d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank        "apk",
12431d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    };
12531d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    /**
12631d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * The maximum size of an attachment we're willing to download (either View or Save)
12731d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * Attachments that are base64 encoded (most) will be about 1.375x their actual size
12831d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * so we should probably factor that in. A 5MB attachment will generally be around
12931d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * 6.8MB downloaded but only 5MB saved.
13031d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     */
13131d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    public static final int MAX_ATTACHMENT_DOWNLOAD_SIZE = (5 * 1024 * 1024);
13231d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    /**
13331d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * The maximum size of an attachment we're willing to upload (measured as stored on disk).
13431d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * Attachments that are base64 encoded (most) will be about 1.375x their actual size
13531d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * so we should probably factor that in. A 5MB attachment will generally be around
13631d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     * 6.8MB uploaded.
13731d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank     */
13831d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank    public static final int MAX_ATTACHMENT_UPLOAD_SIZE = (5 * 1024 * 1024);
13931d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blank
140e714bb9d153cfe13a7f0932e7d67ea08fa5a1d98Marc Blank    private static Uri sUri;
1418a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    public static Uri getAttachmentUri(long accountId, long id) {
142e714bb9d153cfe13a7f0932e7d67ea08fa5a1d98Marc Blank        if (sUri == null) {
1436e5bccf2c984039da5ae1dc08cffa665b73b6474Marc Blank            sUri = Uri.parse(Attachment.ATTACHMENT_PROVIDER_URI_PREFIX);
144e714bb9d153cfe13a7f0932e7d67ea08fa5a1d98Marc Blank        }
145e714bb9d153cfe13a7f0932e7d67ea08fa5a1d98Marc Blank        return sUri.buildUpon()
146e714bb9d153cfe13a7f0932e7d67ea08fa5a1d98Marc Blank                .appendPath(Long.toString(accountId))
147e714bb9d153cfe13a7f0932e7d67ea08fa5a1d98Marc Blank                .appendPath(Long.toString(id))
148e714bb9d153cfe13a7f0932e7d67ea08fa5a1d98Marc Blank                .appendPath(FORMAT_RAW)
149e714bb9d153cfe13a7f0932e7d67ea08fa5a1d98Marc Blank                .build();
1508a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    }
1518a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
15217d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie    // exposed for testing
15317d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie    public static Uri getAttachmentThumbnailUri(long accountId, long id, long width, long height) {
15417d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie        if (sUri == null) {
15517d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie            sUri = Uri.parse(Attachment.ATTACHMENT_PROVIDER_URI_PREFIX);
15617d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie        }
15717d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie        return sUri.buildUpon()
15817d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie                .appendPath(Long.toString(accountId))
15917d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie                .appendPath(Long.toString(id))
16017d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie                .appendPath(FORMAT_THUMBNAIL)
16117d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie                .appendPath(Long.toString(width))
16217d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie                .appendPath(Long.toString(height))
16317d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie                .build();
16417d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie    }
16517d3a29c9d8f7a27c463239f190bdcc4e0804527Jerry Xie
1668a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    /**
1678a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * Return the filename for a given attachment.  This should be used by any code that is
1688a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * going to *write* attachments.
1698a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *
1708a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * This does not create or write the file, or even the directories.  It simply builds
1718a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * the filename that should be used.
1728a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     */
1738a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    public static File getAttachmentFilename(Context context, long accountId, long attachmentId) {
1748a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        return new File(getAttachmentDirectory(context, accountId), Long.toString(attachmentId));
1758a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    }
1768a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
1778a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    /**
1788a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * Return the directory for a given attachment.  This should be used by any code that is
1798a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * going to *write* attachments.
1808a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *
1818a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * This does not create or write the directory.  It simply builds the pathname that should be
1828a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * used.
1838a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     */
1848a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    public static File getAttachmentDirectory(Context context, long accountId) {
1858a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        return context.getDatabasePath(accountId + ".db_att");
1868a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    }
1878a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
1888a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    /**
1898a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * Helper to convert unknown or unmapped attachments to something useful based on filename
1908a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * extensions. The mime type is inferred based upon the table below. It's not perfect, but
1918a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * it helps.
1928a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *
1938a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * <pre>
1948a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *                   |---------------------------------------------------------|
1958a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *                   |                  E X T E N S I O N                      |
1968a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *                   |---------------------------------------------------------|
1978a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *                   | .eml        | known(.png) | unknown(.abc) | none        |
1988a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * | M |-----------------------------------------------------------------------|
1998a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * | I | none        | msg/rfc822  | image/png   | app/abc       | app/oct-str |
2008a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * | M |-------------| (always     |             |               |             |
2018a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * | E | app/oct-str |  overrides  |             |               |             |
2028a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * | T |-------------|             |             |-----------------------------|
2038a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * | Y | text/plain  |             |             | text/plain                  |
2048a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * | P |-------------|             |-------------------------------------------|
2058a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * | E | any/type    |             | any/type                                  |
2068a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * |---|-----------------------------------------------------------------------|
2078a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * </pre>
2088a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *
2098a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * NOTE: Since mime types on Android are case-*sensitive*, return values are always in
2108a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * lower case.
2118a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *
2128a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @param fileName The given filename
2138a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @param mimeType The given mime type
2148a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @return A likely mime type for the attachment
2158a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     */
2168a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    public static String inferMimeType(final String fileName, final String mimeType) {
2178a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        String resultType = null;
2188a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        String fileExtension = getFilenameExtension(fileName);
2198a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        boolean isTextPlain = "text/plain".equalsIgnoreCase(mimeType);
2208a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
2218a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        if ("eml".equals(fileExtension)) {
2228a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            resultType = "message/rfc822";
2238a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        } else {
2248a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            boolean isGenericType =
2258a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                    isTextPlain || "application/octet-stream".equalsIgnoreCase(mimeType);
2268a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            // If the given mime type is non-empty and non-generic, return it
2278a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            if (isGenericType || TextUtils.isEmpty(mimeType)) {
2288a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                if (!TextUtils.isEmpty(fileExtension)) {
2298a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                    // Otherwise, try to find a mime type based upon the file extension
2308a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                    resultType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
2318a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                    if (TextUtils.isEmpty(resultType)) {
2328a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                        // Finally, if original mimetype is text/plain, use it; otherwise synthesize
2338a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                        resultType = isTextPlain ? mimeType : "application/" + fileExtension;
2348a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                    }
2358a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                }
2368a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            } else {
2378a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                resultType = mimeType;
2388a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            }
2398a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        }
2408a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
2418a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        // No good guess could be made; use an appropriate generic type
2428a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        if (TextUtils.isEmpty(resultType)) {
2438a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            resultType = isTextPlain ? "text/plain" : "application/octet-stream";
2448a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        }
2458a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        return resultType.toLowerCase();
2468a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    }
2478a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
2488a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    /**
2498a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * Extract and return filename's extension, converted to lower case, and not including the "."
2508a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *
2518a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @return extension, or null if not found (or null/empty filename)
2528a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     */
2538a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    public static String getFilenameExtension(String fileName) {
2548a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        String extension = null;
2558a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        if (!TextUtils.isEmpty(fileName)) {
2568a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            int lastDot = fileName.lastIndexOf('.');
2578a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
2588a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                extension = fileName.substring(lastDot + 1).toLowerCase();
2598a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            }
2608a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        }
2618a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        return extension;
2628a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    }
2638a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
2648a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    /**
2658a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * Resolve attachment id to content URI.  Returns the resolved content URI (from the attachment
2668a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * DB) or, if not found, simply returns the incoming value.
2678a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *
2688a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @param attachmentUri
2698a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @return resolved content URI
2708a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *
2718a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * TODO:  Throws an SQLite exception on a missing DB file (e.g. unknown URI) instead of just
2728a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * returning the incoming uri, as it should.
2738a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     */
2748a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) {
2758a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        Cursor c = resolver.query(attachmentUri,
2768a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                new String[] { Columns.DATA },
2778a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                null, null, null);
2788a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        if (c != null) {
2798a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            try {
2808a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                if (c.moveToFirst()) {
2818a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                    final String strUri = c.getString(0);
2828a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                    if (strUri != null) {
2838a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                        return Uri.parse(strUri);
2848a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                    }
2858a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                }
2868a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            } finally {
2878a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                c.close();
2888a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            }
2898a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        }
2908a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        return attachmentUri;
2918a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    }
2928a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
2938a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    /**
2948a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * In support of deleting a message, find all attachments and delete associated attachment
2958a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * files.
2968a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @param context
2978a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @param accountId the account for the message
2988a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @param messageId the message
2998a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     */
3008a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    public static void deleteAllAttachmentFiles(Context context, long accountId, long messageId) {
3018a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
3028a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        Cursor c = context.getContentResolver().query(uri, Attachment.ID_PROJECTION,
3038a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                null, null, null);
3048a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        try {
3058a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            while (c.moveToNext()) {
3068a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                long attachmentId = c.getLong(Attachment.ID_PROJECTION_COLUMN);
3078a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                File attachmentFile = getAttachmentFilename(context, accountId, attachmentId);
3088a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                // Note, delete() throws no exceptions for basic FS errors (e.g. file not found)
3098a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                // it just returns false, which we ignore, and proceed to the next file.
3108a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                // This entire loop is best-effort only.
3118a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                attachmentFile.delete();
3128a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            }
3138a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        } finally {
3148a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            c.close();
3158a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        }
3168a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    }
3178a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
3188a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    /**
3199a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook     * In support of deleting a message, find all attachments and delete associated cached
3209a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook     * attachment files.
3219a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook     * @param context
3229a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook     * @param accountId the account for the message
3239a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook     * @param messageId the message
3249a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook     */
3259a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook    public static void deleteAllCachedAttachmentFiles(Context context, long accountId,
3269a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook            long messageId) {
3279a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook        final Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
3289a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook        final Cursor c = context.getContentResolver().query(uri, ATTACHMENT_CACHED_FILE_PROJECTION,
3299a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook                null, null, null);
3309a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook        try {
3319a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook            while (c.moveToNext()) {
3329a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook                final String fileName = c.getString(0);
3339a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook                if (!TextUtils.isEmpty(fileName)) {
3349a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook                    final File cachedFile = new File(fileName);
3359a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook                    // Note, delete() throws no exceptions for basic FS errors (e.g. file not found)
3369a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook                    // it just returns false, which we ignore, and proceed to the next file.
3379a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook                    // This entire loop is best-effort only.
3389a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook                    cachedFile.delete();
3399a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook                }
3409a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook            }
3419a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook        } finally {
3429a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook            c.close();
3439a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook        }
3449a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook    }
3459a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook
3469a95253846ccc7a94dd7d4c618ec2d808e2a4000Paul Westbrook    /**
3478a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * In support of deleting a mailbox, find all messages and delete their attachments.
3488a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *
3498a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @param context
3508a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @param accountId the account for the mailbox
3518a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @param mailboxId the mailbox for the messages
3528a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     */
3538a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    public static void deleteAllMailboxAttachmentFiles(Context context, long accountId,
3548a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            long mailboxId) {
3558a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        Cursor c = context.getContentResolver().query(Message.CONTENT_URI,
3568a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                Message.ID_COLUMN_PROJECTION, MessageColumns.MAILBOX_KEY + "=?",
3578a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                new String[] { Long.toString(mailboxId) }, null);
3588a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        try {
3598a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            while (c.moveToNext()) {
3608a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                long messageId = c.getLong(Message.ID_PROJECTION_COLUMN);
3618a574694606f0e5d781334d0d426fc379c51f3edMarc Blank                deleteAllAttachmentFiles(context, accountId, messageId);
3628a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            }
3638a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        } finally {
3648a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            c.close();
3658a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        }
3668a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    }
3678a574694606f0e5d781334d0d426fc379c51f3edMarc Blank
3688a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    /**
3698a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * In support of deleting or wiping an account, delete all related attachments.
3708a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     *
3718a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @param context
3728a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     * @param accountId the account to scrub
3738a574694606f0e5d781334d0d426fc379c51f3edMarc Blank     */
3748a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    public static void deleteAllAccountAttachmentFiles(Context context, long accountId) {
3758a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        File[] files = getAttachmentDirectory(context, accountId).listFiles();
3768a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        if (files == null) return;
3778a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        for (File file : files) {
3788a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            boolean result = file.delete();
3798a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            if (!result) {
380560bfadc3151f7a06f3b06e9a6c92cfa534c63ecScott Kennedy                LogUtils.e(Logging.LOG_TAG, "Failed to delete attachment file " + file.getName());
3818a574694606f0e5d781334d0d426fc379c51f3edMarc Blank            }
3828a574694606f0e5d781334d0d426fc379c51f3edMarc Blank        }
3838a574694606f0e5d781334d0d426fc379c51f3edMarc Blank    }
384f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
385f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    private static long copyFile(InputStream in, OutputStream out) throws IOException {
386f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        long size = IOUtils.copy(in, out);
387f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        in.close();
388f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        out.flush();
389f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        out.close();
390f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        return size;
391f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    }
392f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
393f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    /**
394f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * Save the attachment to its final resting place (cache or sd card)
395f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     */
396f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    public static void saveAttachment(Context context, InputStream in, Attachment attachment) {
39780d76769852635096227ea467ce6375e61bfe01dTony Mantler        final Uri uri = ContentUris.withAppendedId(Attachment.CONTENT_URI, attachment.mId);
39880d76769852635096227ea467ce6375e61bfe01dTony Mantler        final ContentValues cv = new ContentValues();
39980d76769852635096227ea467ce6375e61bfe01dTony Mantler        final long attachmentId = attachment.mId;
40080d76769852635096227ea467ce6375e61bfe01dTony Mantler        final long accountId = attachment.mAccountKey;
40180d76769852635096227ea467ce6375e61bfe01dTony Mantler        final String contentUri;
40280d76769852635096227ea467ce6375e61bfe01dTony Mantler        final long size;
40380d76769852635096227ea467ce6375e61bfe01dTony Mantler
404f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        try {
405f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            ContentResolver resolver = context.getContentResolver();
406f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            if (attachment.mUiDestination == UIProvider.AttachmentDestination.CACHE) {
407f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                Uri attUri = getAttachmentUri(accountId, attachmentId);
408f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                size = copyFile(in, resolver.openOutputStream(attUri));
409f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                contentUri = attUri.toString();
410f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            } else if (Utility.isExternalStorageMounted()) {
411d0b54a29489314935214db920180fa208fc3a8ecTony Mantler                if (TextUtils.isEmpty(attachment.mFileName)) {
412feacaf6351ad981890fd6dc790added452e3a570Yu Ping Hu                    // TODO: This will prevent a crash but does not surface the underlying problem
413feacaf6351ad981890fd6dc790added452e3a570Yu Ping Hu                    // to the user correctly.
414feacaf6351ad981890fd6dc790added452e3a570Yu Ping Hu                    LogUtils.w(Logging.LOG_TAG, "Trying to save an attachment with no name: %d",
415feacaf6351ad981890fd6dc790added452e3a570Yu Ping Hu                            attachmentId);
416feacaf6351ad981890fd6dc790added452e3a570Yu Ping Hu                    throw new IOException("Can't save an attachment with no name");
417feacaf6351ad981890fd6dc790added452e3a570Yu Ping Hu                }
418f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                File downloads = Environment.getExternalStoragePublicDirectory(
419f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                        Environment.DIRECTORY_DOWNLOADS);
420f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                downloads.mkdirs();
421f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                File file = Utility.createUniqueFile(downloads, attachment.mFileName);
422f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                size = copyFile(in, new FileOutputStream(file));
423f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                String absolutePath = file.getAbsolutePath();
424f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
425f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                // Although the download manager can scan media files, scanning only happens
426f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                // after the user clicks on the item in the Downloads app. So, we run the
427f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                // attachment through the media scanner ourselves so it gets added to
428f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                // gallery / music immediately.
429f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                MediaScannerConnection.scanFile(context, new String[] {absolutePath},
430f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                        null, null);
431f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
432aab514c942e8db9433f54652895e01516cdd588eTony Mantler                final String mimeType = TextUtils.isEmpty(attachment.mMimeType) ?
433aab514c942e8db9433f54652895e01516cdd588eTony Mantler                        "application/octet-stream" :
434aab514c942e8db9433f54652895e01516cdd588eTony Mantler                        attachment.mMimeType;
435aab514c942e8db9433f54652895e01516cdd588eTony Mantler
43632471fc22e104156cf37ae36db05c4ab64722cebTony Mantler                try {
43732471fc22e104156cf37ae36db05c4ab64722cebTony Mantler                    DownloadManager dm =
43832471fc22e104156cf37ae36db05c4ab64722cebTony Mantler                            (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
43932471fc22e104156cf37ae36db05c4ab64722cebTony Mantler                    long id = dm.addCompletedDownload(attachment.mFileName, attachment.mFileName,
44032471fc22e104156cf37ae36db05c4ab64722cebTony Mantler                            false /* do not use media scanner */,
441aab514c942e8db9433f54652895e01516cdd588eTony Mantler                            mimeType, absolutePath, size,
44232471fc22e104156cf37ae36db05c4ab64722cebTony Mantler                            true /* show notification */);
44332471fc22e104156cf37ae36db05c4ab64722cebTony Mantler                    contentUri = dm.getUriForDownloadedFile(id).toString();
44432471fc22e104156cf37ae36db05c4ab64722cebTony Mantler                } catch (final IllegalArgumentException e) {
44532471fc22e104156cf37ae36db05c4ab64722cebTony Mantler                    LogUtils.d(LogUtils.TAG, e, "IAE from DownloadManager while saving attachment");
44632471fc22e104156cf37ae36db05c4ab64722cebTony Mantler                    throw new IOException(e);
44732471fc22e104156cf37ae36db05c4ab64722cebTony Mantler                }
448f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            } else {
449560bfadc3151f7a06f3b06e9a6c92cfa534c63ecScott Kennedy                LogUtils.w(Logging.LOG_TAG,
450560bfadc3151f7a06f3b06e9a6c92cfa534c63ecScott Kennedy                        "Trying to save an attachment without external storage?");
451f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                throw new IOException();
452f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            }
453f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
454f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            // Update the attachment
455f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            cv.put(AttachmentColumns.SIZE, size);
456f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            cv.put(AttachmentColumns.CONTENT_URI, contentUri);
457f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.SAVED);
458f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        } catch (IOException e) {
459f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            // Handle failures here...
460f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            cv.put(AttachmentColumns.UI_STATE, UIProvider.AttachmentState.FAILED);
461f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        }
462f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        context.getContentResolver().update(uri, cv, null, null);
463f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    }
4648a574694606f0e5d781334d0d426fc379c51f3edMarc Blank}
465