1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.emailcommon.utility; 18 19 import com.android.emailcommon.Logging; 20 import com.android.emailcommon.provider.EmailContent.Attachment; 21 import com.android.emailcommon.provider.EmailContent.Message; 22 import com.android.emailcommon.provider.EmailContent.MessageColumns; 23 24 import android.content.ContentResolver; 25 import android.content.ContentUris; 26 import android.content.Context; 27 import android.database.Cursor; 28 import android.net.Uri; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import android.webkit.MimeTypeMap; 32 33 import java.io.File; 34 35 public class AttachmentUtilities { 36 public static final String AUTHORITY = "com.android.email.attachmentprovider"; 37 public static final Uri CONTENT_URI = Uri.parse( "content://" + AUTHORITY); 38 39 public static final String FORMAT_RAW = "RAW"; 40 public static final String FORMAT_THUMBNAIL = "THUMBNAIL"; 41 42 public static class Columns { 43 public static final String _ID = "_id"; 44 public static final String DATA = "_data"; 45 public static final String DISPLAY_NAME = "_display_name"; 46 public static final String SIZE = "_size"; 47 } 48 49 /** 50 * The MIME type(s) of attachments we're willing to send via attachments. 51 * 52 * Any attachments may be added via Intents with Intent.ACTION_SEND or ACTION_SEND_MULTIPLE. 53 */ 54 public static final String[] ACCEPTABLE_ATTACHMENT_SEND_INTENT_TYPES = new String[] { 55 "*/*", 56 }; 57 /** 58 * The MIME type(s) of attachments we're willing to send from the internal UI. 59 * 60 * NOTE: At the moment it is not possible to open a chooser with a list of filter types, so 61 * the chooser is only opened with the first item in the list. 62 */ 63 public static final String[] ACCEPTABLE_ATTACHMENT_SEND_UI_TYPES = new String[] { 64 "image/*", 65 "video/*", 66 }; 67 /** 68 * The MIME type(s) of attachments we're willing to view. 69 */ 70 public static final String[] ACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] { 71 "*/*", 72 }; 73 /** 74 * The MIME type(s) of attachments we're not willing to view. 75 */ 76 public static final String[] UNACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] { 77 }; 78 /** 79 * The MIME type(s) of attachments we're willing to download to SD. 80 */ 81 public static final String[] ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] { 82 "*/*", 83 }; 84 /** 85 * The MIME type(s) of attachments we're not willing to download to SD. 86 */ 87 public static final String[] UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] { 88 }; 89 /** 90 * Filename extensions of attachments we're never willing to download (potential malware). 91 * Entries in this list are compared to the end of the lower-cased filename, so they must 92 * be lower case, and should not include a "." 93 */ 94 public static final String[] UNACCEPTABLE_ATTACHMENT_EXTENSIONS = new String[] { 95 // File types that contain malware 96 "ade", "adp", "bat", "chm", "cmd", "com", "cpl", "dll", "exe", 97 "hta", "ins", "isp", "jse", "lib", "mde", "msc", "msp", 98 "mst", "pif", "scr", "sct", "shb", "sys", "vb", "vbe", 99 "vbs", "vxd", "wsc", "wsf", "wsh", 100 // File types of common compression/container formats (again, to avoid malware) 101 "zip", "gz", "z", "tar", "tgz", "bz2", 102 }; 103 /** 104 * Filename extensions of attachments that can be installed. 105 * Entries in this list are compared to the end of the lower-cased filename, so they must 106 * be lower case, and should not include a "." 107 */ 108 public static final String[] INSTALLABLE_ATTACHMENT_EXTENSIONS = new String[] { 109 "apk", 110 }; 111 /** 112 * The maximum size of an attachment we're willing to download (either View or Save) 113 * Attachments that are base64 encoded (most) will be about 1.375x their actual size 114 * so we should probably factor that in. A 5MB attachment will generally be around 115 * 6.8MB downloaded but only 5MB saved. 116 */ 117 public static final int MAX_ATTACHMENT_DOWNLOAD_SIZE = (5 * 1024 * 1024); 118 /** 119 * The maximum size of an attachment we're willing to upload (measured as stored on disk). 120 * Attachments that are base64 encoded (most) will be about 1.375x their actual size 121 * so we should probably factor that in. A 5MB attachment will generally be around 122 * 6.8MB uploaded. 123 */ 124 public static final int MAX_ATTACHMENT_UPLOAD_SIZE = (5 * 1024 * 1024); 125 126 public static Uri getAttachmentUri(long accountId, long id) { 127 return CONTENT_URI.buildUpon() 128 .appendPath(Long.toString(accountId)) 129 .appendPath(Long.toString(id)) 130 .appendPath(FORMAT_RAW) 131 .build(); 132 } 133 134 public static Uri getAttachmentThumbnailUri(long accountId, long id, 135 int width, int height) { 136 return CONTENT_URI.buildUpon() 137 .appendPath(Long.toString(accountId)) 138 .appendPath(Long.toString(id)) 139 .appendPath(FORMAT_THUMBNAIL) 140 .appendPath(Integer.toString(width)) 141 .appendPath(Integer.toString(height)) 142 .build(); 143 } 144 145 /** 146 * Return the filename for a given attachment. This should be used by any code that is 147 * going to *write* attachments. 148 * 149 * This does not create or write the file, or even the directories. It simply builds 150 * the filename that should be used. 151 */ 152 public static File getAttachmentFilename(Context context, long accountId, long attachmentId) { 153 return new File(getAttachmentDirectory(context, accountId), Long.toString(attachmentId)); 154 } 155 156 /** 157 * Return the directory for a given attachment. This should be used by any code that is 158 * going to *write* attachments. 159 * 160 * This does not create or write the directory. It simply builds the pathname that should be 161 * used. 162 */ 163 public static File getAttachmentDirectory(Context context, long accountId) { 164 return context.getDatabasePath(accountId + ".db_att"); 165 } 166 167 /** 168 * Helper to convert unknown or unmapped attachments to something useful based on filename 169 * extensions. The mime type is inferred based upon the table below. It's not perfect, but 170 * it helps. 171 * 172 * <pre> 173 * |---------------------------------------------------------| 174 * | E X T E N S I O N | 175 * |---------------------------------------------------------| 176 * | .eml | known(.png) | unknown(.abc) | none | 177 * | M |-----------------------------------------------------------------------| 178 * | I | none | msg/rfc822 | image/png | app/abc | app/oct-str | 179 * | M |-------------| (always | | | | 180 * | E | app/oct-str | overrides | | | | 181 * | T |-------------| | |-----------------------------| 182 * | Y | text/plain | | | text/plain | 183 * | P |-------------| |-------------------------------------------| 184 * | E | any/type | | any/type | 185 * |---|-----------------------------------------------------------------------| 186 * </pre> 187 * 188 * NOTE: Since mime types on Android are case-*sensitive*, return values are always in 189 * lower case. 190 * 191 * @param fileName The given filename 192 * @param mimeType The given mime type 193 * @return A likely mime type for the attachment 194 */ 195 public static String inferMimeType(final String fileName, final String mimeType) { 196 String resultType = null; 197 String fileExtension = getFilenameExtension(fileName); 198 boolean isTextPlain = "text/plain".equalsIgnoreCase(mimeType); 199 200 if ("eml".equals(fileExtension)) { 201 resultType = "message/rfc822"; 202 } else { 203 boolean isGenericType = 204 isTextPlain || "application/octet-stream".equalsIgnoreCase(mimeType); 205 // If the given mime type is non-empty and non-generic, return it 206 if (isGenericType || TextUtils.isEmpty(mimeType)) { 207 if (!TextUtils.isEmpty(fileExtension)) { 208 // Otherwise, try to find a mime type based upon the file extension 209 resultType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension); 210 if (TextUtils.isEmpty(resultType)) { 211 // Finally, if original mimetype is text/plain, use it; otherwise synthesize 212 resultType = isTextPlain ? mimeType : "application/" + fileExtension; 213 } 214 } 215 } else { 216 resultType = mimeType; 217 } 218 } 219 220 // No good guess could be made; use an appropriate generic type 221 if (TextUtils.isEmpty(resultType)) { 222 resultType = isTextPlain ? "text/plain" : "application/octet-stream"; 223 } 224 return resultType.toLowerCase(); 225 } 226 227 /** 228 * @return mime-type for a {@link Uri}. 229 * - Use {@link ContentResolver#getType} for a content: URI. 230 * - Use {@link #inferMimeType} for a file: URI. 231 * - Otherwise throw {@link IllegalArgumentException}. 232 */ 233 public static String inferMimeTypeForUri(Context context, Uri uri) { 234 final String scheme = uri.getScheme(); 235 if ("content".equals(scheme)) { 236 return context.getContentResolver().getType(uri); 237 } else if ("file".equals(scheme)) { 238 return inferMimeType(uri.getLastPathSegment(), ""); 239 } else { 240 throw new IllegalArgumentException(); 241 } 242 } 243 244 /** 245 * Extract and return filename's extension, converted to lower case, and not including the "." 246 * 247 * @return extension, or null if not found (or null/empty filename) 248 */ 249 public static String getFilenameExtension(String fileName) { 250 String extension = null; 251 if (!TextUtils.isEmpty(fileName)) { 252 int lastDot = fileName.lastIndexOf('.'); 253 if ((lastDot > 0) && (lastDot < fileName.length() - 1)) { 254 extension = fileName.substring(lastDot + 1).toLowerCase(); 255 } 256 } 257 return extension; 258 } 259 260 /** 261 * Resolve attachment id to content URI. Returns the resolved content URI (from the attachment 262 * DB) or, if not found, simply returns the incoming value. 263 * 264 * @param attachmentUri 265 * @return resolved content URI 266 * 267 * TODO: Throws an SQLite exception on a missing DB file (e.g. unknown URI) instead of just 268 * returning the incoming uri, as it should. 269 */ 270 public static Uri resolveAttachmentIdToContentUri(ContentResolver resolver, Uri attachmentUri) { 271 Cursor c = resolver.query(attachmentUri, 272 new String[] { Columns.DATA }, 273 null, null, null); 274 if (c != null) { 275 try { 276 if (c.moveToFirst()) { 277 final String strUri = c.getString(0); 278 if (strUri != null) { 279 return Uri.parse(strUri); 280 } 281 } 282 } finally { 283 c.close(); 284 } 285 } 286 return attachmentUri; 287 } 288 289 /** 290 * In support of deleting a message, find all attachments and delete associated attachment 291 * files. 292 * @param context 293 * @param accountId the account for the message 294 * @param messageId the message 295 */ 296 public static void deleteAllAttachmentFiles(Context context, long accountId, long messageId) { 297 Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId); 298 Cursor c = context.getContentResolver().query(uri, Attachment.ID_PROJECTION, 299 null, null, null); 300 try { 301 while (c.moveToNext()) { 302 long attachmentId = c.getLong(Attachment.ID_PROJECTION_COLUMN); 303 File attachmentFile = getAttachmentFilename(context, accountId, attachmentId); 304 // Note, delete() throws no exceptions for basic FS errors (e.g. file not found) 305 // it just returns false, which we ignore, and proceed to the next file. 306 // This entire loop is best-effort only. 307 attachmentFile.delete(); 308 } 309 } finally { 310 c.close(); 311 } 312 } 313 314 /** 315 * In support of deleting a mailbox, find all messages and delete their attachments. 316 * 317 * @param context 318 * @param accountId the account for the mailbox 319 * @param mailboxId the mailbox for the messages 320 */ 321 public static void deleteAllMailboxAttachmentFiles(Context context, long accountId, 322 long mailboxId) { 323 Cursor c = context.getContentResolver().query(Message.CONTENT_URI, 324 Message.ID_COLUMN_PROJECTION, MessageColumns.MAILBOX_KEY + "=?", 325 new String[] { Long.toString(mailboxId) }, null); 326 try { 327 while (c.moveToNext()) { 328 long messageId = c.getLong(Message.ID_PROJECTION_COLUMN); 329 deleteAllAttachmentFiles(context, accountId, messageId); 330 } 331 } finally { 332 c.close(); 333 } 334 } 335 336 /** 337 * In support of deleting or wiping an account, delete all related attachments. 338 * 339 * @param context 340 * @param accountId the account to scrub 341 */ 342 public static void deleteAllAccountAttachmentFiles(Context context, long accountId) { 343 File[] files = getAttachmentDirectory(context, accountId).listFiles(); 344 if (files == null) return; 345 for (File file : files) { 346 boolean result = file.delete(); 347 if (!result) { 348 Log.e(Logging.LOG_TAG, "Failed to delete attachment file " + file.getName()); 349 } 350 } 351 } 352 } 353