/* * 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 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 mProperties; public FormData() { mProperties = new LinkedHashMap(); } 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 mDataList; public PostFormDataHelper(String url, List 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 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)); } }