/* * 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(); } }