/* * Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria) * * 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.arialyy.aria.http; import android.net.TrafficStats; import android.net.Uri; import android.os.Process; import android.text.TextUtils; import com.arialyy.aria.core.AriaConfig; import com.arialyy.aria.core.common.CompleteInfo; import com.arialyy.aria.core.common.RequestEnum; import com.arialyy.aria.core.download.DTaskWrapper; import com.arialyy.aria.core.download.DownloadEntity; import com.arialyy.aria.core.inf.OnFileInfoCallback; import com.arialyy.aria.core.processor.IHttpFileLenAdapter; import com.arialyy.aria.exception.AriaIOException; import com.arialyy.aria.exception.BaseException; import com.arialyy.aria.exception.TaskException; import com.arialyy.aria.util.ALog; import com.arialyy.aria.util.CheckUtil; import com.arialyy.aria.util.CommonUtil; import com.arialyy.aria.util.FileUtil; import com.arialyy.aria.util.RecordUtil; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.CookieManager; import java.net.HttpCookie; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; /** * 下载文件信息获取 */ public class HttpFileInfoThread implements Runnable { private static final String TAG = "HttpFileInfoThread"; private DownloadEntity mEntity; private DTaskWrapper mTaskWrapper; private int mConnectTimeOut; private OnFileInfoCallback onFileInfoCallback; private HttpTaskOption taskOption; public HttpFileInfoThread(DTaskWrapper taskWrapper, OnFileInfoCallback callback) { this.mTaskWrapper = taskWrapper; mEntity = taskWrapper.getEntity(); mConnectTimeOut = AriaConfig.getInstance().getDConfig().getConnectTimeOut(); onFileInfoCallback = callback; taskOption = (HttpTaskOption) taskWrapper.getTaskOption(); } @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); TrafficStats.setThreadStatsTag(UUID.randomUUID().toString().hashCode()); HttpURLConnection conn = null; try { URL url = ConnectionHelp.handleUrl(mEntity.getUrl(), taskOption); conn = ConnectionHelp.handleConnection(url, taskOption); ConnectionHelp.setConnectParam(taskOption, conn); conn.setRequestProperty("Range", "bytes=" + 0 + "-"); conn.setConnectTimeout(mConnectTimeOut); conn.connect(); handleConnect(conn); } catch (IOException e) { e.printStackTrace(); failDownload(new AriaIOException(TAG, String.format("下载失败,filePath: %s, url: %s", mEntity.getDownloadPath(), mEntity.getUrl())), true); } finally { if (conn != null) { try { conn.getInputStream().close(); } catch (IOException e) { e.printStackTrace(); } conn.disconnect(); } } } private void handleConnect(HttpURLConnection conn) throws IOException { if (taskOption.getRequestEnum() == RequestEnum.POST) { Map params = taskOption.getParams(); if (params != null) { OutputStreamWriter dos = new OutputStreamWriter(conn.getOutputStream()); Set keys = params.keySet(); StringBuilder sb = new StringBuilder(); for (String key : keys) { sb.append(key).append("=").append(URLEncoder.encode(params.get(key))).append("&"); } String url = sb.toString(); url = url.substring(0, url.length() - 1); dos.write(url); dos.flush(); dos.close(); } } IHttpFileLenAdapter lenAdapter = taskOption.getFileLenAdapter(); if (lenAdapter == null) { lenAdapter = new FileLenAdapter(); } else { ALog.d(TAG, "使用自定义adapter"); } long len = lenAdapter.handleFileLen(conn.getHeaderFields()); if (!FileUtil.checkMemorySpace(mEntity.getFilePath(), len)) { failDownload(new TaskException(TAG, String.format("下载失败,内存空间不足;filePath: %s, url: %s", mEntity.getDownloadPath(), mEntity.getUrl())), false); return; } int code = conn.getResponseCode(); boolean end = false; if (TextUtils.isEmpty(mEntity.getMd5Code())) { String md5Code = conn.getHeaderField("Content-MD5"); mEntity.setMd5Code(md5Code); } boolean isChunked = false; final String str = conn.getHeaderField("Transfer-Encoding"); if (!TextUtils.isEmpty(str) && str.equals("chunked")) { isChunked = true; } Map> headers = conn.getHeaderFields(); String disposition = conn.getHeaderField("Content-Disposition"); if (taskOption.isUseServerFileName()) { if (!TextUtils.isEmpty(disposition)) { mEntity.setDisposition(CommonUtil.encryptBASE64(disposition)); handleContentDisposition(disposition); } else { ALog.w(TAG, "Content-Disposition对于端字段为空,使用服务器端文件名失败"); } } CookieManager msCookieManager = new CookieManager(); List cookiesHeader = headers.get("Set-Cookie"); if (cookiesHeader != null) { for (String cookie : cookiesHeader) { msCookieManager.getCookieStore().add(null, HttpCookie.parse(cookie).get(0)); } taskOption.setCookieManager(msCookieManager); } mTaskWrapper.setCode(code); if (code == HttpURLConnection.HTTP_PARTIAL) { if (!checkLen(len) && !isChunked) { if (len < 0) { failDownload( new AriaIOException(TAG, String.format("任务下载失败,文件长度小于0, url: %s", mEntity.getUrl())), false); } return; } mEntity.setFileSize(len); mTaskWrapper.setSupportBP(true); end = true; } else if (code == HttpURLConnection.HTTP_OK) { String contentType = conn.getHeaderField("Content-Type"); if (TextUtils.isEmpty(contentType)) { return; } if (contentType.equals("text/html")) { BufferedReader reader = new BufferedReader(new InputStreamReader(ConnectionHelp.convertInputStream(conn))); StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line); } reader.close(); handleUrlReTurn(conn, CommonUtil.getWindowReplaceUrl(sb.toString())); return; } else if (!checkLen(len) && !isChunked) { if (len < 0) { failDownload( new AriaIOException(TAG, String.format("任务下载失败,文件长度小于0, url: %s", mEntity.getUrl())), false); } ALog.d(TAG, "len < 0"); return; } mEntity.setFileSize(len); mTaskWrapper.setNewTask(true); mTaskWrapper.setSupportBP(false); end = true; } else if (code == HttpURLConnection.HTTP_NOT_FOUND) { failDownload(new AriaIOException(TAG, String.format("任务下载失败,errorCode:404, url: %s", mEntity.getUrl())), true); } else if (code == HttpURLConnection.HTTP_MOVED_TEMP || code == HttpURLConnection.HTTP_MOVED_PERM || code == HttpURLConnection.HTTP_SEE_OTHER || code == HttpURLConnection.HTTP_CREATED // 201 跳转 || code == 307) { handleUrlReTurn(conn, conn.getHeaderField("Location")); } else { failDownload(new AriaIOException(TAG, String.format("任务下载失败,errorCode:%s, errorMsg: %s, url: %s", code, conn.getResponseMessage(), mEntity.getUrl())), !CheckUtil.httpIsBadRequest(code)); } if (end) { taskOption.setChunked(isChunked); if (onFileInfoCallback != null) { CompleteInfo info = new CompleteInfo(code, mTaskWrapper); onFileInfoCallback.onComplete(mEntity.getUrl(), info); } mEntity.update(); } } /** * 处理"Content-Disposition"参数 * Content-Disposition * * @throws UnsupportedEncodingException */ private void handleContentDisposition(String disposition) throws UnsupportedEncodingException { if (disposition.contains(";")) { String[] infos = disposition.split(";"); if (infos[0].equals("attachment")) { for (String info : infos) { if (info.startsWith("filename") && info.contains("=")) { String[] temp = info.split("="); if (temp.length > 1) { String newName = URLDecoder.decode(temp[1], "utf-8").replaceAll("\"", ""); mEntity.setServerFileName(newName); renameFile(newName); break; } } } } else if (infos[0].equals("form-data") && infos.length > 2) { String[] temp = infos[2].split("="); if (temp.length > 1) { String newName = URLDecoder.decode(temp[1], "utf-8").replaceAll("\"", ""); mEntity.setServerFileName(newName); renameFile(newName); } } else { ALog.w(TAG, "不识别的Content-Disposition参数"); } } } /** * 重命名文件 */ private void renameFile(String newName) { if (TextUtils.isEmpty(newName)) { ALog.w(TAG, "重命名失败【服务器返回的文件名为空】"); return; } ALog.d(TAG, String.format("文件重命名为:%s", newName)); File oldFile = new File(mEntity.getFilePath()); String newPath = oldFile.getParent() + "/" + newName; if (oldFile.exists()) { boolean b = oldFile.renameTo(new File(newPath)); ALog.d(TAG, String.format("文件重命名%s", b ? "成功" : "失败")); } mEntity.setFileName(newName); mEntity.setFilePath(newPath); RecordUtil.modifyTaskRecord(oldFile.getPath(), newPath, mEntity.getTaskType()); } /** * 处理30x跳转 */ private void handleUrlReTurn(HttpURLConnection conn, String newUrl) throws IOException { ALog.d(TAG, "30x跳转,新url为【" + newUrl + "】"); if (TextUtils.isEmpty(newUrl) || newUrl.equalsIgnoreCase("null")) { if (onFileInfoCallback != null) { onFileInfoCallback.onFail(mEntity, new TaskException(TAG, "获取重定向链接失败"), false); } return; } if (newUrl.startsWith("/")) { Uri uri = Uri.parse(mEntity.getUrl()); newUrl = uri.getHost() + newUrl; } if (!CheckUtil.checkUrl(newUrl)) { failDownload(new TaskException(TAG, "下载失败,重定向url错误"), false); return; } taskOption.setRedirectUrl(newUrl); mEntity.setRedirect(true); mEntity.setRedirectUrl(newUrl); String cookies = conn.getHeaderField("Set-Cookie"); conn.disconnect(); URL url = ConnectionHelp.handleUrl(newUrl, taskOption); conn = ConnectionHelp.handleConnection(url, taskOption); ConnectionHelp.setConnectParam(taskOption, conn); conn.setRequestProperty("Cookie", cookies); conn.setRequestProperty("Range", "bytes=" + 0 + "-"); conn.setConnectTimeout(mConnectTimeOut); conn.connect(); handleConnect(conn); conn.disconnect(); } /** * 检查长度是否合法,并且检查新获取的文件长度是否和数据库的文件长度一直,如果不一致,则表示该任务为新任务 * * @param len 从服务器获取的文件长度 * @return {@code true}合法 */ private boolean checkLen(long len) { if (len != mEntity.getFileSize()) { ALog.d(TAG, "长度不一致,任务为新任务"); mTaskWrapper.setNewTask(true); } return true; } private void failDownload(BaseException e, boolean needRetry) { if (onFileInfoCallback != null) { onFileInfoCallback.onFail(mEntity, e, needRetry); } } private static class FileLenAdapter implements IHttpFileLenAdapter { @Override public long handleFileLen(Map> headers) { if (headers == null || headers.isEmpty()) { ALog.e(TAG, "header为空,获取文件长度失败"); return -1; } List sLength = headers.get("Content-Length"); if (sLength == null || sLength.isEmpty()) { return -1; } String temp = sLength.get(0); long len = TextUtils.isEmpty(temp) ? -1 : Long.parseLong(temp); // 某些服务,如果设置了conn.setRequestProperty("Range", "bytes=" + 0 + "-"); // 会返回 Content-Range: bytes 0-225427911/225427913 if (len < 0) { List sRange = headers.get("Content-Range"); if (sRange == null || sRange.isEmpty()) { len = -1; } else { int start = temp.indexOf("/"); len = Long.parseLong(temp.substring(start + 1)); } } return len; } } }