All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.androidquery.callback.AbstractAjaxCallback Maven / Gradle / Ivy

Go to download

Android-Query (AQuery) is a light-weight library for doing asynchronous tasks and manipulating UI elements in Android

The newest version!
/*
 * Copyright 2011 - AndroidQuery.com ([email protected])
 * 
 * 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.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 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 type;
	private Reference whandler;
	private Object handler;
	private String callback;
	private WeakReference progress;
	
	private String url;
	private String networkUrl;
	private Map params;
	private Map headers;
	private Map cookies;
	
	private Transformer transformer;
	
	protected T result;
	
	private int policy = Constants.CACHE_DEFAULT;
	private File cacheDir;
	private File targetFile;
	private 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 act;
	
	private int method = Constants.METHOD_DETECT;
	private HttpUriRequest request;
	
	private boolean uiCallback = true;
	private int retry = 0;
	
	@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;
	}
	
	/**
	 * 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 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(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 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();
		}
		headers.put(name, value);
		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();
		}
		cookies.put(name, value);
		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();
		}
		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 params){
		this.params = (Map) params;
		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(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 = "]*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(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();
						}
					}
					
				}
			}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 extractParams(Uri uri){
		
		Map params = new HashMap(); 
		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 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();
			}
			
			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 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 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 headers, Map 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 headers, Map 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 headers, Map 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 pairs = new ArrayList();
			
			for(Map.Entry 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 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;
		        	}
		        }
	        
	        }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);
		
		
	}
	
	
	/*
	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 [email protected]
	 * @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 params){
		
		for(Map.Entry 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 headers, Map 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 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 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();
		
	}
	
}