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