diff options
Diffstat (limited to 'libs/networking/WordPressNetworking/src/main')
7 files changed, 629 insertions, 0 deletions
diff --git a/libs/networking/WordPressNetworking/src/main/AndroidManifest.xml b/libs/networking/WordPressNetworking/src/main/AndroidManifest.xml new file mode 100644 index 000000000..f19a8bd89 --- /dev/null +++ b/libs/networking/WordPressNetworking/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.wordpress.android.networking"> +</manifest> diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/Authenticator.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/Authenticator.java new file mode 100644 index 000000000..cf7bd78bb --- /dev/null +++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/Authenticator.java @@ -0,0 +1,13 @@ +package org.wordpress.android.networking; + +/** + * Interface that provides a method that should perform the necessary task to make sure + * the provided AuthenticatorRequest will be authenticated. + * + * The Authenticator must call AuthenticatorRequest.send() when it has completed its operations. For + * convenience the AuthenticatorRequest class provides AuthenticatorRequest.setAccessToken so the Authenticator can + * easily update the access token. + */ +public interface Authenticator { + void authenticate(final AuthenticatorRequest authenticatorRequest); +} diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java new file mode 100644 index 000000000..40d2af7f3 --- /dev/null +++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/AuthenticatorRequest.java @@ -0,0 +1,96 @@ +package org.wordpress.android.networking; + +import com.android.volley.VolleyError; +import com.wordpress.rest.Oauth; +import com.wordpress.rest.RestClient; +import com.wordpress.rest.RestRequest; +import com.wordpress.rest.RestRequest.ErrorListener; + +/** + * Encapsulates the behaviour for asking the Authenticator for an access token. This + * allows the request maker to disregard the authentication state when making requests. + */ +public class AuthenticatorRequest { + private RestRequest mRequest; + private RestRequest.ErrorListener mListener; + private RestClient mRestClient; + private Authenticator mAuthenticator; + + protected AuthenticatorRequest(RestRequest request, ErrorListener listener, RestClient restClient, + Authenticator authenticator) { + mRequest = request; + mListener = listener; + mRestClient = restClient; + mAuthenticator = authenticator; + } + + public String getSiteId() { + return extractSiteIdFromUrl(mRestClient.getEndpointURL(), mRequest.getUrl()); + } + + /** + * Parse out the site ID from an URL. + * Note: For batch REST API calls, only the first siteID is returned + * + * @return The site ID + */ + public static String extractSiteIdFromUrl(String restEndpointUrl, String url) { + if (url == null) { + return null; + } + + final String sitePrefix = restEndpointUrl.endsWith("/") ? restEndpointUrl + "sites/" : restEndpointUrl + "/sites/"; + final String batchCallPrefix = restEndpointUrl.endsWith("/") ? restEndpointUrl + "batch/?urls%5B%5D=%2Fsites%2F" + : restEndpointUrl + "/batch/?urls%5B%5D=%2Fsites%2F"; + + if (url.startsWith(sitePrefix) && !sitePrefix.equals(url)) { + int marker = sitePrefix.length(); + if (url.indexOf("/", marker) < marker) { + return null; + } + return url.substring(marker, url.indexOf("/", marker)); + } else if (url.startsWith(batchCallPrefix) && !batchCallPrefix.equals(url)) { + int marker = batchCallPrefix.length(); + if (url.indexOf("%2F", marker) < marker) { + return null; + } + return url.substring(marker, url.indexOf("%2F", marker)); + } + + // not a sites/$siteId request or a batch request + return null; + } + + /** + * Attempt to send the request, checks to see if we have an access token and if not + * asks the Authenticator to authenticate the request. + * + * If no Authenticator is provided the request is always sent. + */ + protected void send(){ + if (mAuthenticator == null) { + mRestClient.send(mRequest); + } else { + mAuthenticator.authenticate(this); + } + } + + public void sendWithAccessToken(String token){ + mRequest.setAccessToken(token); + mRestClient.send(mRequest); + } + + public void sendWithAccessToken(Oauth.Token token){ + sendWithAccessToken(token.toString()); + } + + /** + * If an access token cannot be obtained the request can be aborted and the + * handler's onFailure method is called + */ + public void abort(VolleyError error){ + if (mListener != null) { + mListener.onErrorResponse(error); + } + } +} diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactory.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactory.java new file mode 100644 index 000000000..492b2b99f --- /dev/null +++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactory.java @@ -0,0 +1,19 @@ +package org.wordpress.android.networking; + +import com.android.volley.RequestQueue; +import com.wordpress.rest.RestClient; + +public class RestClientFactory { + private static RestClientFactoryAbstract sFactory; + + public static RestClient instantiate(RequestQueue queue) { + return instantiate(queue, RestClient.REST_CLIENT_VERSIONS.V1); + } + + public static RestClient instantiate(RequestQueue queue, RestClient.REST_CLIENT_VERSIONS version) { + if (sFactory == null) { + sFactory = new RestClientFactoryDefault(); + } + return sFactory.make(queue, version); + } +} diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java new file mode 100644 index 000000000..799a5a8e8 --- /dev/null +++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryAbstract.java @@ -0,0 +1,9 @@ +package org.wordpress.android.networking; + +import com.android.volley.RequestQueue; +import com.wordpress.rest.RestClient; + +public interface RestClientFactoryAbstract { + public RestClient make(RequestQueue queue); + public RestClient make(RequestQueue queue, RestClient.REST_CLIENT_VERSIONS version); +} diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java new file mode 100644 index 000000000..b87d4b40f --- /dev/null +++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientFactoryDefault.java @@ -0,0 +1,14 @@ +package org.wordpress.android.networking; + +import com.android.volley.RequestQueue; +import com.wordpress.rest.RestClient; + +public class RestClientFactoryDefault implements RestClientFactoryAbstract { + public RestClient make(RequestQueue queue) { + return new RestClient(queue); + } + + public RestClient make(RequestQueue queue, RestClient.REST_CLIENT_VERSIONS version) { + return new RestClient(queue, version); + } +} diff --git a/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java new file mode 100644 index 000000000..6e06cfeee --- /dev/null +++ b/libs/networking/WordPressNetworking/src/main/java/org/wordpress/android/networking/RestClientUtils.java @@ -0,0 +1,475 @@ +/** + * Interface to the WordPress.com REST API. + */ +package org.wordpress.android.networking; + +import android.content.Context; +import android.net.Uri; +import android.text.TextUtils; + +import com.android.volley.DefaultRetryPolicy; +import com.android.volley.Request; +import com.android.volley.Request.Method; +import com.android.volley.RequestQueue; +import com.android.volley.RetryPolicy; +import com.android.volley.toolbox.RequestFuture; +import com.wordpress.rest.JsonRestRequest; +import com.wordpress.rest.RestClient; +import com.wordpress.rest.RestRequest; +import com.wordpress.rest.RestRequest.ErrorListener; +import com.wordpress.rest.RestRequest.Listener; + +import org.json.JSONObject; +import org.wordpress.android.util.LanguageUtils; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +public class RestClientUtils { + private static final String NOTIFICATION_FIELDS = "id,type,unread,body,subject,timestamp,meta"; + private static final String COMMENT_REPLY_CONTENT_FIELD = "content"; + private static String sUserAgent = "WordPress Networking Android"; + + private RestClient mRestClient; + private Authenticator mAuthenticator; + private Context mContext; + + /** + * Socket timeout in milliseconds for rest requests + */ + public static final int REST_TIMEOUT_MS = 30000; + + /** + * Default number of retries for POST rest requests + */ + public static final int REST_MAX_RETRIES_POST = 0; + + /** + * Default number of retries for GET rest requests + */ + public static final int REST_MAX_RETRIES_GET = 3; + + /** + * Default backoff multiplier for rest requests + */ + public static final float REST_BACKOFF_MULT = 2f; + + public static void setUserAgent(String userAgent) { + sUserAgent = userAgent; + } + + public RestClientUtils(Context context, RequestQueue queue, Authenticator authenticator, RestRequest.OnAuthFailedListener onAuthFailedListener) { + this(context, queue, authenticator, onAuthFailedListener, RestClient.REST_CLIENT_VERSIONS.V1); + } + + public RestClientUtils(Context context, RequestQueue queue, Authenticator authenticator, RestRequest.OnAuthFailedListener onAuthFailedListener, RestClient.REST_CLIENT_VERSIONS version) { + // load an existing access token from prefs if we have one + mContext = context; + mAuthenticator = authenticator; + mRestClient = RestClientFactory.instantiate(queue, version); + if (onAuthFailedListener != null) { + mRestClient.setOnAuthFailedListener(onAuthFailedListener); + } + mRestClient.setUserAgent(sUserAgent); + } + + public Authenticator getAuthenticator() { + return mAuthenticator; + } + + public RestClient getRestClient() { + return mRestClient; + } + + public void getCategories(String siteId, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/categories", siteId); + get(path, null, null, listener, errorListener); + } + + /** + * get a list of recent comments + * <p/> + * https://developer.wordpress.com/docs/api/1.1/get/sites/%24site/comments/ + */ + public void getComments(String siteId, Map<String, String> params, final Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/comments", siteId); + get(path, params, null, listener, errorListener); + } + + /** + * Reply to a comment + * <p/> + * https://developer.wordpress.com/docs/api/1/post/sites/%24site/posts/%24post_ID/replies/new/ + */ + public void replyToComment(String reply, String path, Listener listener, ErrorListener errorListener) { + Map<String, String> params = new HashMap<String, String>(); + params.put(COMMENT_REPLY_CONTENT_FIELD, reply); + post(path, params, null, listener, errorListener); + } + + /** + * Reply to a comment. + * <p/> + * https://developer.wordpress.com/docs/api/1/post/sites/%24site/posts/%24post_ID/replies/new/ + */ + public void replyToComment(long siteId, long commentId, String content, Listener listener, + ErrorListener errorListener) { + Map<String, String> params = new HashMap<String, String>(); + params.put(COMMENT_REPLY_CONTENT_FIELD, content); + String path = String.format("sites/%d/comments/%d/replies/new", siteId, commentId); + post(path, params, null, listener, errorListener); + } + + /** + * Follow a site given an ID or domain + * <p/> + * https://developer.wordpress.com/docs/api/1/post/sites/%24site/follows/new/ + */ + public void followSite(String siteId, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/follows/new", siteId); + post(path, listener, errorListener); + } + + /** + * Unfollow a site given an ID or domain + * <p/> + * https://developer.wordpress.com/docs/api/1/post/sites/%24site/follows/mine/delete/ + */ + public void unfollowSite(String siteId, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/follows/mine/delete", siteId); + post(path, listener, errorListener); + } + + /** + * Get notifications with the provided params. + * <p/> + * https://developer.wordpress.com/docs/api/1/get/notifications/ + */ + public void getNotifications(Map<String, String> params, Listener listener, ErrorListener errorListener) { + params.put("number", "40"); + params.put("num_note_items", "20"); + params.put("fields", NOTIFICATION_FIELDS); + get("notifications", params, null, listener, errorListener); + } + + /** + * Get notifications with default params. + * <p/> + * https://developer.wordpress.com/docs/api/1/get/notifications/ + */ + public void getNotifications(Listener listener, ErrorListener errorListener) { + getNotifications(new HashMap<String, String>(), listener, errorListener); + } + + /** + * Update the seen timestamp. + * <p/> + * https://developer.wordpress.com/docs/api/1/post/notifications/seen + */ + public void markNotificationsSeen(String timestamp, Listener listener, ErrorListener errorListener) { + Map<String, String> params = new HashMap<String, String>(); + params.put("time", timestamp); + post("notifications/seen", params, null, listener, errorListener); + } + + /** + * Moderate a comment. + * <p/> + * http://developer.wordpress.com/docs/api/1/sites/%24site/comments/%24comment_ID/ + */ + public void moderateComment(String siteId, String commentId, String status, Listener listener, + ErrorListener errorListener) { + Map<String, String> params = new HashMap<String, String>(); + params.put("status", status); + String path = String.format("sites/%s/comments/%s/", siteId, commentId); + post(path, params, null, listener, errorListener); + } + + /** + * Edit the content of a comment + */ + public void editCommentContent(long siteId, long commentId, String content, Listener listener, + ErrorListener errorListener) { + Map<String, String> params = new HashMap<String, String>(); + params.put("content", content); + String path = String.format("sites/%d/comments/%d/", siteId, commentId); + post(path, params, null, listener, errorListener); + } + + /** + * Like or unlike a comment. + */ + public void likeComment(String siteId, String commentId, boolean isLiked, Listener listener, + ErrorListener errorListener) { + Map<String, String> params = new HashMap<String, String>(); + String path = String.format("sites/%s/comments/%s/likes/", siteId, commentId); + + if (!isLiked) { + path += "mine/delete"; + } else { + path += "new"; + } + + post(path, params, null, listener, errorListener); + } + + public void getFreeSearchThemes(String siteId, int limit, int offset, String searchTerm, Listener listener, ErrorListener errorListener) { + getSearchThemes("free", siteId, limit, offset, searchTerm, listener, errorListener); + } + + public void getSearchThemes(String tier, String siteId, int limit, int offset, String searchTerm, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/themes?tier=" + tier + "&number=%d&offset=%d&search=%s", siteId, limit, offset, searchTerm); + get(path, listener, errorListener); + } + + public void getFreeThemes(String siteId, int limit, int offset, Listener listener, ErrorListener errorListener) { + getThemes("free", siteId, limit, offset, listener, errorListener); + } + + public void getPurchasedThemes(String siteId, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/themes/purchased", siteId); + get(path, listener, errorListener); + } + + /** + * Get all a site's themes + */ + public void getThemes(String tier, String siteId, int limit, int offset, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/themes/?tier=" + tier + "&number=%d&offset=%d", siteId, limit, offset); + get(path, listener, errorListener); + } + + /** + * Set a site's theme + */ + public void setTheme(String siteId, String themeId, Listener listener, ErrorListener errorListener) { + Map<String, String> params = new HashMap<>(); + params.put("theme", themeId); + String path = String.format("sites/%s/themes/mine", siteId); + post(path, params, null, listener, errorListener); + } + + /** + * Get a site's current theme + */ + public void getCurrentTheme(String siteId, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/themes/mine", siteId); + get(path, listener, errorListener); + } + + public void getGeneralSettings(String siteId, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/settings", siteId); + Map<String, String> params = new HashMap<String, String>(); + get(path, params, null, listener, errorListener); + } + + public void setGeneralSiteSettings(String siteId, Listener listener, ErrorListener errorListener, + Map<String, String> params) { + String path = String.format("sites/%s/settings", siteId); + post(path, params, null, listener, errorListener); + } + + /** + * Delete a site + */ + public void deleteSite(String siteId, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/delete", siteId); + post(path, listener, errorListener); + } + + public void getSitePurchases(String siteId, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/purchases", siteId); + get(path, listener, errorListener); + } + + public void exportContentAll(String siteId, Listener listener, ErrorListener errorListener) { + String path = String.format("sites/%s/exports/start", siteId); + post(path, listener, errorListener); + } + + public void sendLoginEmail(Map<String, String> params, Listener listener, ErrorListener errorListener) { + post("auth/send-login-email", params, null, listener, errorListener); + } + + public void isAvailable(String email, Listener listener, ErrorListener errorListener) { + String path = String.format("is-available/email?q=%s", email); + get(path, listener, errorListener); + } + + /** + * Make GET request + */ + public Request<JSONObject> get(String path, Listener listener, ErrorListener errorListener) { + return get(path, null, null, listener, errorListener); + } + + /** + * Make GET request with params + */ + public Request<JSONObject> get(String path, Map<String, String> params, RetryPolicy retryPolicy, Listener listener, + ErrorListener errorListener) { + // turn params into querystring + HashMap<String, String> paramsWithLocale = getRestLocaleParams(mContext); + if (params != null) { + paramsWithLocale.putAll(params); + } + + String realPath = getSanitizedPath(path); + if (TextUtils.isEmpty(realPath)) { + realPath = path; + } + paramsWithLocale.putAll(getSanitizedParameters(path)); + + RestRequest request = mRestClient.makeRequest(Method.GET, mRestClient.getAbsoluteURL(realPath, paramsWithLocale), null, + listener, errorListener); + + if (retryPolicy == null) { + retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_GET, REST_BACKOFF_MULT); + } + request.setRetryPolicy(retryPolicy); + AuthenticatorRequest authCheck = new AuthenticatorRequest(request, errorListener, mRestClient, mAuthenticator); + authCheck.send(); + return request; + } + + /** + * Make Synchronous GET request + * + * @throws TimeoutException + * @throws ExecutionException + * @throws InterruptedException + */ + public JSONObject getSynchronous(String path) throws InterruptedException, ExecutionException, TimeoutException { + return getSynchronous(path, null, null); + } + + /** + * Make Synchronous GET request with params + * + * @throws TimeoutException + * @throws ExecutionException + * @throws InterruptedException + */ + public JSONObject getSynchronous(String path, Map<String, String> params, RetryPolicy retryPolicy) + throws InterruptedException, ExecutionException, TimeoutException { + RequestFuture<JSONObject> future = RequestFuture.newFuture(); + + HashMap<String, String> paramsWithLocale = getRestLocaleParams(mContext); + if (params != null) { + paramsWithLocale.putAll(params); + } + + String realPath = getSanitizedPath(path); + if (TextUtils.isEmpty(realPath)) { + realPath = path; + } + paramsWithLocale.putAll(getSanitizedParameters(path)); + + RestRequest request = mRestClient.makeRequest(Method.GET, mRestClient.getAbsoluteURL(realPath, paramsWithLocale), null, future, future); + + if (retryPolicy == null) { + retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_GET, REST_BACKOFF_MULT); + } + request.setRetryPolicy(retryPolicy); + + AuthenticatorRequest authCheck = new AuthenticatorRequest(request, null, mRestClient, mAuthenticator); + authCheck.send(); //this insert the request into the queue. //TODO: Verify that everything is OK on REST calls without a valid token + JSONObject response = future.get(); + return response; + } + + /** + * Make POST request + */ + public void post(String path, Listener listener, ErrorListener errorListener) { + Map<String, String> params = null; + post(path, params, null, listener, errorListener); + } + + /** + * Make POST request with params + */ + public void post(final String path, Map<String, String> params, RetryPolicy retryPolicy, Listener listener, + ErrorListener errorListener) { + final RestRequest request = mRestClient.makeRequest(Method.POST, mRestClient.getAbsoluteURL(path, getRestLocaleParams(mContext)), params, + listener, errorListener); + if (retryPolicy == null) { + retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_POST, + REST_BACKOFF_MULT); //Do not retry on failure + } + request.setRetryPolicy(retryPolicy); + AuthenticatorRequest authCheck = new AuthenticatorRequest(request, errorListener, mRestClient, mAuthenticator); + authCheck.send(); + } + + + /** + * Make a JSON POST request + */ + public void post(final String path, JSONObject params, RetryPolicy retryPolicy, Listener listener, + ErrorListener errorListener) { + final JsonRestRequest request = mRestClient.makeRequest(mRestClient.getAbsoluteURL(path, getRestLocaleParams(mContext)), params, + listener, errorListener); + if (retryPolicy == null) { + retryPolicy = new DefaultRetryPolicy(REST_TIMEOUT_MS, REST_MAX_RETRIES_POST, + REST_BACKOFF_MULT); //Do not retry on failure + } + request.setRetryPolicy(retryPolicy); + AuthenticatorRequest authCheck = new AuthenticatorRequest(request, errorListener, mRestClient, mAuthenticator); + authCheck.send(); + } + + /** + * Takes a URL and returns the path within, or an empty string (not null) + */ + public static String getSanitizedPath(String unsanitizedPath){ + if (unsanitizedPath != null) { + int qmarkPos = unsanitizedPath.indexOf('?'); + if (qmarkPos > -1) { //strip any querystring params off this to obtain the path + return unsanitizedPath.substring(0, qmarkPos+1); + } else { + // return the string as is, consider the whole string as the path + return unsanitizedPath; + } + } + return ""; + } + + /** + * Takes a URL with query strings and returns a Map of query string values. + */ + public static HashMap<String, String> getSanitizedParameters(String unsanitizedPath){ + HashMap<String, String> queryParams = new HashMap<>(); + + Uri uri = Uri.parse(unsanitizedPath); + + if (uri.getQueryParameterNames() != null ) { + Iterator iter = uri.getQueryParameterNames().iterator(); + while (iter.hasNext()) { + String name = (String)iter.next(); + String value = uri.getQueryParameter(name); + queryParams.put(name, value); + } + } + + return queryParams; + } + + /** + * Returns locale parameter used in REST calls which require the response to be localized + */ + public static HashMap<String, String> getRestLocaleParams(Context context) { + HashMap<String, String> params = new HashMap<>(); + String deviceLanguageCode = LanguageUtils.getCurrentDeviceLanguageCode(context); + if (!TextUtils.isEmpty(deviceLanguageCode)) { + //patch locale if it's any of the deprecated codes as can be read in Locale.java source code: + deviceLanguageCode = LanguageUtils.patchDeviceLanguageCode(deviceLanguageCode); + params.put("locale", deviceLanguageCode); + } + return params; + } + +} |