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 */
16package com.android.mail.utils;
17
18import com.google.common.collect.ImmutableMap;
19
20import android.app.DownloadManager;
21import android.content.Context;
22import android.net.ConnectivityManager;
23import android.net.NetworkInfo;
24import android.os.Bundle;
25import android.os.ParcelFileDescriptor;
26import android.os.SystemClock;
27import android.text.TextUtils;
28
29import com.android.mail.R;
30import com.android.mail.providers.Attachment;
31
32import java.io.FileInputStream;
33import java.io.FileNotFoundException;
34import java.io.FileOutputStream;
35import java.io.InputStream;
36import java.io.IOException;
37import java.io.File;
38import java.text.DecimalFormat;
39import java.text.SimpleDateFormat;
40import java.util.Date;
41import java.util.Map;
42
43public 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     */
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     */
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     */
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.
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
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     */
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