1 /* 2 * Copyright (C) 2012 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 package com.android.mail.utils; 17 18 import android.app.DownloadManager; 19 import android.content.Context; 20 import android.content.res.AssetFileDescriptor; 21 import android.content.res.AssetFileDescriptor.AutoCloseInputStream; 22 import android.net.ConnectivityManager; 23 import android.net.NetworkInfo; 24 import android.os.Bundle; 25 import android.os.ParcelFileDescriptor; 26 import android.os.SystemClock; 27 import android.text.TextUtils; 28 29 import com.android.mail.R; 30 import com.android.mail.providers.Attachment; 31 import com.google.common.collect.ImmutableMap; 32 33 import java.io.File; 34 import java.io.FileInputStream; 35 import java.io.FileNotFoundException; 36 import java.io.FileOutputStream; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.text.DecimalFormat; 40 import java.text.SimpleDateFormat; 41 import java.util.Date; 42 import java.util.Map; 43 44 public class AttachmentUtils { 45 private static final String LOG_TAG = LogTag.getLogTag(); 46 47 private static final int KILO = 1024; 48 private static final int MEGA = KILO * KILO; 49 50 /** Any IO reads should be limited to this timeout */ 51 private static final long READ_TIMEOUT = 3600 * 1000; 52 53 private static final float MIN_CACHE_THRESHOLD = 0.25f; 54 private static final int MIN_CACHE_AVAILABLE_SPACE_BYTES = 100 * 1024 * 1024; 55 56 /** 57 * Singleton map of MIME->friendly description 58 * @see #getMimeTypeDisplayName(Context, String) 59 */ 60 private static Map<String, String> sDisplayNameMap; 61 62 /** 63 * @return A string suitable for display in bytes, kilobytes or megabytes 64 * depending on its size. 65 */ convertToHumanReadableSize(Context context, long size)66 public static String convertToHumanReadableSize(Context context, long size) { 67 final String count; 68 if (size == 0) { 69 return ""; 70 } else if (size < KILO) { 71 count = String.valueOf(size); 72 return context.getString(R.string.bytes, count); 73 } else if (size < MEGA) { 74 count = String.valueOf(size / KILO); 75 return context.getString(R.string.kilobytes, count); 76 } else { 77 DecimalFormat onePlace = new DecimalFormat("0.#"); 78 count = onePlace.format((float) size / (float) MEGA); 79 return context.getString(R.string.megabytes, count); 80 } 81 } 82 83 /** 84 * Return a friendly localized file type for this attachment, or the empty string if 85 * unknown. 86 * @param context a Context to do resource lookup against 87 * @return friendly file type or empty string 88 */ getDisplayType(final Context context, final Attachment attachment)89 public static String getDisplayType(final Context context, final Attachment attachment) { 90 if ((attachment.flags & Attachment.FLAG_DUMMY_ATTACHMENT) != 0) { 91 // This is a dummy attachment, display blank for type. 92 return ""; 93 } 94 95 // try to get a friendly name for the exact mime type 96 // then try to show a friendly name for the mime family 97 // finally, give up and just show the file extension 98 final String contentType = attachment.getContentType(); 99 String displayType = getMimeTypeDisplayName(context, contentType); 100 int index = !TextUtils.isEmpty(contentType) ? contentType.indexOf('/') : -1; 101 if (displayType == null && index > 0) { 102 displayType = getMimeTypeDisplayName(context, contentType.substring(0, index)); 103 } 104 if (displayType == null) { 105 String extension = Utils.getFileExtension(attachment.getName()); 106 // show '$EXTENSION File' for unknown file types 107 if (extension != null && extension.length() > 1 && extension.indexOf('.') == 0) { 108 displayType = context.getString(R.string.attachment_unknown, 109 extension.substring(1).toUpperCase()); 110 } 111 } 112 if (displayType == null) { 113 // no extension to display, but the map doesn't accept null entries 114 displayType = ""; 115 } 116 return displayType; 117 } 118 119 /** 120 * Returns a user-friendly localized description of either a complete a MIME type or a 121 * MIME family. 122 * @param context used to look up localized strings 123 * @param type complete MIME type or just MIME family 124 * @return localized description text, or null if not recognized 125 */ getMimeTypeDisplayName(final Context context, String type)126 public static synchronized String getMimeTypeDisplayName(final Context context, 127 String type) { 128 if (sDisplayNameMap == null) { 129 String docName = context.getString(R.string.attachment_application_msword); 130 String presoName = context.getString(R.string.attachment_application_vnd_ms_powerpoint); 131 String sheetName = context.getString(R.string.attachment_application_vnd_ms_excel); 132 133 sDisplayNameMap = new ImmutableMap.Builder<String, String>() 134 .put("image", context.getString(R.string.attachment_image)) 135 .put("audio", context.getString(R.string.attachment_audio)) 136 .put("video", context.getString(R.string.attachment_video)) 137 .put("text", context.getString(R.string.attachment_text)) 138 .put("application/pdf", context.getString(R.string.attachment_application_pdf)) 139 140 // Documents 141 .put("application/msword", docName) 142 .put("application/vnd.openxmlformats-officedocument.wordprocessingml.document", 143 docName) 144 145 // Presentations 146 .put("application/vnd.ms-powerpoint", 147 presoName) 148 .put("application/vnd.openxmlformats-officedocument.presentationml.presentation", 149 presoName) 150 151 // Spreadsheets 152 .put("application/vnd.ms-excel", sheetName) 153 .put("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 154 sheetName) 155 156 .build(); 157 } 158 return sDisplayNameMap.get(type); 159 } 160 161 /** 162 * Cache the file specified by the given attachment. This will attempt to use any 163 * {@link ParcelFileDescriptor} in the Bundle parameter 164 * @param context 165 * @param attachment Attachment to be cached 166 * @param attachmentFds optional {@link Bundle} containing {@link ParcelFileDescriptor} if the 167 * caller has opened the files 168 * @return String file path for the cached attachment 169 */ 170 // TODO(pwestbro): Once the attachment has a field for the cached path, this method should be 171 // changed to update the attachment, and return a boolean indicating that the attachment has 172 // been cached. cacheAttachmentUri(Context context, Attachment attachment, Bundle attachmentFds)173 public static String cacheAttachmentUri(Context context, Attachment attachment, 174 Bundle attachmentFds) { 175 final File cacheDir = context.getCacheDir(); 176 177 final long totalSpace = cacheDir.getTotalSpace(); 178 if (attachment.size > 0) { 179 final long usableSpace = cacheDir.getUsableSpace() - attachment.size; 180 if (isLowSpace(totalSpace, usableSpace)) { 181 LogUtils.w(LOG_TAG, "Low memory (%d/%d). Can't cache attachment %s", 182 usableSpace, totalSpace, attachment); 183 return null; 184 } 185 } 186 InputStream inputStream = null; 187 FileOutputStream outputStream = null; 188 File file = null; 189 try { 190 final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-kk:mm:ss"); 191 file = File.createTempFile(dateFormat.format(new Date()), ".attachment", cacheDir); 192 final AssetFileDescriptor fileDescriptor = attachmentFds != null 193 && attachment.contentUri != null ? (AssetFileDescriptor) attachmentFds 194 .getParcelable(attachment.contentUri.toString()) 195 : null; 196 if (fileDescriptor != null) { 197 // Get the input stream from the file descriptor 198 inputStream = new AutoCloseInputStream(fileDescriptor); 199 } else { 200 if (attachment.contentUri == null) { 201 // The contentUri of the attachment is null. This can happen when sending a 202 // message that has been previously saved, and the attachments had been 203 // uploaded. 204 LogUtils.d(LOG_TAG, "contentUri is null in attachment: %s", attachment); 205 throw new FileNotFoundException("Missing contentUri in attachment"); 206 } 207 // Attempt to open the file 208 if (attachment.virtualMimeType == null) { 209 inputStream = context.getContentResolver().openInputStream(attachment.contentUri); 210 } else { 211 AssetFileDescriptor fd = context.getContentResolver().openTypedAssetFileDescriptor( 212 attachment.contentUri, attachment.virtualMimeType, null, null); 213 if (fd != null) { 214 inputStream = new AutoCloseInputStream(fd); 215 } 216 } 217 } 218 outputStream = new FileOutputStream(file); 219 final long now = SystemClock.elapsedRealtime(); 220 final byte[] bytes = new byte[1024]; 221 while (true) { 222 int len = inputStream.read(bytes); 223 if (len <= 0) { 224 break; 225 } 226 outputStream.write(bytes, 0, len); 227 if (SystemClock.elapsedRealtime() - now > READ_TIMEOUT) { 228 throw new IOException("Timed out reading attachment data"); 229 } 230 } 231 outputStream.flush(); 232 String cachedFileUri = file.getAbsolutePath(); 233 LogUtils.d(LOG_TAG, "Cached %s to %s", attachment.contentUri, cachedFileUri); 234 235 final long usableSpace = cacheDir.getUsableSpace(); 236 if (isLowSpace(totalSpace, usableSpace)) { 237 file.delete(); 238 LogUtils.w(LOG_TAG, "Low memory (%d/%d). Can't cache attachment %s", 239 usableSpace, totalSpace, attachment); 240 cachedFileUri = null; 241 } 242 243 return cachedFileUri; 244 } catch (IOException | SecurityException e) { 245 // Catch any exception here to allow for unexpected failures during caching se we don't 246 // leave app in inconsistent state as we call this method outside of a transaction for 247 // performance reasons. 248 LogUtils.e(LOG_TAG, e, "Failed to cache attachment %s", attachment); 249 if (file != null) { 250 file.delete(); 251 } 252 return null; 253 } finally { 254 try { 255 if (inputStream != null) { 256 inputStream.close(); 257 } 258 if (outputStream != null) { 259 outputStream.close(); 260 } 261 } catch (IOException e) { 262 LogUtils.w(LOG_TAG, e, "Failed to close stream"); 263 } 264 } 265 } 266 isLowSpace(long totalSpace, long usableSpace)267 private static boolean isLowSpace(long totalSpace, long usableSpace) { 268 // For caching attachments we want to enable caching if there is 269 // more than 100MB available, or if 25% of total space is free on devices 270 // where the cache partition is < 400MB. 271 return usableSpace < 272 Math.min(totalSpace * MIN_CACHE_THRESHOLD, MIN_CACHE_AVAILABLE_SPACE_BYTES); 273 } 274 275 /** 276 * Checks if the attachment can be downloaded with the current network 277 * connection. 278 * 279 * @param attachment the attachment to be checked 280 * @return true if the attachment can be downloaded. 281 */ canDownloadAttachment(Context context, Attachment attachment)282 public static boolean canDownloadAttachment(Context context, Attachment attachment) { 283 ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService( 284 Context.CONNECTIVITY_SERVICE); 285 NetworkInfo info = connectivityManager.getActiveNetworkInfo(); 286 if (info == null) { 287 return false; 288 } else if (info.isConnected()) { 289 if (info.getType() != ConnectivityManager.TYPE_MOBILE) { 290 // not mobile network 291 return true; 292 } else { 293 // mobile network 294 Long maxBytes = DownloadManager.getMaxBytesOverMobile(context); 295 return maxBytes == null || attachment == null || attachment.size <= maxBytes; 296 } 297 } else { 298 return false; 299 } 300 } 301 } 302