/* * Copyright (c) 2013. wyouflf (wyouflf@gmail.com) * * 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 org.robam.xutils.http; import android.os.SystemClock; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.StatusLine; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.impl.client.AbstractHttpClient; import org.apache.http.protocol.HttpContext; import org.robam.xutils.HttpUtils; import org.robam.xutils.Utils.LogUtils; import org.robam.xutils.Utils.OtherUtils; import org.robam.xutils.core.CompatibleAsyncTask; import org.robam.xutils.http.callback.DefaultHttpRedirectHandler; import org.robam.xutils.http.callback.FileDownloadHandler; import org.robam.xutils.http.callback.HttpRedirectHandler; import org.robam.xutils.http.callback.RequestCallBack; import org.robam.xutils.http.callback.RequestCallBackHandler; import org.robam.xutils.http.callback.StringDownloadHandler; import java.io.File; import java.io.IOException; import java.net.UnknownHostException; /** * 最关键的HTTP请求. * * @param <T> */ public class HttpHandler<T> extends CompatibleAsyncTask<Object, Object, Void> implements RequestCallBackHandler { private final AbstractHttpClient client; private final HttpContext context; private final StringDownloadHandler mStringDownloadHandler = new StringDownloadHandler(); private final FileDownloadHandler mFileDownloadHandler = new FileDownloadHandler(); private HttpRedirectHandler httpRedirectHandler; private String requestUrl; private String requestMethod; private HttpRequestBase request; private boolean isUploading = true; private final RequestCallBack<T> callback; private int retriedTimes = 0; private String fileSavePath = null; private boolean isDownloadingFile = false; /** * 自动从断点处下载 */ private boolean autoResume = false; /** * 是否根据响应头的重命名文件 */ private boolean autoRename = false; /** * 响应头的字符集 */ private String charset; // The default charset of response header info. public HttpHandler(AbstractHttpClient client, HttpContext context, String charset, RequestCallBack<T> callback) { this.client = client; this.context = context; this.callback = callback; this.charset = charset; } private State state = State.WAITING; public State getState() { return state; } private long expiry = HttpCache.getDefaultExpiryTime(); public void setExpiry(long expiry) { this.expiry = expiry; } public void setHttpRedirectHandler(HttpRedirectHandler httpRedirectHandler) { if (httpRedirectHandler != null) { this.httpRedirectHandler = httpRedirectHandler; } } /** * 这是请求,看准了. * 先查询是否断点续传,如果续传,先获取已有文件的长度,然后设置请求头从断点处开始. * * @param request Http请求客户端 * @return 返回的是响应信息 * @throws HttpException */ @SuppressWarnings("unchecked") private ResponseInfo<T> sendRequest(HttpRequestBase request) throws HttpException { if (autoResume && isDownloadingFile) { File downloadFile = new File(fileSavePath); long fileLen = 0; // 文件存在,先获取文件的长度 if (downloadFile.isFile() && downloadFile.exists()) { fileLen = downloadFile.length(); } if (fileLen > 0) { //文件长度大于0的,就从断点处开始,原来这么简单,就一个设置请求头RANGE request.setHeader("RANGE", "bytes=" + fileLen + "-"); } } // 重试 boolean retry = true; HttpRequestRetryHandler retryHandler = client.getHttpRequestRetryHandler(); //一直在重复请求,直到返回了. while (retry) { IOException exception = null; try { requestMethod = request.getMethod(); // 查询是否支持请求方法的缓存 if (HttpUtils.sHttpCache.isEnabled(requestMethod)) { String result = HttpUtils.sHttpCache.get(requestUrl); if (result != null) { // TODO:难道只要查询到了就马上返回了吗? return new ResponseInfo<T>(null, (T) result, true); } } ResponseInfo<T> responseInfo = null; if (!isCancelled()) { // 这就是真正的请求了!得到了Response HttpResponse response = client.execute(request, context); // 得到了响应 responseInfo = handleResponse(response); } return responseInfo; } catch (UnknownHostException e) { exception = e; // 注意:这只是根据错误和HttpContext判断是否需要重试. retry = retryHandler.retryRequest(exception, ++retriedTimes, context); } catch (IOException e) { exception = e; retry = retryHandler.retryRequest(exception, ++retriedTimes, context); } catch (NullPointerException e) { exception = new IOException(e.getMessage()); exception.initCause(e); retry = retryHandler.retryRequest(exception, ++retriedTimes, context); } catch (HttpException e) { throw e; } catch (Throwable e) { exception = new IOException(e.getMessage()); exception.initCause(e); retry = retryHandler.retryRequest(exception, ++retriedTimes, context); } if (!retry && exception != null) { throw new HttpException(exception); } } return null; } /** * 反正先会执行到这里 * * @param params The parameters of the task. * @return */ @Override protected Void doInBackground(Object... params) { if (this.state == State.STOPPED || params == null || params.length == 0) { return null; } if (params.length > 3) { // 文件保存路径.如果是空判定为不是下载. fileSavePath = String.valueOf(params[1]); isDownloadingFile = fileSavePath != null; autoResume = (Boolean) params[2]; autoRename = (Boolean) params[3]; } try { if (this.state == State.STOPPED) { return null; } // init request & requestUrl request = (HttpRequestBase) params[0]; requestUrl = request.getURI().toString(); if (callback != null) { callback.setRequestUrl(requestUrl); } // 开始下载 this.publishProgress(UPDATE_START); lastUpdateTime = SystemClock.uptimeMillis(); //关键,调用发送请求的. ResponseInfo<T> responseInfo = sendRequest(request); if (responseInfo != null) { // 得到了结果从这里返回 this.publishProgress(UPDATE_SUCCESS, responseInfo); return null; } } catch (HttpException e) { this.publishProgress(UPDATE_FAILURE, e, e.getMessage()); } return null; } private final static int UPDATE_START = 1; private final static int UPDATE_LOADING = 2; private final static int UPDATE_FAILURE = 3; private final static int UPDATE_SUCCESS = 4; /** * 更新当前的进度. * * @param values The values indicating progress. */ @Override @SuppressWarnings("unchecked") protected void onProgressUpdate(Object... values) { //为了防止资源的浪费,一般都先进行判断再往下执行. if (this.state == State.STOPPED || values == null || values.length == 0 || callback == null) return; // values[0]是当前的状态 switch ((Integer) values[0]) { case UPDATE_START: this.state = State.STARTED; callback.onStart(); break; case UPDATE_LOADING: if (values.length != 3) return; this.state = State.LOADING; callback.onLoading( Long.valueOf(String.valueOf(values[1])), Long.valueOf(String.valueOf(values[2])), isUploading); break; case UPDATE_FAILURE: if (values.length != 3) return; this.state = State.FAILURE; callback.onFailure((HttpException) values[1], (String) values[2]); break; case UPDATE_SUCCESS: if (values.length != 2) return; this.state = State.SUCCESS; // 原来就是从这里返回啊 callback.onSuccess((ResponseInfo<T>) values[1]); break; default: break; } } /** * 处理响应 * TODO:疑惑:如果下载一个很大的文件,缓存的文件存在哪里?按照现在的想法,HttpResponse有一个缓存的地方,而不占用当前APP的内存.如果占用APP内存,下载100M的早就崩了 * * @param response * @return * @throws HttpException * @throws java.io.IOException */ @SuppressWarnings("unchecked") private ResponseInfo<T> handleResponse(HttpResponse response) throws HttpException, IOException { if (response == null) { throw new HttpException("response is null"); } if (isCancelled()) return null; // 获取状态码 StatusLine status = response.getStatusLine(); int statusCode = status.getStatusCode(); if (statusCode < 300) { // 只有小于300的状态码是正常的 Object result = null; HttpEntity entity = response.getEntity(); if (entity != null) { isUploading = false; //从这里可以看出,这个类的设计并不好,扩展性太差了,这就限定了只能下载文件和字符串.应该学学Volley吧一切都抽象出去.今天的感悟. if (isDownloadingFile) { // 下载文件就用FileDownloadHandler处理. // 判断是否支持续传. autoResume = autoResume && OtherUtils.isSupportRange(response); // 从响应头获取文件名 String responseFileName = autoRename ? OtherUtils.getFileNameFromHttpResponse(response) : null; result = mFileDownloadHandler.handleEntity(entity, this, fileSavePath, autoResume, responseFileName); } else { // 下载字符串就用StringDownLoadHandler处理.字符串可以缓存 result = mStringDownloadHandler.handleEntity(entity, this, charset); if (HttpUtils.sHttpCache.isEnabled(requestMethod)) { HttpUtils.sHttpCache.put(requestUrl, (String) result, expiry); } } } return new ResponseInfo<T>(response, (T) result, false); } else if (statusCode == 301 || statusCode == 302) { // 301 & 302 重定向.所以需要再一次请求.考虑很周到,之前一直没意识到. if (httpRedirectHandler == null) { httpRedirectHandler = new DefaultHttpRedirectHandler(); } HttpRequestBase request = httpRedirectHandler.getDirectRequest(response); if (request != null) { // 又发起请求 return this.sendRequest(request); } } else if (statusCode == 416) { // 请求头的Range范围不合法 throw new HttpException(statusCode, "maybe the file has downloaded completely"); } else { throw new HttpException(statusCode, status.getReasonPhrase()); } return null; } /** * stop request task. */ public void stop() { this.state = State.STOPPED; if (request != null && !request.isAborted()) { try { request.abort(); } catch (Throwable e) { } } if (!this.isCancelled()) { try { this.cancel(true); } catch (Throwable e) { } } if (callback != null) { callback.onStopped(); } } public boolean isStopped() { return this.state == State.STOPPED; } public RequestCallBack<T> getRequestCallBack() { return this.callback; } private long lastUpdateTime; /** * 这是实现RequestCallbackHandler接口的.目的是用来更新进度.根据返回值决定是否还要继续执行.. * 不过我觉得这样控制有点不好.应该分开控制的. * * @param total * @param current * @param forceUpdateUI * @return true:继续,false:停止. */ @Override public boolean updateProgress(long total, long current, boolean forceUpdateUI) { LogUtils.i("update progress......"); if (callback != null && this.state != State.STOPPED) { if (forceUpdateUI) { this.publishProgress(UPDATE_LOADING, total, current); } else { // 刷新的频率是非常快的,就是通过这里用实际那来控制频率 long currTime = SystemClock.uptimeMillis(); if (currTime - lastUpdateTime >= callback.getRate()) { lastUpdateTime = currTime; this.publishProgress(UPDATE_LOADING, total, current); } } } return this.state != State.STOPPED; } public enum State { WAITING(0), STARTED(1), LOADING(2), FAILURE(3), STOPPED(4), SUCCESS(5); private int value = 0; State(int value) { this.value = value; } public static State valueOf(int value) { switch (value) { case 0: return WAITING; case 1: return STARTED; case 2: return LOADING; case 3: return FAILURE; case 4: return STOPPED; case 5: return SUCCESS; default: return FAILURE; } } public int value() { return this.value; } } }