Home | History | Annotate | Download | only in utils
      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      */
     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      */
     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      */
    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.
    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 
    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      */
    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