/*
* Copyright 2011 - AndroidQuery.com (tinyeeliu@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 com.androidquery.callback;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.conn.params.ConnManagerParams;
import org.apache.http.conn.params.ConnPerRouteBean;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SocketFactory;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.xmlpull.v1.XmlPullParser;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Xml;
import android.view.View;
import com.androidquery.AQuery;
import com.androidquery.AbstractAQuery.LoadListener;
import com.androidquery.auth.AccountHandle;
import com.androidquery.auth.GoogleHandle;
import com.androidquery.util.AQUtility;
import com.androidquery.util.Common;
import com.androidquery.util.Constants;
import com.androidquery.util.PredefinedBAOS;
import com.androidquery.util.Progress;
import com.androidquery.util.XmlDom;
/**
* The core class of ajax callback handler.
*
*/
public abstract class AbstractAjaxCallback<T, K> implements Runnable {
private static int NET_TIMEOUT = 30000;
private static String AGENT = null;
private static int NETWORK_POOL = 4;
private static boolean GZIP = true;
private static boolean REUSE_CLIENT = true;
private Class<T> type;
private Reference<Object> whandler;
private Object handler;
private String callback;
private WeakReference<Object> progress;
private String url;
private String networkUrl;
protected Map<String, Object> params;
protected Map<String, String> headers;
protected Map<String, String> cookies;
private Transformer transformer;
protected T result;
private int policy = Constants.CACHE_DEFAULT;
private File cacheDir;
private File targetFile;
protected AccountHandle ah;
protected AjaxStatus status;
protected boolean fileCache;
protected boolean memCache;
private boolean refresh;
private int timeout = 0;
private long expire;
private String encoding = "UTF-8";
private WeakReference<Activity> act;
private int method = Constants.METHOD_DETECT;
private HttpUriRequest request;
private boolean uiCallback = true;
private int retry = 0;
private LoadListener loadListener;
@SuppressWarnings("unchecked")
private K self() {
return (K) this;
}
private void clear() {
whandler = null;
handler = null;
progress = null;
request = null;
transformer = null;
ah = null;
act = null;
loadListener = null;
callback = null;
targetFile = null;
cacheDir = null;
cookies = null;
headers = null;
params = null;
proxy = null;
networkUrl = null;
url = null;
result = null;
type = null;
status = null;
abort = false;
blocked = false;
completed = false;
fileCache = false;
memCache = false;
reauth = false;
refresh = false;
method = Constants.METHOD_DETECT;
policy = Constants.CACHE_DEFAULT;
lastStatus = 200;
expire = 0;
retry = 0;
timeout = 0;
uiCallback = true;
}
/**
* Sets the timeout.
*
* @param timeout
* the default network timeout in milliseconds
*/
public static void setTimeout(int timeout) {
NET_TIMEOUT = timeout;
}
/**
* Sets the agent.
*
* @param agent
* the default agent sent in http header
*/
public static void setAgent(String agent) {
AGENT = agent;
}
/**
* Use gzip.
*
* @param gzip
*/
public static void setGZip(boolean gzip) {
GZIP = gzip;
}
/**
* Sets the default static transformer. This transformer should be
* stateless. If state is required, use the AjaxCallback.transformer() or
* AQuery.transformer().
*
* Transformers are selected in the following priority: 1. Native 2.
* instance transformer() 3. static setTransformer()
*
* @param agent
* the default transformer to transform raw data to specified
* type
*/
private static Transformer st;
public static void setTransformer(Transformer transformer) {
st = transformer;
}
/**
* Gets the ajax response type.
*
* @return the type
*/
public Class<T> getType() {
return type;
}
/**
* Set a callback handler with a weak reference. Use weak handler if you do
* not want the ajax callback to hold the handler object from garbage
* collection. For example, if the handler is an activity, weakHandler
* should be used since the method shouldn't be invoked if an activity is
* already dead and garbage collected.
*
* @param handler
* the handler
* @param callback
* the callback
* @return self
*/
public K weakHandler(Object handler, String callback) {
this.whandler = new WeakReference<Object>(handler);
this.callback = callback;
this.handler = null;
return self();
}
/**
* Set a callback handler. See weakHandler for handler objects, such as
* Activity, that should not be held from garbaged collected.
*
* @param handler
* the handler
* @param callback
* the callback
* @return self
*/
public K handler(Object handler, String callback) {
this.handler = handler;
this.callback = callback;
this.whandler = null;
return self();
}
/**
* Url.
*
* @param url
* the url
* @return self
*/
public K url(String url) {
this.url = url;
return self();
}
public K networkUrl(String url) {
this.networkUrl = url;
return self();
}
/**
* Set the desired ajax response type. Type parameter is required otherwise
* the ajax callback will not occur.
*
* Current supported type: JSONObject.class, String.class, byte[].class,
* Bitmap.class, XmlDom.class
*
*
* @param type
* the type
* @return self
*/
public K type(Class<T> type) {
this.type = type;
return self();
}
public K method(int method) {
this.method = method;
return self();
}
public K timeout(int timeout) {
this.timeout = timeout;
return self();
}
public K retry(int retry) {
this.retry = retry;
return self();
}
/**
* Set the transformer that transform raw data to desired type. If not set,
* default transformer will be used.
*
* Default transformer supports:
*
* JSONObject, JSONArray, XmlDom, String, byte[], and Bitmap.
*
*
* @param transformer
* transformer
* @return self
*/
public K transformer(Transformer transformer) {
this.transformer = transformer;
return self();
}
/**
* Set ajax request to be file cached.
*
* @param cache
* the cache
* @return self
*/
public K fileCache(boolean cache) {
this.fileCache = cache;
return self();
}
/**
* Indicate ajax request to be memcached. Note: The default ajax handler
* does not supply a memcache. Subclasses such as BitmapAjaxCallback can
* provide their own memcache.
*
* @param cache
* the cache
* @return self
*/
public K memCache(boolean cache) {
this.memCache = cache;
return self();
}
public K policy(int policy) {
this.policy = policy;
return self();
}
/**
* Indicate the ajax request should ignore memcache and filecache.
*
* @param refresh
* the refresh
* @return self
*/
public K refresh(boolean refresh) {
this.refresh = refresh;
return self();
}
/**
* Indicate the ajax request should use the main ui thread for callback.
* Default is true.
*
* @param uiCallback
* use the main ui thread for callback
* @return self
*/
public K uiCallback(boolean uiCallback) {
this.uiCallback = uiCallback;
return self();
}
/**
* The expire duation for filecache. If a cached copy will be served if a
* cached file exists within current time minus expire duration.
*
* @param expire
* the expire
* @return self
*/
public K expire(long expire) {
this.expire = expire;
return self();
}
/**
* Set the header fields for the http request.
*
* @param name
* the name
* @param value
* the value
* @return self
*/
public K header(String name, String value) {
if (headers == null) {
headers = new HashMap<String, String>();
}
headers.put(name, value);
return self();
}
/**
* Set the header fields for the http request.
*
* @param headers
* the header
* @return self
*/
public K headers(Map<String, String> headers) {
this.headers = (Map<String, String>) headers;
return self();
}
/**
* Set the cookies for the http request.
*
* @param name
* the name
* @param value
* the value
* @return self
*/
public K cookie(String name, String value) {
if (cookies == null) {
cookies = new HashMap<String, String>();
}
cookies.put(name, value);
return self();
}
/**
* Set cookies for the http request.
*
* @param cookies
* the cookies
* @return self
*/
public K cookies(Map<String, String> cookies) {
this.cookies = (Map<String, String>) cookies;
return self();
}
/**
* Set the encoding used to parse the response.
*
* Default is UTF-8.
*
* @param encoding
* @return self
*/
public K encoding(String encoding) {
this.encoding = encoding;
return self();
}
private HttpHost proxy;
public K proxy(String host, int port) {
proxy = new HttpHost(host, port);
return self();
}
public K targetFile(File file) {
this.targetFile = file;
return self();
}
/**
* Set http POST params. If params are set, http POST method will be used.
* The UTF-8 encoded value.toString() will be sent with POST.
*
* Header field
* "Content-Type: application/x-www-form-urlencoded;charset=UTF-8" will be
* added if no Content-Type header field presents.
*
* @param name
* the name
* @param value
* the value
* @return self
*/
public K param(String name, Object value) {
if (params == null) {
params = new HashMap<String, Object>();
}
params.put(name, value);
return self();
}
/**
* Set the http POST params. See param(String name, Object value).
*
* @param params
* the params
* @return self
*/
@SuppressWarnings("unchecked")
public K params(Map<String, ?> params) {
this.params = (Map<String, Object>) params;
return self();
}
/**
* Set the loadlistener can listen downloading percent
*
* @param li
* @return
*/
public K load(LoadListener loadlistener) {
loadListener = loadlistener;
return self();
}
/**
* Set the progress view (can be a progress bar or any view) to be shown
* (VISIBLE) and hide (GONE) depends on progress.
*
* @param view
* the progress view
* @return self
*/
public K progress(View view) {
return progress((Object) view);
}
/**
* Set the dialog to be shown and dismissed depends on progress.
*
* @param dialog
* @return self
*/
public K progress(Dialog dialog) {
return progress((Object) dialog);
}
public K progress(Object progress) {
if (progress != null) {
this.progress = new WeakReference<Object>(progress);
}
return self();
}
private static final Class<?>[] DEFAULT_SIG = { String.class, Object.class,
AjaxStatus.class };
private boolean completed;
void callback() {
showProgress(false);
completed = true;
if (isActive()) {
if (callback != null) {
Object handler = getHandler();
Class<?>[] AJAX_SIG = { String.class, type, AjaxStatus.class };
AQUtility.invokeHandler(handler, callback, true, true,
AJAX_SIG, DEFAULT_SIG, url, result, status);
} else {
try {
callback(url, result, status);
} catch (Exception e) {
AQUtility.report(e);
}
}
} else {
skip(url, result, status);
}
filePut();
if (!blocked) {
status.close();
}
wake();
AQUtility.debugNotify();
}
private void wake() {
if (!blocked)
return;
synchronized (this) {
try {
notifyAll();
} catch (Exception e) {
}
}
}
private boolean blocked;
/**
* Block the current thread until the ajax call is completed. Returns
* immediately if ajax is already completed. Exception will be thrown if
* this method is called in main thread.
*
*/
public void block() {
if (AQUtility.isUIThread()) {
throw new IllegalStateException("Cannot block UI thread.");
}
if (completed)
return;
try {
synchronized (this) {
blocked = true;
// wait at most the network timeout plus 5 seconds, this
// guarantee thread will never be blocked forever
this.wait(NET_TIMEOUT + 5000);
}
} catch (Exception e) {
}
}
/**
* The callback method to be overwritten for subclasses.
*
* @param url
* the url
* @param object
* the object
* @param status
* the status
*/
public void callback(String url, T object, AjaxStatus status) {
}
protected void skip(String url, T object, AjaxStatus status) {
}
protected T fileGet(String url, File file, AjaxStatus status) {
try {
byte[] data = null;
if (isStreamingContent()) {
status.file(file);
} else {
data = AQUtility.toBytes(new FileInputStream(file));
}
return transform(url, data, status);
} catch (Exception e) {
AQUtility.debug(e);
return null;
}
}
protected T datastoreGet(String url) {
return null;
}
protected void showProgress(final boolean show) {
final Object p = progress == null ? null : progress.get();
if (p != null) {
if (AQUtility.isUIThread()) {
Common.showProgress(p, url, show);
} else {
AQUtility.post(new Runnable() {
@Override
public void run() {
Common.showProgress(p, url, show);
}
});
}
}
}
@SuppressWarnings("unchecked")
protected T transform(String url, byte[] data, AjaxStatus status) {
if (type == null) {
return null;
}
File file = status.getFile();
if (data != null) {
if (type.equals(Bitmap.class)) {
return (T) BitmapFactory.decodeByteArray(data, 0, data.length);
}
if (type.equals(JSONObject.class)) {
JSONObject result = null;
String str = null;
try {
str = new String(data, encoding);
result = (JSONObject) new JSONTokener(str).nextValue();
} catch (Exception e) {
AQUtility.debug(e);
AQUtility.debug(str);
}
return (T) result;
}
if (type.equals(JSONArray.class)) {
JSONArray result = null;
try {
String str = new String(data, encoding);
result = (JSONArray) new JSONTokener(str).nextValue();
} catch (Exception e) {
AQUtility.debug(e);
}
return (T) result;
}
if (type.equals(String.class)) {
String result = null;
if (status.getSource() == AjaxStatus.NETWORK) {
AQUtility.debug("network");
result = correctEncoding(data, encoding, status);
} else {
AQUtility.debug("file");
try {
result = new String(data, encoding);
} catch (Exception e) {
AQUtility.debug(e);
}
}
return (T) result;
}
/*
* if(type.equals(XmlDom.class)){
*
* XmlDom result = null;
*
* try { result = new XmlDom(data); } catch (Exception e) {
* AQUtility.debug(e); }
*
* return (T) result; }
*/
if (type.equals(byte[].class)) {
return (T) data;
}
if (transformer != null) {
return transformer.transform(url, type, encoding, data, status);
}
if (st != null) {
return st.transform(url, type, encoding, data, status);
}
} else if (file != null) {
if (type.equals(File.class)) {
return (T) file;
}
if (type.equals(XmlDom.class)) {
XmlDom result = null;
try {
FileInputStream fis = new FileInputStream(file);
result = new XmlDom(fis);
status.closeLater(fis);
} catch (Exception e) {
AQUtility.report(e);
return null;
}
return (T) result;
}
if (type.equals(XmlPullParser.class)) {
XmlPullParser parser = Xml.newPullParser();
try {
FileInputStream fis = new FileInputStream(file);
parser.setInput(fis, encoding);
status.closeLater(fis);
} catch (Exception e) {
AQUtility.report(e);
return null;
}
return (T) parser;
}
if (type.equals(InputStream.class)) {
try {
FileInputStream fis = new FileInputStream(file);
status.closeLater(fis);
return (T) fis;
} catch (Exception e) {
AQUtility.report(e);
return null;
}
}
}
return null;
}
// This is an adhoc way to get charset without html parsing library, might
// not cover all cases.
private String getCharset(String html) {
String pattern = "<meta [^>]*http-equiv[^>]*\"Content-Type\"[^>]*>";
Pattern p = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(html);
if (!m.find())
return null;
String tag = m.group();
return parseCharset(tag);
}
private String parseCharset(String tag) {
if (tag == null)
return null;
int i = tag.indexOf("charset");
if (i == -1)
return null;
int e = tag.indexOf(";", i);
if (e == -1)
e = tag.length();
String charset = tag.substring(i + 7, e).replaceAll("[^\\w-]", "");
return charset;
}
private String correctEncoding(byte[] data, String target, AjaxStatus status) {
String result = null;
try {
if (!"utf-8".equalsIgnoreCase(target)) {
return new String(data, target);
}
String header = parseCharset(status.getHeader("Content-Type"));
AQUtility.debug("parsing header", header);
if (header != null) {
return new String(data, header);
}
result = new String(data, "utf-8");
String charset = getCharset(result);
AQUtility.debug("parsing needed", charset);
if (charset != null && !"utf-8".equalsIgnoreCase(charset)) {
AQUtility.debug("correction needed", charset);
result = new String(data, charset);
status.data(result.getBytes("utf-8"));
}
} catch (Exception e) {
AQUtility.report(e);
}
return result;
}
protected T memGet(String url) {
return null;
}
protected void memPut(String url, T object) {
}
protected void filePut(String url, T object, File file, byte[] data) {
if (file == null || data == null)
return;
AQUtility.storeAsync(file, data, 0);
}
protected File accessFile(File cacheDir, String url) {
if (expire < 0)
return null;
File file = AQUtility.getExistedCacheByUrl(cacheDir, url);
if (file != null && expire != 0) {
long diff = System.currentTimeMillis() - file.lastModified();
if (diff > expire) {
return null;
}
}
return file;
}
/**
* Starts the async process.
*
* If activity is passed, the callback method will not be invoked if the
* activity is no longer in use. Specifically, isFinishing() is called to
* determine if the activity is active.
*
* @param act
* activity
*/
public void async(Activity act) {
if (act.isFinishing()) {
AQUtility
.warn("Warning",
"Possible memory leak. Calling ajax with a terminated activity.");
}
if (type == null) {
AQUtility.warn("Warning",
"type() is not called with response type.");
return;
}
this.act = new WeakReference<Activity>(act);
async((Context) act);
}
/**
* Starts the async process.
*
* @param context
* the context
*/
public void async(Context context) {
if (status == null) {
status = new AjaxStatus();
status.redirect(url).refresh(refresh);
} else if (status.getDone()) {
status.reset();
result = null;
}
showProgress(true);
if (ah != null) {
if (!ah.authenticated()) {
AQUtility.debug("auth needed", url);
ah.auth(this);
return;
}
}
work(context);
}
private boolean isActive() {
if (act == null)
return true;
Activity a = act.get();
if (a == null || a.isFinishing()) {
return false;
}
return true;
}
public void failure(int code, String message) {
if (status != null) {
status.code(code).message(message);
callback();
}
}
private void work(Context context) {
T object = memGet(url);
if (object != null) {
result = object;
status.source(AjaxStatus.MEMORY).done();
callback();
} else {
cacheDir = AQUtility.getCacheDir(context, policy);
execute(this);
}
}
protected boolean cacheAvailable(Context context) {
// return fileCache && AQUtility.getExistedCacheByUrl(context, url) !=
// null;
return fileCache
&& AQUtility.getExistedCacheByUrl(
AQUtility.getCacheDir(context, policy), url) != null;
}
/**
* AQuert internal use. Do not call this method directly.
*/
@Override
public void run() {
if (!status.getDone()) {
try {
backgroundWork();
} catch (Throwable e) {
AQUtility.debug(e);
status.code(AjaxStatus.NETWORK_ERROR).done();
}
if (!status.getReauth()) {
// if doesn't need to reauth
if (uiCallback) {
AQUtility.post(this);
} else {
afterWork();
}
}
} else {
afterWork();
}
}
private void backgroundWork() {
if (!refresh) {
if (fileCache) {
fileWork();
}
}
if (result == null) {
datastoreWork();
}
if (result == null) {
networkWork();
}
}
private String getCacheUrl() {
if (ah != null) {
return ah.getCacheUrl(url);
}
return url;
}
private String getNetworkUrl(String url) {
String result = url;
if (networkUrl != null) {
result = networkUrl;
}
if (ah != null) {
result = ah.getNetworkUrl(result);
}
return result;
}
private void fileWork() {
File file = accessFile(cacheDir, getCacheUrl());
// if file exist
if (file != null) {
// convert
status.source(AjaxStatus.FILE);
result = fileGet(url, file, status);
// if result is ok
if (result != null) {
status.time(new Date(file.lastModified())).done();
}
}
}
private void datastoreWork() {
result = datastoreGet(url);
if (result != null) {
status.source(AjaxStatus.DATASTORE).done();
}
}
private boolean reauth;
private void networkWork() {
if (url == null) {
status.code(AjaxStatus.NETWORK_ERROR).done();
return;
}
byte[] data = null;
try {
network(retry + 1);
if (ah != null && ah.expired(this, status) && !reauth) {
AQUtility.debug("reauth needed", status.getMessage());
reauth = true;
if (ah.reauth(this)) {
network();
} else {
status.reauth(true);
return;
}
}
data = status.getData();
} catch (Exception e) {
AQUtility.debug(e);
status.code(AjaxStatus.NETWORK_ERROR).message("network error");
}
try {
result = transform(url, data, status);
} catch (Exception e) {
AQUtility.debug(e);
}
if (result == null && data != null) {
status.code(AjaxStatus.TRANSFORM_ERROR).message("transform error");
}
lastStatus = status.getCode();
status.done();
}
protected File getCacheFile() {
return AQUtility.getCacheFile(cacheDir, getCacheUrl());
}
protected boolean isStreamingContent() {
return File.class.equals(type) || XmlPullParser.class.equals(type)
|| InputStream.class.equals(type) || XmlDom.class.equals(type);
}
private File getPreFile() {
boolean pre = isStreamingContent();
File result = null;
if (pre) {
if (targetFile != null) {
result = targetFile;
} else if (fileCache) {
result = getCacheFile();
} else {
File dir = AQUtility.getTempDir();
if (dir == null)
dir = cacheDir;
result = AQUtility.getCacheFile(dir, url);
}
}
if (result != null && !result.exists()) {
try {
result.getParentFile().mkdirs();
result.createNewFile();
} catch (Exception e) {
AQUtility.report(e);
return null;
}
}
return result;
}
private void filePut() {
if (result != null && fileCache) {
byte[] data = status.getData();
try {
if (data != null && status.getSource() == AjaxStatus.NETWORK) {
File file = getCacheFile();
if (!status.getInvalid()) {
// AQUtility.debug("write", url);
filePut(url, result, file, data);
} else {
if (file.exists()) {
file.delete();
}
}
// 3. in function filePut(), add code for remove cache file
// when user called status.invalidate() in case of
// status.getFile() is not null
} else if (status.getFile() != null
&& status.getSource() == AjaxStatus.NETWORK
&& status.getInvalid()) {
File file = status.getFile();
if (file != null && file.exists()) {
file.delete();
}
}
} catch (Exception e) {
AQUtility.debug(e);
}
status.data(null);
}
}
private static String extractUrl(Uri uri) {
String result = uri.getScheme() + "://" + uri.getAuthority()
+ uri.getPath();
String fragment = uri.getFragment();
if (fragment != null)
result += "#" + fragment;
return result;
}
private static Map<String, Object> extractParams(Uri uri) {
Map<String, Object> params = new HashMap<String, Object>();
String[] pairs = uri.getQuery().split("&");
for (String pair : pairs) {
String[] split = pair.split("=");
if (split.length >= 2) {
params.put(split[0], split[1]);
} else if (split.length == 1) {
params.put(split[0], "");
}
}
return params;
}
// added retry logic
private void network(int attempts) throws IOException {
if (attempts <= 1) {
network();
return;
}
for (int i = 0; i < attempts; i++) {
try {
network();
return;
} catch (IOException e) {
if (i == attempts - 1) {
throw e;
}
}
}
}
private void network() throws IOException {
String url = this.url;
Map<String, Object> params = this.params;
// convert get to post request, if url length is too long to be handled
// on web
if (params == null && url.length() > 2000) {
Uri uri = Uri.parse(url);
url = extractUrl(uri);
params = extractParams(uri);
}
url = getNetworkUrl(url);
if (Constants.METHOD_DELETE == method) {
httpDelete(url, headers, status);
} else if (Constants.METHOD_PUT == method) {
httpPut(url, headers, params, status);
} else {
if (Constants.METHOD_POST == method && params == null) {
params = new HashMap<String, Object>();
}
if (params == null) {
httpGet(url, headers, status);
} else {
if (isMultiPart(params)) {
httpMulti(url, headers, params, status);
} else {
httpPost(url, headers, params, status);
}
}
}
}
private void afterWork() {
if (url != null && memCache) {
memPut(url, result);
}
callback();
clear();
}
private static ExecutorService fetchExe;
public static void execute(Runnable job) {
if (fetchExe == null) {
fetchExe = Executors.newFixedThreadPool(NETWORK_POOL);
}
fetchExe.execute(job);
}
/**
* Return the number of active ajax threads. Note that this doesn't
* necessarily correspond to active network connections. Ajax threads might
* be reading a cached url from file system or transforming the response
* after a network transfer.
*
*/
public static int getActiveCount() {
int result = 0;
if (fetchExe instanceof ThreadPoolExecutor) {
result = ((ThreadPoolExecutor) fetchExe).getActiveCount();
}
return result;
}
/**
* Sets the simultaneous network threads limit. Highest limit is 25.
*
* @param limit
* the new network threads limit
*/
public static void setNetworkLimit(int limit) {
NETWORK_POOL = Math.max(1, Math.min(25, limit));
fetchExe = null;
AQUtility.debug("setting network limit", NETWORK_POOL);
}
/**
* Cancel ALL ajax tasks.
*
* Warning: Do not call this method unless you are exiting an application.
*
*/
public static void cancel() {
if (fetchExe != null) {
fetchExe.shutdownNow();
fetchExe = null;
}
BitmapAjaxCallback.clearTasks();
}
private static String patchUrl(String url) {
url = url.replaceAll(" ", "%20").replaceAll("\\|", "%7C");
return url;
}
private void httpGet(String url, Map<String, String> headers,
AjaxStatus status) throws IOException {
AQUtility.debug("get", url);
url = patchUrl(url);
HttpGet get = new HttpGet(url);
httpDo(get, url, headers, status);
}
private void httpDelete(String url, Map<String, String> headers,
AjaxStatus status) throws IOException {
AQUtility.debug("get", url);
url = patchUrl(url);
HttpDelete del = new HttpDelete(url);
httpDo(del, url, headers, status);
}
private void httpPost(String url, Map<String, String> headers,
Map<String, Object> params, AjaxStatus status)
throws ClientProtocolException, IOException {
AQUtility.debug("post", url);
HttpEntityEnclosingRequestBase req = new HttpPost(url);
httpEntity(url, req, headers, params, status);
}
private void httpPut(String url, Map<String, String> headers,
Map<String, Object> params, AjaxStatus status)
throws ClientProtocolException, IOException {
AQUtility.debug("put", url);
HttpEntityEnclosingRequestBase req = new HttpPut(url);
httpEntity(url, req, headers, params, status);
}
private void httpEntity(String url, HttpEntityEnclosingRequestBase req,
Map<String, String> headers, Map<String, Object> params,
AjaxStatus status) throws ClientProtocolException, IOException {
// This setting seems to improve post performance
// http://stackoverflow.com/questions/3046424/http-post-requests-using-httpclient-take-2-seconds-why
req.getParams().setBooleanParameter(
CoreProtocolPNames.USE_EXPECT_CONTINUE, false);
HttpEntity entity = null;
Object value = params.get(AQuery.POST_ENTITY);
if (value instanceof HttpEntity) {
entity = (HttpEntity) value;
} else {
List<NameValuePair> pairs = new ArrayList<NameValuePair>();
for (Map.Entry<String, Object> e : params.entrySet()) {
value = e.getValue();
if (value != null) {
pairs.add(new BasicNameValuePair(e.getKey(), value
.toString()));
}
}
entity = new UrlEncodedFormEntity(pairs, "UTF-8");
}
if (headers != null && !headers.containsKey("Content-Type")) {
headers.put("Content-Type",
"application/x-www-form-urlencoded;charset=UTF-8");
}
req.setEntity(entity);
httpDo(req, url, headers, status);
}
private static SocketFactory ssf;
/**
* Set the secure socket factory.
*
* Could be used to work around SSL certificate not truested issue.
*
* http://stackoverflow.com/questions/1217141/self-signed-ssl-acceptance-
* android
*/
public static void setSSF(SocketFactory sf) {
ssf = sf;
client = null;
}
public static void setReuseHttpClient(boolean reuse) {
REUSE_CLIENT = reuse;
client = null;
}
private static DefaultHttpClient client;
private static DefaultHttpClient getClient() {
if (client == null || !REUSE_CLIENT) {
AQUtility.debug("creating http client");
HttpParams httpParams = new BasicHttpParams();
// httpParams.setParameter(CoreProtocolPNames.PROTOCOL_VERSION,
// HttpVersion.HTTP_1_1);
HttpConnectionParams.setConnectionTimeout(httpParams, NET_TIMEOUT);
HttpConnectionParams.setSoTimeout(httpParams, NET_TIMEOUT);
// ConnManagerParams.setMaxConnectionsPerRoute(httpParams, new
// ConnPerRouteBean(NETWORK_POOL));
ConnManagerParams.setMaxConnectionsPerRoute(httpParams,
new ConnPerRouteBean(25));
// Added this line to avoid issue at:
// http://stackoverflow.com/questions/5358014/android-httpclient-oom-on-4g-lte-htc-thunderbolt
HttpConnectionParams.setSocketBufferSize(httpParams, 8192);
SchemeRegistry registry = new SchemeRegistry();
registry.register(new Scheme("http", PlainSocketFactory
.getSocketFactory(), 80));
registry.register(new Scheme("https",
ssf == null ? SSLSocketFactory.getSocketFactory() : ssf,
443));
ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(
httpParams, registry);
client = new DefaultHttpClient(cm, httpParams);
}
return client;
}
// helper method to support underscore subdomain
private HttpResponse execute(HttpUriRequest hr, DefaultHttpClient client,
HttpContext context) throws ClientProtocolException, IOException {
HttpResponse response = null;
if (hr.getURI().getAuthority().contains("_")) {
URL urlObj = hr.getURI().toURL();
HttpHost host;
if (urlObj.getPort() == -1) {
host = new HttpHost(urlObj.getHost(), 80, urlObj.getProtocol());
} else {
host = new HttpHost(urlObj.getHost(), urlObj.getPort(),
urlObj.getProtocol());
}
response = client.execute(host, hr, context);
} else {
response = client.execute(hr, context);
}
return response;
}
private void httpDo(HttpUriRequest hr, String url,
Map<String, String> headers, AjaxStatus status)
throws ClientProtocolException, IOException {
if (AGENT != null) {
hr.addHeader("User-Agent", AGENT);
}
if (headers != null) {
for (String name : headers.keySet()) {
hr.addHeader(name, headers.get(name));
}
}
if (GZIP
&& (headers == null || !headers.containsKey("Accept-Encoding"))) {
hr.addHeader("Accept-Encoding", "gzip");
}
String cookie = makeCookie();
if (cookie != null) {
hr.addHeader("Cookie", cookie);
}
if (ah != null) {
ah.applyToken(this, hr);
}
DefaultHttpClient client = getClient();
HttpParams hp = hr.getParams();
if (proxy != null)
hp.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
if (timeout > 0) {
hp.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);
hp.setParameter(CoreConnectionPNames.SO_TIMEOUT, timeout);
}
HttpContext context = new BasicHttpContext();
CookieStore cookieStore = new BasicCookieStore();
context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);
request = hr;
if (abort) {
throw new IOException("Aborted");
}
HttpResponse response = null;
try {
// response = client.execute(hr, context);
response = execute(hr, client, context);
} catch (HttpHostConnectException e) {
// if proxy is used, automatically retry without proxy
if (proxy != null) {
AQUtility.debug("proxy failed, retrying without proxy");
hp.setParameter(ConnRoutePNames.DEFAULT_PROXY, null);
// response = client.execute(hr, context);
response = execute(hr, client, context);
} else {
throw e;
}
}
byte[] data = null;
String redirect = url;
int code = response.getStatusLine().getStatusCode();
String message = response.getStatusLine().getReasonPhrase();
String error = null;
HttpEntity entity = response.getEntity();
File file = null;
if (code < 200 || code >= 300) {
InputStream is = null;
try {
if (entity != null) {
is = entity.getContent();
byte[] s = toData(getEncoding(entity), is);
error = new String(s, "UTF-8");
AQUtility.debug("error", error);
}
} catch (Exception e) {
AQUtility.debug(e);
} finally {
AQUtility.close(is);
}
} else {
HttpHost currentHost = (HttpHost) context
.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
HttpUriRequest currentReq = (HttpUriRequest) context
.getAttribute(ExecutionContext.HTTP_REQUEST);
redirect = currentHost.toURI() + currentReq.getURI();
int size = Math.max(32,
Math.min(1024 * 64, (int) entity.getContentLength()));
OutputStream os = null;
InputStream is = null;
try {
file = getPreFile();
if (file == null) {
os = new PredefinedBAOS(size);
} else {
file.createNewFile();
os = new BufferedOutputStream(new FileOutputStream(file));
}
is = entity.getContent();
if ("gzip".equalsIgnoreCase(getEncoding(entity))) {
is = new GZIPInputStream(is);
}
copy(is, os, (int) entity.getContentLength());
os.flush();
if (file == null) {
data = ((PredefinedBAOS) os).toByteArray();
} else {
if (!file.exists() || file.length() == 0) {
file = null;
}
}
// 2. in function httpDo(), add code for IOException processing
// to remove partial content file in cache.
} catch (IOException e) {
if (file != null) {
AQUtility.close(os);
os = null;
file.delete();
}
throw e;
} finally {
AQUtility.close(is);
AQUtility.close(os);
}
}
AQUtility.debug("response", code);
if (data != null) {
AQUtility.debug(data.length, url);
}
status.code(code).message(message).error(error).redirect(redirect)
.time(new Date()).data(data).file(file).client(client)
.context(context).headers(response.getAllHeaders());
}
private String getEncoding(HttpEntity entity) {
if (entity == null)
return null;
Header eheader = entity.getContentEncoding();
if (eheader == null)
return null;
return eheader.getValue();
}
private void copy(InputStream is, OutputStream os, int max)
throws IOException {
Object o = null;
if (progress != null) {
o = progress.get();
}
Progress p = null;
if (o != null) {
p = new Progress(o);
}
AQUtility.copy(is, os, max, p, loadListener);
}
/*
* private void copy(InputStream is, OutputStream os, String encoding, int
* max) throws IOException{
*
* if("gzip".equalsIgnoreCase(encoding)){ is = new GZIPInputStream(is); }
*
* Object o = null;
*
* if(progress != null){ o = progress.get(); }
*
* Progress p = null;
*
* if(o != null){ p = new Progress(o); }
*
* AQUtility.copy(is, os, max, p);
*
*
* }
*/
/**
* Set the authentication type of this request. This method requires API 5+.
*
* @param act
* the current activity
* @param type
* the auth type
* @param account
* the account, such as someone@gmail.com
* @return self
*/
public K auth(Activity act, String type, String account) {
if (android.os.Build.VERSION.SDK_INT >= 5 && type.startsWith("g.")) {
ah = new GoogleHandle(act, type, account);
}
return self();
}
/**
* Set the authentication account handle.
*
* @param handle
* the account handle
* @return self
*/
public K auth(AccountHandle handle) {
ah = handle;
return self();
}
/**
* Gets the url.
*
* @return the url
*/
public String getUrl() {
return url;
}
/**
* Gets the handler.
*
* @return the handler
*/
public Object getHandler() {
if (handler != null)
return handler;
if (whandler == null)
return null;
return whandler.get();
}
/**
* Gets the callback method name.
*
* @return the callback
*/
public String getCallback() {
return callback;
}
private static int lastStatus = 200;
protected static int getLastStatus() {
return lastStatus;
}
/**
* Gets the result. Can be null if ajax is not completed or the ajax call
* failed. This method should only be used after the block() method.
*
* @return the result
*/
public T getResult() {
return result;
}
/**
* Gets the ajax status. This method should only be used after the block()
* method.
*
* @return the status
*/
public AjaxStatus getStatus() {
return status;
}
/**
* Gets the encoding. Default is UTF-8.
*
* @return the encoding
*/
public String getEncoding() {
return encoding;
}
private boolean abort;
/**
* Abort the http request that will interrupt the network transfer. This
* method currently doesn't work with multi-part post.
*
* If no network transfer is involved (eg. response is file cached), this
* method has no effect.
*
*/
public void abort() {
abort = true;
if (request != null && !request.isAborted()) {
request.abort();
}
}
private static final String lineEnd = "\r\n";
private static final String twoHyphens = "--";
private static final String boundary = "*****";
private static boolean isMultiPart(Map<String, Object> params) {
for (Map.Entry<String, Object> entry : params.entrySet()) {
Object value = entry.getValue();
AQUtility.debug(entry.getKey(), value);
if (value instanceof File || value instanceof byte[]
|| value instanceof InputStream)
return true;
}
return false;
}
private void httpMulti(String url, Map<String, String> headers,
Map<String, Object> params, AjaxStatus status) throws IOException {
AQUtility.debug("multipart", url);
HttpURLConnection conn = null;
DataOutputStream dos = null;
URL u = new URL(url);
conn = (HttpURLConnection) u.openConnection();
conn.setInstanceFollowRedirects(false);
conn.setConnectTimeout(NET_TIMEOUT * 4);
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("Content-Type",
"multipart/form-data;charset=utf-8;boundary=" + boundary);
if (headers != null) {
for (String name : headers.keySet()) {
conn.setRequestProperty(name, headers.get(name));
}
}
String cookie = makeCookie();
if (cookie != null) {
conn.setRequestProperty("Cookie", cookie);
}
if (ah != null) {
ah.applyToken(this, conn);
}
dos = new DataOutputStream(conn.getOutputStream());
for (Map.Entry<String, Object> entry : params.entrySet()) {
writeObject(dos, entry.getKey(), entry.getValue());
}
dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
dos.flush();
dos.close();
conn.connect();
int code = conn.getResponseCode();
String message = conn.getResponseMessage();
byte[] data = null;
String encoding = conn.getContentEncoding();
String error = null;
if (code < 200 || code >= 300) {
error = new String(toData(encoding, conn.getErrorStream()), "UTF-8");
AQUtility.debug("error", error);
} else {
data = toData(encoding, conn.getInputStream());
}
AQUtility.debug("response", code);
if (data != null) {
AQUtility.debug(data.length, url);
}
status.code(code).message(message).redirect(url).time(new Date())
.data(data).error(error).client(null);
}
private byte[] toData(String encoding, InputStream is) throws IOException {
boolean gzip = "gzip".equalsIgnoreCase(encoding);
if (gzip) {
is = new GZIPInputStream(is);
}
return AQUtility.toBytes(is);
}
private static void writeObject(DataOutputStream dos, String name,
Object obj) throws IOException {
if (obj == null)
return;
if (obj instanceof File) {
File file = (File) obj;
writeData(dos, name, file.getName(), new FileInputStream(file));
} else if (obj instanceof byte[]) {
writeData(dos, name, name, new ByteArrayInputStream((byte[]) obj));
} else if (obj instanceof InputStream) {
writeData(dos, name, name, (InputStream) obj);
} else {
writeField(dos, name, obj.toString());
}
}
private static void writeData(DataOutputStream dos, String name,
String filename, InputStream is) throws IOException {
dos.writeBytes(twoHyphens + boundary + lineEnd);
dos.writeBytes("Content-Disposition: form-data; name=\"" + name + "\";"
+ " filename=\"" + filename + "\"" + lineEnd);
// added to specify type
dos.writeBytes("Content-Type: application/octet-stream");
dos.writeBytes(lineEnd);
dos.writeBytes("Content-Transfer-Encoding: binary");
dos.writeBytes(lineEnd);
dos.writeBytes(lineEnd);
AQUtility.copy(is, dos);
dos.writeBytes(lineEnd);
}
private static void writeField(DataOutputStream dos, String name,
String value) throws IOException {
dos.writeBytes(twoHyphens + boundary + lineEnd);
dos.writeBytes("Content-Disposition: form-data; name=\"" + name + "\"");
dos.writeBytes(lineEnd);
dos.writeBytes(lineEnd);
byte[] data = value.getBytes("UTF-8");
dos.write(data);
dos.writeBytes(lineEnd);
}
private String makeCookie() {
if (cookies == null || cookies.size() == 0)
return null;
Iterator<String> iter = cookies.keySet().iterator();
StringBuilder sb = new StringBuilder();
while (iter.hasNext()) {
String key = iter.next();
String value = cookies.get(key);
sb.append(key);
sb.append("=");
sb.append(value);
if (iter.hasNext()) {
sb.append("; ");
}
}
return sb.toString();
}
}