package org.nicktate.projectile; import android.content.Context; import android.text.TextUtils; import com.android.volley.DefaultRetryPolicy; import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.RetryPolicy; import com.android.volley.toolbox.HurlStack; import com.android.volley.toolbox.Volley; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; /** * Convenience class used to interface with {@link com.android.volley.toolbox.Volley} requests via the builder paradigm. */ public class Projectile { private static final Object sLock = new Object(); private static Projectile sInstance; // global configs private static RequestQueue sRequestQueue; private static boolean sUseOkHttp = true; private static String sBaseUrl; /** * Cancel requests based on {@link com.android.volley.RequestQueue.RequestFilter} * * @param filter Filter to determine whether or not a request should be cancelled. */ public void cancelRequests(RequestQueue.RequestFilter filter) { sRequestQueue.cancelAll(filter); } /** * Cancel all outgoing requests */ public void cancelAllRequests() { cancelRequests( new RequestQueue.RequestFilter() { @Override public boolean apply(Request<?> request) { return true; } } ); } public RequestBuilder aim(String url) { return new RequestBuilder(this, addBaseUrl(sBaseUrl,url)); } public class RequestBuilder { private final Projectile mProjectile; private String mUrl; private Method mMethod = Method.GET; private Priority mPriority = Priority.NORMAL; private Map<String, String> mHeaders = new HashMap<>(); private Map<String, String> mParams = new HashMap<>(); private RetryPolicy mRetryPolicy; private int mNetworkTimeout = 10000; // 10 seconds private int mRetryCount = 3; private float mBackoffMultiplier = 1f; private Object mTag = null; private boolean mShouldCache = true; public RequestBuilder(Projectile projectile, String url) { mProjectile = projectile; mUrl = url; } public RequestBuilder method(Method method) { mMethod = method; return this; } public RequestBuilder priority(Priority priority) { mPriority = priority; return this; } public RequestBuilder addHeaders(Map<String, String> headers) { if (headers != null) mHeaders.putAll(headers); return this; } public RequestBuilder headers(Map<String, String> headers) { return addHeaders(headers); } public RequestBuilder addHeader(String key, String value) { if (key != null && value != null) mHeaders.put(key, value); return this; } public RequestBuilder header(String key, String value) { return addHeader(key, value); } public RequestBuilder addParams(Map<String, String> params) { if (params != null) mParams.putAll(params); return this; } public RequestBuilder params(Map<String, String> params) { return addParams(params); } public RequestBuilder addParam(String key, String value) { if (key != null && value != null) mParams.put(key, value); return this; } public RequestBuilder param(String key, String value) { return addParam(key, value); } public RequestBuilder retryPolicy(RetryPolicy policy) { mRetryPolicy = policy; return this; } public RequestBuilder timeout(int timeoutMillis) { mNetworkTimeout = timeoutMillis; return this; } public RequestBuilder retryCount(int retryCount) { mRetryCount = retryCount; return this; } public RequestBuilder backoffMultiplier(float backoff) { mBackoffMultiplier = backoff; return this; } public RequestBuilder tag(Object tag) { mTag = tag; return this; } public RequestBuilder shouldCache(boolean shouldCache) { mShouldCache = shouldCache; return this; } public <T> void fire(ResponseListener<T> listener) { if (listener == null) throw new IllegalStateException("You must set a response listener before you fire your request!"); if (mUrl == null) throw new IllegalStateException("Your target url cannot be null for the request"); // volley only uses the getParams() method in PUT or POST requests, so in-case of GET or DELETE, // we must manually add them to query parameters // todo: look into RFC spec about other request types: https://tools.ietf.org/html/rfc2616 if(mMethod == Method.GET || mMethod == Method.DELETE) { mUrl = addQueryParams(mUrl, mParams); } // if custom retry policy was not provided, use DefaultRetryPolicy with configured params if(mRetryPolicy == null) { mRetryPolicy = new DefaultRetryPolicy(mNetworkTimeout, mRetryCount, mBackoffMultiplier); } mProjectile.sRequestQueue.add(new ProjectileRequest<> ( mMethod.getValue(), mUrl, mHeaders, mParams, listener, mPriority.getValue(), mRetryPolicy, mTag, mShouldCache )); } } /** * Takes map of params and adds them to the URL, taking into account any existing query parameters. * * @param url * @param params * @return String appended with URL encoded query parameters. */ private static String addQueryParams(String url, Map<String, String> params) { if(params == null || params.isEmpty()) return url; boolean isFirst = true; // re-adding exiting query parameters to ensure they are URL encoded if(url.contains("?")) { String[] split = url.split("\\?"); url = split[0]; String[] queryPairs = split[1].split("&"); for(String tuple : queryPairs) { String[] queryParam = tuple.split("="); if(queryParam.length == 2) { try { url += isFirst ? "?" : "&"; url += queryParam[0] + "=" + URLEncoder.encode(URLDecoder.decode(queryParam[1], "utf-8"), "utf-8"); isFirst = false; } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } // if split is length 1, then assume empty value else if(queryParam.length == 1) { url += isFirst ? "?" : "&"; url += queryParam[0] + "="; isFirst = false; } } } // add any passed in parameters to the query for(Map.Entry<String, String> entry : params.entrySet()) { try { url += isFirst ? "?" : "&"; url += entry.getKey() + "=" + URLEncoder.encode(URLDecoder.decode(entry.getValue(), "utf-8"), "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } isFirst = false; } return url; } /** * Add a base url to the provided relative url * * @param baseUrl * @param relativeUrl * @return */ private static String addBaseUrl(String baseUrl, String relativeUrl) { if(TextUtils.isEmpty(baseUrl)) { return relativeUrl; } if(TextUtils.isEmpty(relativeUrl)) { return baseUrl; } // network-path URL // todo: need to determine whether current schema versus just returning http if(relativeUrl.startsWith("//")) { return "http:" + relativeUrl; } String fullUrl; if(baseUrl.endsWith("/")) { if(relativeUrl.startsWith("/")) { fullUrl = baseUrl + relativeUrl.substring(1); } else { fullUrl = baseUrl + relativeUrl; } } else { if(relativeUrl.startsWith("/")) { fullUrl = baseUrl + relativeUrl; } else { fullUrl = baseUrl + "/" + relativeUrl; } } return fullUrl; } public static Projectile draw(Context ctx) { synchronized (sLock) { if (sInstance == null) { sInstance = new Builder(ctx).build(); } } return sInstance; } /** * Set whether or not to use {@link com.squareup.okhttp.OkHttpClient} for url requests * * @param shouldUse If true, use okHttp for requests; else use standard URLConnection */ public static void useOkHttp(boolean shouldUse) { sUseOkHttp = shouldUse; } /** * Set whether or not to use {@link com.squareup.okhttp.OkHttpClient} for url requests * * @param baseUrl Base url for requests */ public static void setBaseUrl(String baseUrl) { sBaseUrl = baseUrl; } /** * Set a custom queue to use for requests * * @param queue RequestQueue to use for managing requests */ public static void setRequestQueue(RequestQueue queue) { sRequestQueue = queue; } private static class Builder { private Context ctx; public Builder(Context ctx) { if (ctx == null) throw new IllegalArgumentException("Context must not be null!"); this.ctx = ctx.getApplicationContext(); } public Projectile build() { Context context = ctx; return new Projectile(context); } } private Projectile(Context ctx) { if(sRequestQueue == null) sRequestQueue = Volley.newRequestQueue(ctx, sUseOkHttp ? new OkHttpStack() : new HurlStack()); } }