• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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