Files
EhViewer/app/src/main/java/com/hippo/network/HttpHelper.java
T
2015-03-08 21:47:41 +08:00

901 lines
27 KiB
Java

/*
* Copyright (C) 2014-2015 Hippo Seven
*
* 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
* 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.hippo.network;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.webkit.CookieManager;
import android.webkit.MimeTypeMap;
import com.hippo.util.FastByteArrayOutputStream;
import com.hippo.util.Log;
import com.hippo.util.Utils;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
/**
* It is not thread safe
*/
public class HttpHelper {
private static final String TAG = HttpHelper.class.getSimpleName();
private static final int MAX_RETRY = 3;
private static final int MAX_REDIRECTS = 3;
private static final int CONNECT_TIMEOUT = 5000;
private static final int READ_TIMEOUT = 5000;
public static final String DEFAULT_USER_AGENT =
"Mozilla/5.0 (Windows NT 6.1; WOW64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/39.0.2171.95 Safari/537.36";
public static final String USER_AGENT =
System.getProperty("http.agent", DEFAULT_USER_AGENT);
private static final int HTTP_TEMP_REDIRECT = 307;
private static final String DEFAULT_CHARSET = "utf-8";
private static final String CHARSET_KEY = "charset=";
private static final CookieManager sCookieManager = CookieManager.getInstance();
private int mResponseCode = -1;
public void reset() {
mResponseCode = -1;
}
public int getResponseCode() {
return mResponseCode;
}
/**
* Get cookie for the url
* @param url the URL
* @return the cookie for the URL
*/
protected String getCookie(URL url) {
return sCookieManager.getCookie(url.toString());
}
/**
* Store cookie for the url
* @param url the URL
* @param value the cookie for the URL
*/
protected void storeCookie(URL url, String value) {
sCookieManager.setCookie(url.toString(), value);
}
/**
* Prepare before connecting
* @param conn the connection
*/
protected void onBeforeConnect(HttpURLConnection conn) {
conn.setInstanceFollowRedirects(true);
conn.setRequestProperty("User-Agent", USER_AGENT);
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
}
private Object doRequst(RequestHelper rh) throws Exception {
URL url;
HttpURLConnection conn = null;
int redirectionCount = 0;
try {
url = rh.getUrl();
while (redirectionCount++ < MAX_REDIRECTS) {
Log.d(TAG, "Request: " + url.toString());
conn = (HttpURLConnection) url.openConnection();
// Prepare before connecting
onBeforeConnect(conn);
// Set cookie
String cookie = getCookie(url);
if (cookie != null) {
conn.setRequestProperty("Cookie", cookie);
}
// Do custom staff
rh.onBeforeConnect(conn);
conn.connect();
// Store cookie
List<String> cookieList = conn.getHeaderFields().get("Set-Cookie");
if (cookieList != null) {
for (String cookieTemp : cookieList) {
if (cookieTemp != null) {
storeCookie(url, cookieTemp);
}
}
}
final int responseCode = conn.getResponseCode();
mResponseCode = responseCode;
Log.d(TAG, "Response code: " + responseCode);
switch (responseCode) {
case HttpURLConnection.HTTP_MOVED_PERM:
case HttpURLConnection.HTTP_MOVED_TEMP:
case HttpURLConnection.HTTP_SEE_OTHER:
case HTTP_TEMP_REDIRECT:
final String location = conn.getHeaderField("Location");
Log.d(TAG, "New location: " + location);
conn.disconnect();
url = new URL(url, location);
break;
default:
return rh.onAfterConnect(conn);
}
}
} finally {
if (conn != null)
conn.disconnect();
}
throw new RedirectionException();
}
private Object requst(RequestHelper rh) throws Exception {
Exception exception = null;
for (int times = 0;
times < MAX_RETRY &&(times == 0 || rh.onRetry(exception));
times++) {
try {
return doRequst(rh);
} catch (Exception e) {
exception = e;
}
}
rh.onRequestFailed(exception);
throw exception;
}
public static interface RequestHelper {
/**
* Get the URL to connect
* @return the URL to connect
*/
public URL getUrl() throws MalformedURLException;
/**
* Add header or do something else for HttpURLConnection before connect
*
* @param conn the connection
* @throws Exception
*/
public void onBeforeConnect(HttpURLConnection conn) throws Exception;
/**
* Get what do you need from HttpURLConnection after connect
* Return null means get error
*
* @param conn the connection
* @return what you want to return
* @throws Exception
*/
public Object onAfterConnect(HttpURLConnection conn) throws Exception;
/**
* Retry http connecting, or stop
*
* @param previousException previous thrown
* @return true for retry, false for stop
*/
public boolean onRetry(Exception previousException);
/**
* Called when request failed by exception
*/
public void onRequestFailed(Exception exception);
}
public static abstract class GetStringHelper implements RequestHelper {
private final String mUrl;
public GetStringHelper(String url) {
mUrl = url;
}
@Override
public URL getUrl() throws MalformedURLException {
return new URL(mUrl);
}
@Override
public void onBeforeConnect(HttpURLConnection conn)
throws Exception {
conn.addRequestProperty("Accept-Encoding", "gzip");
}
private String getCharsetFromContentType(String contentType) {
if (contentType == null) {
return null;
}
String[] values = contentType.split(";");
for (String value : values) {
value = value.trim();
if (value.toLowerCase().startsWith(CHARSET_KEY)) {
return value.substring(CHARSET_KEY.length());
}
}
return null;
}
private String getBody(HttpURLConnection conn)
throws Exception {
String body = null;
InputStream is = null;
ByteArrayOutputStream baos = null;
try {
try {
// First try to get input stream
is = conn.getInputStream();
} catch (Exception t){
// If we get error, get error stream
is = conn.getErrorStream();
}
String encoding = conn.getContentEncoding();
if (encoding != null && encoding.equalsIgnoreCase("gzip"))
is = new GZIPInputStream(is);
int length = conn.getContentLength();
if (length >= 0)
baos = new ByteArrayOutputStream(length);
else
baos = new ByteArrayOutputStream();
Utils.copy(is, baos);
// Get charset
String charset = getCharsetFromContentType(conn.getContentType());
if (charset == null) {
charset = DEFAULT_CHARSET;
}
body = baos.toString(charset);
} finally {
Utils.closeQuietly(is);
Utils.closeQuietly(baos);
}
return body;
}
@Override
public Object onAfterConnect(HttpURLConnection conn)
throws Exception {
return getBody(conn);
}
@Override
public boolean onRetry(Exception previousException) {
// Do not care about exception, just retry
return true;
}
@Override
public void onRequestFailed(Exception exception) {
// Empty
}
}
/**
* RequstHelper for GET method
*/
public static class GetHelper extends GetStringHelper {
public GetHelper(String url) {
super(url);
}
@Override
public void onBeforeConnect(HttpURLConnection conn)
throws Exception {
super.onBeforeConnect(conn);
conn.setRequestMethod("GET");
}
}
/**
* RequstHelper for post form data, use POST method
*/
public static class PostFormHelper extends GetStringHelper {
private final String[][] mArgs;
public PostFormHelper(String url, String[][] args) {
super(url);
mArgs = args;
}
@Override
public void onBeforeConnect(HttpURLConnection conn)
throws Exception {
super.onBeforeConnect(conn);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type",
"application/x-www-form-urlencoded");
DataOutputStream out = new DataOutputStream(conn.getOutputStream());
StringBuilder sb = new StringBuilder();
int i = 0;
for (String[] arg : mArgs) {
if (i != 0)
sb.append("&");
sb.append(URLEncoder.encode(arg[0], "UTF-8"));
sb.append("=");
sb.append(URLEncoder.encode(arg[1], "UTF-8"));
i++;
}
out.writeBytes(sb.toString());
out.flush();
out.close();
}
}
/**
* RequstHelper for post json, use POST method
*/
public static class PostJsonHelper extends GetStringHelper {
private final JSONObject mJo;
public PostJsonHelper(String url, JSONObject jo) {
super(url);
mJo = jo;
}
@Override
public void onBeforeConnect(HttpURLConnection conn) throws Exception {
super.onBeforeConnect(conn);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
DataOutputStream out = new DataOutputStream(conn.getOutputStream());
String body = mJo.toString();
Log.d(TAG, "Post json: " + body);
out.write(body.getBytes("utf-8"));
out.flush();
out.close();
}
}
public abstract static class FormData {
private final Map<String, String> mProperties;
public FormData() {
mProperties = new LinkedHashMap<String, String>();
}
public void setProperty(String key, String value) {
mProperties.put(key, value);
}
public void clearProperty(String key) {
mProperties.remove(key);
}
public void clearAllProperties() {
mProperties.clear();
}
/**
* Put information to target OutputStream.
*
* @param os target outputStream
* @throws java.io.IOException
*/
public abstract void output(OutputStream os) throws IOException;
public void doOutPut(OutputStream os) throws IOException {
StringBuilder sb = new StringBuilder();
for (String key : mProperties.keySet())
sb.append(key).append(": ").append(mProperties.get(key)).append("\r\n");
sb.append("\r\n");
os.write(sb.toString().getBytes());
output(os);
}
}
public static class StringData extends FormData {
private final String mStr;
public StringData(String str) {
mStr = str;
}
@Override
public void output(OutputStream os) throws IOException {
os.write(mStr.getBytes());
os.write("\r\n".getBytes());
}
}
public static class BitmapData extends FormData {
private final Bitmap mBitmap;
public BitmapData(Bitmap bmp) {
mBitmap = bmp;
setProperty("Content-Type", "image/jpeg");
}
@Override
public void output(OutputStream os) throws IOException {
mBitmap.compress(Bitmap.CompressFormat.JPEG, 98, os);
os.write("\r\n".getBytes());
}
}
public static class FileData extends FormData {
private final File mFile;
public FileData(File file) {
mFile = file;
setProperty("Content-Type",
URLConnection.guessContentTypeFromName(file.getName()));
}
@Override
public void output(OutputStream os) throws IOException {
InputStream is = new FileInputStream(mFile);
byte[] buffer = new byte[1024];
int bytesRead;
while((bytesRead = is.read(buffer)) !=-1)
os.write(buffer, 0, bytesRead);
is.close();
os.write("\r\n".getBytes());
}
}
public static class PostFormDataHelper extends GetStringHelper {
private static final String BOUNDARY = "----WebKitFormBoundary7eDB0hDQ91s22Tkf";
private final List<FormData> mDataList;
public PostFormDataHelper(String url, List<FormData> dataList) {
super(url);
mDataList = dataList;
}
@Override
public void onBeforeConnect(HttpURLConnection conn) throws Exception {
super.onBeforeConnect(conn);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type",
"multipart/form-data; boundary=" + BOUNDARY);
DataOutputStream out = new DataOutputStream(conn.getOutputStream());
for (FormData data : mDataList) {
out.write("--".getBytes());
out.write(BOUNDARY.getBytes());
out.write("\r\n".getBytes());
data.doOutPut(out);
}
out.write("--".getBytes());
out.write(BOUNDARY.getBytes());
out.write("--".getBytes());
out.flush();
out.close();
}
}
public static class GetImageHelper implements RequestHelper {
private final String mUrl;
public GetImageHelper(String url) {
mUrl = url;
}
@Override
public URL getUrl() throws MalformedURLException {
return new URL(mUrl);
}
@Override
public void onBeforeConnect(HttpURLConnection conn)
throws Exception {
conn.setRequestMethod("GET");
}
@Override
public Object onAfterConnect(HttpURLConnection conn)
throws Exception {
// If just
// Bitmap bmp = BitmapFactory.decodeStream(conn.getInputStream(), null, Ui.getBitmapOpt());
// bitmap might be incomplete.
int size = conn.getContentLength();
FastByteArrayOutputStream fbaos = new FastByteArrayOutputStream(size == -1 ? 24 * 1024 : (size + 100));
Utils.copy(conn.getInputStream(), fbaos);
Bitmap bmp = BitmapFactory.decodeByteArray(fbaos.getBuffer(), 0, fbaos.size(), null);
if (bmp == null) {
throw new IllegalStateException("Can not decode to bitmap.");
}
return bmp;
}
@Override
public boolean onRetry(Exception previousException) {
// Do not care about exception, just retry
return true;
}
@Override
public void onRequestFailed(Exception exception) {
// Empty
}
}
public static interface OnDownloadListener {
/**
* Called before connecting
*/
public void onStartConnecting();
/**
* Called after connecting and before downloading
*
* @param totalSize content length, -1 for unknown
*/
public void onStartDownloading(int totalSize);
/**
* File name is fixed
*
* @param newName new file name
*/
public void onNameFix(String newName);
/**
* Called repeatedly during downloading
*
* @param downloadSize downloaded size
* @param totalSize content length, -1 for unknown
*/
public void onDownload(int downloadSize, int totalSize);
/**
* Called when download ok
*/
public void onSuccess();
/**
* Called when download stop
*/
public void onStop();
/**
* Called when download failed
*
* @param e the exception why fail
*/
public void onFailure(Exception e);
}
public static class DownloadControlor {
private boolean mStop = false;
public void stop() {
mStop = true;
}
public void reset() {
mStop = false;
}
public boolean isStop() {
return mStop;
}
}
public static class DownloadHelper implements RequestHelper {
private static final String DOWNLOAD_EXTENSION = ".download";
private final String mUrl;
private final File mDir;
private String mFileName;
private File mFile;
private File mTempFile;
private final DownloadControlor mControlor;
private final OnDownloadListener mListener;
private int mContentLength;
private int mReceivedSize;
public DownloadHelper(String url, File dir, String fileName,
DownloadControlor controlor, OnDownloadListener listener) {
mUrl = url;
mDir = dir;
mFileName = fileName;
mControlor = controlor;
mListener = listener;
}
@Override
public URL getUrl() throws MalformedURLException {
return new URL(mUrl);
}
private void checkStop() throws StopRequestException {
if (mControlor != null && mControlor.isStop()) {
throw new StopRequestException();
}
}
@Override
public void onBeforeConnect(HttpURLConnection conn) throws Exception {
checkStop();
conn.setRequestMethod("GET");
// Sometimes for application/octet-stream, we need to add this line
// to get content length
conn.setRequestProperty("Range", "bytes=0-");
if (mListener != null) {
mListener.onStartConnecting();
}
}
private int getContentLength(HttpURLConnection conn) {
int contentLength = conn.getContentLength();
String range;
if (contentLength == -1 &&
(range = conn.getHeaderField("Content-Range")) != null) {
// Content-Range looks like bytes 500-999/1234
contentLength = 0;
int step = 0;
boolean isNum = false;
char ch;
for (int i = 0; i < range.length(); i++) {
ch = range.charAt(i);
if (ch >= '0' && ch <= '9') {
if (!isNum) {
isNum = true;
step++;
}
} else {
isNum = false;
}
if (isNum && step == 3) {
contentLength = contentLength * 10 + ch - '0';
}
}
}
return contentLength;
}
/**
* Get new extension
*
* @param ext new extension
* @return null for not changed
*/
protected String fixNewExtension(String ext) {
return null;
}
@Override
public Object onAfterConnect(HttpURLConnection conn) throws Exception {
// Check stop
if (mControlor != null && mControlor.isStop())
throw new StopRequestException();
String contentType = conn.getContentType();
int index = contentType.indexOf(';');
if (index != -1)
contentType = contentType.substring(0, index);
mContentLength = getContentLength(conn);
if (mListener != null) {
mListener.onStartDownloading(mContentLength);
}
// Fix extension
MimeTypeMap mime = MimeTypeMap.getSingleton();
String newExtension = mime.getExtensionFromMimeType(contentType);
if (newExtension == null) {
index = contentType.lastIndexOf('/');
if (index != -1) {
newExtension = contentType.substring(index + 1);
}
}
newExtension = fixNewExtension(newExtension);
if (newExtension != null) {
index = mFileName.lastIndexOf('.');
if (index == -1) {
mFileName = mFileName + '.' + newExtension;
} else {
mFileName = mFileName.substring(0, index) + '.' + newExtension;
}
}
// Make sure parent exist
if (!mDir.mkdirs() && !mDir.isDirectory()) {
throw new FileNotFoundException("Can not create dir " +
mDir.getPath());
}
mFile = new File(mDir, mFileName);
mTempFile = new File(mDir, mFileName + DOWNLOAD_EXTENSION);
// Transfer
transferData(conn.getInputStream(), new FileOutputStream(mTempFile));
// Get ok, rename
Utils.deleteFile(mFile);
if (!mTempFile.renameTo(mFile)) {
throw new FileNotFoundException("Rename " + mTempFile.getPath() +
" to " + mFile.getPath() + " error");
}
// Callback
if (mListener != null) {
mListener.onSuccess();
}
// We do not need result
return null;
}
private void transferData(InputStream in, OutputStream out)
throws Exception {
final byte data[] = new byte[512 * 1024];
mReceivedSize = 0;
while (true) {
// Check stop first
checkStop();
int bytesRead = in.read(data);
if (bytesRead == -1)
break;
out.write(data, 0, bytesRead);
mReceivedSize += bytesRead;
if (mListener != null) {
mListener.onDownload(mReceivedSize, mContentLength);
}
}
if (mContentLength != -1 && mReceivedSize != mContentLength) {
throw new UncompletedException("Received size is " + mReceivedSize
+ ", but ContentLength is " + mContentLength);
}
}
@Override
public boolean onRetry(Exception previousException) {
// Delete unfinished file
Utils.deleteFile(mTempFile);
Utils.deleteFile(mFile);
if (previousException instanceof StopRequestException) {
return false;
} else {
return true;
}
}
@Override
public void onRequestFailed(Exception exception) {
if (mListener != null) {
if (exception instanceof StopRequestException) {
mListener.onStop();
} else {
mListener.onFailure(exception);
}
}
}
}
/**
* Http GET method
* @param url the url to get
* @return body
*/
public String get(String url) throws Exception {
return (String) requst(new GetHelper(url));
}
/**
* Post form data
* @param url the url to post
* @param args the form to post
* @return body
*/
public String postForm(String url, String[][] args) throws Exception {
return (String) requst(new PostFormHelper(url, args));
}
/**
* Post json data
* @param url the url to post
* @param json the json to post
* @return body
*/
public String postJson(String url, JSONObject json) throws Exception {
return (String) requst(new PostJsonHelper(url, json));
}
/**
* Post data, multipart/form-data
* @param url the url to post
* @param dataList the data list to post
* @return body
*/
public String postFormData(String url, List<FormData> dataList) throws Exception {
return (String)requst(new PostFormDataHelper(url, dataList));
}
/**
* Get image
* @param url the url to get image
* @return bitmap
*/
public Bitmap getImage(String url) throws Exception {
return (Bitmap) requst(new GetImageHelper(url));
}
/**
* Init DownloadHelper
*
* @param url the url to download
* @param dir the dir to download file
* @param file the file name
* @param controlor download control
* @param listener download listener
*/
public void download(String url, File dir, String file,
DownloadControlor controlor, OnDownloadListener listener) throws Exception {
requst(new DownloadHelper(url, dir, file, controlor, listener));
}
}