/* * Copyright (C) 2010 mAPPn.Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.lan.nicehair.common.download; import java.io.File; import java.util.Random; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Environment; import android.os.StatFs; import android.os.SystemClock; import android.webkit.MimeTypeMap; import com.lan.nicehair.utils.AppLog; import com.lan.nicehair.utils.Utils; /** * 下载服务工具类 * * @author andrew * @date 2011-4-27 * */ public class Helper { private static final String TAG = null; public static Random rnd = new Random(SystemClock.uptimeMillis()); /** * Exception thrown from methods called by generateSaveFile() for any fatal error. */ public static class GenerateSaveFileError extends Exception { /** */ private static final long serialVersionUID = 7750062109363258607L; int mStatus; String mMessage; public GenerateSaveFileError(int status, String message) { mStatus = status; mMessage = message; } } /** * Creates a filename (where the file should be saved) from info about a download. */ public static String generateSaveFile( Context context, String url, String hint, String contentLocation, String mimeType, int destination, long contentLength, int source) throws GenerateSaveFileError { return chooseFullPath(context, url, hint, contentLocation, mimeType, destination, contentLength, source); } /** * 通过MIME TYPE判断文件的后缀名 * @param mimeType MIME TYPE * @param useDefaults 是否使用默认后缀 * @return 后缀名,可能为Null */ private static String chooseExtensionFromMimeType(String mimeType, boolean useDefaults) { String extension = null; if (mimeType != null) { extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); if (extension != null) { AppLog.d(TAG,"adding extension from MIME type."); extension = "." + extension; return extension; } else { AppLog.d(TAG,"couldn't find extension for " + mimeType); } if (mimeType.toLowerCase().startsWith("text/")) { if (mimeType.equalsIgnoreCase("text/html")) { AppLog.d(TAG,"adding default html extension"); extension = Constants.DEFAULT_DL_HTML_EXTENSION; return extension; } else if (useDefaults) { AppLog.d(TAG,"adding default text extension"); extension = Constants.DEFAULT_DL_TEXT_EXTENSION; return extension; } } } else if (useDefaults) { AppLog.d(TAG,"adding default binary extension"); extension = Constants.DEFAULT_DL_BINARY_EXTENSION; } return extension; } /** * 通过文件名判断后缀名 * @param mimeType MIME TYPE * @param filename 文件名 * @param dotIndex '.'下标 * @return 后缀名 */ private static String chooseExtensionFromFilename(String mimeType, String filename, int dotIndex) { String extension = null; if (mimeType != null) { // Compare the last segment of the extension against the mime type. // If there's a mismatch, discard the entire extension. int lastDotIndex = filename.lastIndexOf('.'); String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( filename.substring(lastDotIndex + 1)); if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) { extension = chooseExtensionFromMimeType(mimeType, false); if (extension != null) { AppLog.d(TAG,"substituting extension from type"); } else { AppLog.d(TAG,"couldn't find extension for " + mimeType); } } } if (extension == null) { AppLog.d(TAG,"keeping extension"); extension = filename.substring(dotIndex); } return extension; } /** * 外部存储设备是否就绪 */ public static boolean isExternalMediaMounted() { if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { // No SD card found. AppLog.d(TAG,"no external storage"); return false; } return true; } /** * 获取内部存储地址 * * @throws GenerateSaveFileError 如果存储区域大小不足,会抛出此异常 */ private static File getCacheDestination(Context context, long contentLength) throws GenerateSaveFileError { File base = context.getCacheDir(); if (getAvailableBytes(base) < contentLength) { // No files to purge, give up. AppLog.d(TAG,"download aborted - not enough internal free space"); throw new GenerateSaveFileError( DownloadManager.Impl.STATUS_INSUFFICIENT_SPACE_ERROR, "not enough free space in internal download storage, unable to free any " + "more"); } return base; } /** * 获取外部存储地址 * @param source 请求来源 * @param mimeType 文件MIME TYPE * * @throws GenerateSaveFileError 如果外部存储区域没有被加载、只读、存储大小不足,会抛出此异常 */ private static File getExternalDestination(long contentLength, int source, String mimeType) throws GenerateSaveFileError { if (!isExternalMediaMounted()) { throw new GenerateSaveFileError(DownloadManager.Impl.STATUS_DEVICE_NOT_FOUND_ERROR, "external media not mounted"); } File root = Environment.getExternalStorageDirectory(); if (getAvailableBytes(root) < contentLength) { // Insufficient space. AppLog.d(TAG,"download aborted - not enough external free space"); throw new GenerateSaveFileError(DownloadManager.Impl.STATUS_INSUFFICIENT_SPACE_ERROR, "insufficient space on external media"); } // 通过下载请求来源确定文件路径(机锋市场,云推送,升级...) File base = null; if(Constants.DOWNLOAD_FROM_MARKET == source) { base = new File(root.getPath(), Constants.DEFAULT_MARKET_SUBDIR); } else if(Constants.DOWNLOAD_FROM_BBS == source) { base = new File(root.getPath(), Constants.DEFAULT_BBS_SUBDIR); } else if(Constants.DOWNLOAD_FROM_CLOUD == source) { base = new File(root.getPath(), Constants.DEFAULT_CLOUD_SUBDIR); } if (!base.isDirectory() && !base.mkdirs()) { // Can't create download directory, e.g. because a file called "download" // already exists at the root level, or the SD card filesystem is read-only. throw new GenerateSaveFileError(DownloadManager.Impl.STATUS_FILE_ERROR, "unable to create external downloads directory " + base.getPath()); } return base; } /** * @return the number of bytes available on the filesystem rooted at the given File */ public static long getAvailableBytes(File root) { StatFs stat = new StatFs(root.getPath()); // put a bit of margin (in case creating the file grows the system by a few blocks) long availableBlocks = (long) stat.getAvailableBlocks() - 4; return stat.getBlockSize() * availableBlocks; } /** * 获取网络类型 */ public static Integer getActiveNetworkType(Context context) { ConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); if (connectivity == null) { AppLog.d(TAG,"couldn't get connectivity manager"); return null; } NetworkInfo activeInfo = connectivity.getActiveNetworkInfo(); if (activeInfo == null) { AppLog.d(TAG,"network is not available"); return null; } return activeInfo.getType(); } /** * Returns whether the network is available */ public static boolean isNetworkAvailable(Context context) { ConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); if (connectivity == null) { AppLog.d(TAG,"couldn't get connectivity manager"); } else { NetworkInfo[] info = connectivity.getAllNetworkInfo(); if (info != null) { for (int i = 0; i < info.length; i++) { if (info[i].getState() == NetworkInfo.State.CONNECTED) { AppLog.d(TAG,"network is available"); return true; } } } } AppLog.d(TAG,"network is not available"); return false; } /** * Checks whether the filename looks legitimate */ public static boolean isFilenameValid(String filename, int sourceType) { File dir = new File(filename).getParentFile(); if (Constants.DOWNLOAD_FROM_MARKET == sourceType) { return dir.equals(new File(Environment.getExternalStorageDirectory(), Constants.DEFAULT_MARKET_SUBDIR)); } else if (Constants.DOWNLOAD_FROM_BBS == sourceType) { return dir.equals(new File(Environment.getExternalStorageDirectory(), Constants.DEFAULT_BBS_SUBDIR)); } else if (Constants.DOWNLOAD_FROM_CLOUD == sourceType) { return dir.equals(new File(Environment.getExternalStorageDirectory(), Constants.DEFAULT_CLOUD_SUBDIR)); } return dir.equals(new File(Environment.getExternalStorageDirectory() + Constants.DEFAULT_SUBDIR)); } /** * 获取文件存储的完全路径 */ private static String chooseFullPath(Context context, String url, String hint, String contentLocation, String mimeType, int destination, long contentLength, int source) throws GenerateSaveFileError { File base = locateDestinationDirectory(context, mimeType, source, destination, contentLength); String filename = chooseFilename(url, hint, contentLocation, destination); // Split filename between base and extension // Add an extension if filename does not have one String extension = null; int dotIndex = filename.indexOf('.'); if (dotIndex < 0) { extension = chooseExtensionFromMimeType(mimeType, true); } else { extension = chooseExtensionFromFilename(mimeType, filename, dotIndex); filename = filename.substring(0, dotIndex); } boolean recoveryDir = Constants.RECOVERY_DIRECTORY .equalsIgnoreCase(filename + extension); filename = base.getPath() + File.separator + filename; AppLog.d(TAG,"target file: " + filename + extension); return chooseUniqueFilename(destination, filename, extension, recoveryDir); } private static File locateDestinationDirectory(Context context, String mimeType, int source, int destination, long contentLength) throws GenerateSaveFileError { if (destination == DownloadManager.Impl.DESTINATION_CACHE_PARTITION) { return getCacheDestination(context, contentLength); } return getExternalDestination(contentLength, source, mimeType); } private static String chooseFilename(String url, String hint, String contentLocation, int destination) { String filename = null; // First, try to use the hint from the application, if there's one if (filename == null && hint != null && !hint.endsWith("/")) { AppLog.i(TAG,"getting filename from hint"); int index = hint.lastIndexOf('/') + 1; if (index > 0) { filename = hint.substring(index); } else { filename = hint; } } // If we still have nothing at this point, try the content location if (filename == null && contentLocation != null) { String decodedContentLocation = Uri.decode(contentLocation); if (decodedContentLocation != null && !decodedContentLocation.endsWith("/") && decodedContentLocation.indexOf('?') < 0) { AppLog.i(TAG,"getting filename from content-location"); int index = decodedContentLocation.lastIndexOf('/') + 1; if (index > 0) { filename = decodedContentLocation.substring(index); } else { filename = decodedContentLocation; } } } // If all the other http-related approaches failed, use the plain uri if (filename == null) { String decodedUrl = Uri.decode(url); if (decodedUrl != null && !decodedUrl.endsWith("/") && decodedUrl.indexOf('?') < 0) { int index = decodedUrl.lastIndexOf('/') + 1; if (index > 0) { AppLog.i(TAG,"getting filename from uri"); filename = decodedUrl.substring(index); } } } // Finally, if couldn't get filename from URI, get a generic filename if (filename == null) { AppLog.i(TAG,"using default filename"); filename = Constants.DEFAULT_DL_FILENAME; } // The VFAT file system is assumed as target for downloads. // Replace invalid characters according to the specifications of VFAT. filename = replaceInvalidVfatCharacters(filename); return filename; } private static String chooseUniqueFilename(int destination, String filename, String extension, boolean recoveryDir) { String fullFilename = filename + extension; if (!new File(fullFilename).exists() && (!recoveryDir || destination != DownloadManager.Impl.DESTINATION_CACHE_PARTITION)) { return fullFilename; } filename = filename + Constants.FILENAME_SEQUENCE_SEPARATOR; /* * This number is used to generate partially randomized filenames to avoid * collisions. * It starts at 1. * The next 9 iterations increment it by 1 at a time (up to 10). * The next 9 iterations increment it by 1 to 10 (random) at a time. * The next 9 iterations increment it by 1 to 100 (random) at a time. * ... Up to the point where it increases by 100000000 at a time. * (the maximum value that can be reached is 1000000000) * As soon as a number is reached that generates a filename that doesn't exist, * that filename is used. * If the filename coming in is [base].[ext], the generated filenames are * [base]-[sequence].[ext]. */ int sequence = 1; for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) { for (int iteration = 0; iteration < 9; ++iteration) { fullFilename = filename + sequence + extension; if (!new File(fullFilename).exists()) { return fullFilename; } AppLog.i(TAG,"file with sequence number " + sequence + " exists"); sequence += rnd.nextInt(magnitude) + 1; } } return null; } /** * Replace invalid filename characters according to * specifications of the VFAT. * @note Package-private due to testing. */ private static String replaceInvalidVfatCharacters(String filename) { final char START_CTRLCODE = 0x00; final char END_CTRLCODE = 0x1f; final char QUOTEDBL = 0x22; final char ASTERISK = 0x2A; final char SLASH = 0x2F; final char COLON = 0x3A; final char LESS = 0x3C; final char GREATER = 0x3E; final char QUESTION = 0x3F; final char BACKSLASH = 0x5C; final char BAR = 0x7C; final char DEL = 0x7F; final char UNDERSCORE = 0x5F; StringBuffer sb = new StringBuffer(); char ch; boolean isRepetition = false; for (int i = 0; i < filename.length(); i++) { ch = filename.charAt(i); if ((START_CTRLCODE <= ch && ch <= END_CTRLCODE) || ch == QUOTEDBL || ch == ASTERISK || ch == SLASH || ch == COLON || ch == LESS || ch == GREATER || ch == QUESTION || ch == BACKSLASH || ch == BAR || ch == DEL){ if (!isRepetition) { sb.append(UNDERSCORE); isRepetition = true; } } else { sb.append(ch); isRepetition = false; } } return sb.toString(); } }